Java多线程:静态synchronized方法和成员synchronized方法能否同时执行

背景

疫情期间,在家刷面经,因此通过了某大厂的一面二面。以为自己就此可以升职加薪,出任CTO,迎娶白富美,走上人生巅峰了……然而,三面的面试官亲切地问了我几个小问题,轻轻一巴掌把我拍落进尘埃。其中一个问题就是这个。

面试官:假设有一个类,这个类有一个成员方法a,被synchronized修饰。同时这个类还有一个静态方法b,也被synchronized修饰。现在,加入线程1调用成员方法a,线程2调用静态方法b,这两个方法能否被同时执行?

这是一道很基础的题,应该认真学习的毕业生都会……然而,我可能就是不认真学习的那种。工作近两年,从事Android开发,平时多线程接触得很少,这个简单的问题就把我问懵了。

我:应该……不会同时执行把。
面试官:为什么呢?
我:因为静态方法是属于类的,会把整个类锁住。
面试官:……你多线程这一块以后需要加强。

然后,就没有然后了……

实践

从面试官的态度,我猜测正确答案应该是会同时执行的。那实际情况是不是呢?我面试完后,写了实际代码测试。代码如下:

import java.io.*;

class Main  
{
	public static void main (String[] args) throws java.lang.Exception
	{
		Test test = new Test();
		// 线程1执行成员方法
		Thread thread1 = new Thread(new Runnable(){
		    
		    @Override
		    public void run(){
		        test.a();
		    }
		});
		
		
		// 线程2执行静态方法
		Thread thread2 = new Thread(new Runnable(){
		    
		    @Override
		    public void run(){
		        Test.b();
		    }
		});
		
		// 同时开启两个线程
		thread1.start();
		thread2.start();
		
	}
}


// 测试类
class Test {
    
    // 同步成员方法
    public synchronized void a(){
        for (int i = 0; i < 200 ; i++){
            System.out.println("run a:" + i);
        } 
        System.out.println("a is end!");
    }
    
    // 同步静态方法
    public static synchronized void b(){
        for (int i = 0; i < 200 ; i++){
            System.out.println("b:" + i);
        } 
        System.out.println("b is end!");
    }
}

完整输出太长,只贴其中一段:

……
run a:81
run a:82
run a:83
run a:84
b:0
b:1
b:2
b:3
b:4
……

在a没有执行完成的时候,b已经开始执行了。b执行一段时间后,又开始执行a……反复交替,同时执行。

结论:静态synchronized方法和成员synchronized方法可以同时执行!可以同时执行!可以同时执行!

原因

知其然,更要知其所以然。想知道能同时执行的原因,为什么不问问神奇的度娘呢?

所有的非静态同步方法用的都是同一把锁:实例对象本身;
而所有的静态同步方法用的也是同一把锁:类对象本身
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。

噢~我懂了!代码稍微修改,把两个方法同时改成成员个方法,或者两个方法同时改成静态方法,就会按顺序执行了。一个是成员,一个是静态,相当于用的是两把不同的锁,所以可以同时执行。

这里的类对象是啥意思呢?
将Test类稍微修改,再次运行

// 测试类
class Test {
    
    // 方法不同步
    public void a(){
    	// 同步语句块,锁住getClass()
    	synchronized(getClass()){
            for (int i = 0; i < 200 ; i++){
                System.out.println("run a:" + i);
            } 
            System.out.println("a is end!");
        }
    }
    
    // 同步静态方法
    public static synchronized void b(){
        for (int i = 0; i < 200 ; i++){
            System.out.println("b:" + i);
        } 
        System.out.println("b is end!");
    }
}

运行发现,两个方法是按顺序执行的。原来,锁住的类对象,即是指getClass()方法获取到的对象!

将Test类再修改一下,b改成成员方法,a锁住this,会是什么效果呢?

// 测试类
class Test {
    
    // 方法不同步
    public void a(){
    	// 同步语句块,锁住getClass()
    	synchronized(this){
            for (int i = 0; i < 200 ; i++){
                System.out.println("run a:" + i);
            } 
            System.out.println("a is end!");
        }
    }
    
    // 同步成员方法
    public synchronized void b(){
        for (int i = 0; i < 200 ; i++){
            System.out.println("b:" + i);
        } 
        System.out.println("b is end!");
    }
}

运行发现,a和b也是顺序执行的。由此,上面结论可以稍微修改如下:

同步成员方法,相当于synchronized(this)语句块,锁住的是this对象。
同步静态方法,相当于synchronized(getClass())语句块,锁住的是getClass()对象。
this和getClass()不是同一个对象。因此同步成员方法,和同步静态方法部存在竞态条件,可以同时执行。

扩展

为什么锁的不是同一个对象,就能会同时执行呢?
我之前一直有一个错误的理解: 锁是线程添加给对象的,用来保证线程执行的正确性。
然而,仔细思考之后,才发现这种思想不对。正确的理解应该是:锁是对象添加给自己的,用来保证自身数据的正确性。
java之中,synchronized关键字锁定的对象有几种状态,分别是:无锁、偏向锁、轻量级锁和重量级锁。变化流程如下:

  • 无锁: 没有线程访问同步资源,此时对象处于无锁状态。
  • 偏向锁: 有一个线程访问同步资源,则该对象标记为偏向锁,并记录线程id。下次这个线程再访问同步资源时,可以直接给它。
  • 轻量锁: 当另一个线程来访问同步资源时,发现该对象已经被标记为偏向锁,但线程id不是自己的。则撤销偏向锁,升级为轻量锁(如果撤销完成后线程没有死的话)。轻量锁会再自己线程的栈帧中创建锁记录,存储对象头MarkWord。需要资源时会尝试将对象头的指针指向自己的锁记录,如果指向失败则自旋等待。成功则获取锁,执行同步方法,完成后再释放锁。
  • 重量锁: 如果有多个线程同时竞争资源,线程自旋失败,则膨胀为重量级锁,线程进入内核态,当前线程释放锁之后,需要唤醒其他线程。

举个栗子:

假设我有个对象(我在无中生有,暗度陈仓……),在银行窗口工作。去办理业务的客户,需要她填表盖章。而她是个脸盲,通过脸分不清用户谁是谁,只能查看身份证(Thread Id)来确定用户身份。

  • 不加锁: 一开始,有人递身份证去她就填表盖章。一次只有一个客户的时候,还没啥问题。后来多个客户来了,一股脑把身份证递给她,让她填表。她就可能把客户A的信息和客户B的信息弄混,出现同步问题。

  • 无锁: 因为产生同步问题,她就准别了一块“请勿打扰”的牌子。没有客户的时候,她就把牌子藏着,无聊地数手指。随时只要客户过来,她都能马上接待。

  • 偏向锁: 这天来了一个客户,客户需要在她这里填表,然后到其他窗口审核,再拿到她这里补充信息,然后又到其他窗口办理……她就记下了这个用户的身份证号,下次这个用户来,直接给他继续填表,补充信息。

  • 轻量级锁: 过了一会,第二个客户过来了,让她帮填表。她发现身份证不是自己记录的那个用户。就说:“我不记得哪些表是你们谁的,干脆你们每人领一份表格自己拿着,我有空的时候再递给我吧。”于是她将表格给每个客户一份,自己只记录在填哪份表格(MarkWord存到线程栈帧中,对象头只存指针)。后面又来了几个客户,都是让他么自己领表。她在填表期间把“请勿打扰”的牌子挂起来,让后面来找她的客户原地等待。填完之后把牌子放下,再接收后面的表格。

  • 重量级锁: 后来客户越来越多,有的客户等得不赖烦了,在她填表期间,也把身份证往窗口塞。她一看继续让大家原地等待,客户的情绪就要爆发了。于是喊道:“填表期间,大家先去旁边椅子上休息,我填好了再叫你们。”于是原地等待的客户就阻塞再旁边椅子上,她弄好之后,再把“请勿打扰”的牌子放下了,然后还要提醒旁边等待的客户,让他们过来填表。

多线程操作和以上流程类似,每个对象都有一块“请勿打扰”的牌子,当她挂起牌子之后,她就不接待其他用户,要等她处理往当前的事情,摘下牌子才继续执行。

如果我有两个对象(她们长得很美,而我想得很美),都在银行工作,分别接待客户A和客户B,她们当然是可以同时工作,互不影响的。

参考:https://blog.csdn.net/lengxiao1993/article/details/81568130
https://blog.csdn.net/u012722531/article/details/78244786


版权声明:本文为luozaifei1997原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。