当前位置: 首页 > 科技观察

面试无法解释Synchronized底层原理?推荐阅读这篇文章!

时间:2023-03-20 00:46:07 科技观察

1、Synchronized的基本使用Synchronized是Java中解决并发问题最常用的方法之一,也是最简单的方法。synchronized主要有3个作用:保证线程互斥访问同步代码,保证共享变量的修改及时可见,有效解决重排序问题。从语法上看,Synchronized的三种用法:(1)修改普通方法(2)修改静态方法(3)修改代码块接下来我将用几个例子程序来说明这三种使用方法(为了对比,三段代码除了使用Synchronized的方式不同外,基本相同)1.不带同步:代码段1:packagecom.paddx.test.concurrent;publicclassSynchronizedTest{publicvoidmethod1(){System.out.println("Method1start");try{System.out.println("Method1execute");Thread.sleep(3000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Method1end");}publicvoidmethod2(){System.out.println("Method2start");try{System.out.println("Method2execute");Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out。println("Method2end");}publicstaticvoidmain(String[]args){finalSynchronizedTesttest=newSynchronizedTest();newThread(newRunnable(){@Overridepublicvoidrun(){test.method1();}}).start();newThread(newRunnable(){@Overridepublicvoidrun(){test.method2();}}).start();}}执行结果如下,线程1和线程2同时进入执行状态,线程2执行速度比线程1,所以线程2先执行完毕。在这个过程中,线程1和线程2是同时执行的。方法一启动方法一执行方法二启动方法二执行方法二结束方法一结束2、同步常用方法:代码段2:packagecom.paddx.test.concurrent;publicclassSynchronizedTest{publicsynchronizedvoidmethod1(){System.out.println("Method1start");try{System.out.println("Method1execute");Thread.sleep(3000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Method1end");}publicsynchronizedvoidmethod2(){System.out.println("Method2start");try{System.out.println("Method2execute");Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out。println("Method2end");}publicstaticvoidmain(String[]args){finalSynchronizedTesttest=newSynchronizedTest();newThread(newRunnable(){@Overridepublicvoidrun(){test.method1();}}).start();newThread(newRunnable(){@Overridepublicvoidrun(){test.method2();}}).start();}}执行结果如下。对比代码段1,很明显线程2需要等待线程1的method1执行完Complete才开始执行method2方法方法1startMethod1executeMethod1endMethod2startMethod2executeMethod2end3。静态方法(类)同步代码段3:packagecom.paddx.test.concurrent;publicclassSynchronizedTest{publicstaticsynchronizedvoidmethod1(){System.out.println("Method1start");try{System.out.println("Method1execute");Thread.sleep(3000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Method1end");}publicstaticsynchronizedvoidmethod2(){System.out.println("Method2start");try{System.out.println("Method2execute");Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Method2end");}publicstaticvoidmain(String[]args){finalSynchronizedTesttest=newSynchronizedTest();finalSynchronizedTesttest2=newSynchronizedTest();newThread(newRunnable(){@Overridepublicvoidrun(){test.method1();}}).start();newThread(newRunnable(){@Overridepublicvoidrun(){test2.method2();}}).start();}}执行结果如下,为同步版本的静态方法本质上是类的同步(静态方法本质上是属于类的方法,而不是对象上的方法),所以test和test2虽然属于不同的对象,但是它们都属于SynchronizedTest类的实例,所以只能顺序执行method1和method2,不能同时执行Method1startMethod1executeMethod1endMethod2startMethod2executeMethod2end4,代码块同步代码段4:packagecom.paddx.test.concurrent;publicclassSynchronizedTest{publicvoidmethod1(){System.out.println("Method1start");try{synchronized(this){System.out.println("Method1execute");Thread.sleep(3000);}}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Method1end");}publicvoidmethod2(){System.out.println("Method2start");try{synchronized(this){System.out.println("Method2execute");Thread.sleep(1000);}}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Method2end");}publicstaticvoidmain(String[]args){finalSynchronizedTesttest=newSynchronizedTest();newThread(newRunnable(){@Overridepublicvoidrun(){test.method1();}}).start();newThread(newRunnable(){@Overridepublicvoidrun(){test.method2();}}).start();}}执行结果如下,虽然线程1和线程2都进入了对应的方法开始执行,但是线程2在进入synchronized块之前,需要等待线程1中synchronized块执行完成。Method1startMethod1executeMethod2startMethod1endMethod2executeMethod2end2.Synchronized的原理如果你对上面的执行结果还有疑惑,别着急,我们先了解一下Synchronized的原理再回到上面的问题一目了然。我们先反编译下面这段代码看看Synchronized是如何同步代码块的:两条指令,我们直接参考JVM规范中的描述:monitorenter:每个对象关联一个monitor。当且仅当监视器有所有者时,它才会被锁定。执行monitorenter的线程试图获得与objectref关联的监视器的所有权,如下所示:如果与objectref关联的监视器的条目计数为零,则线程进入监视器并将其条目计数设置为一。线程就是监视器的所有者。如果线程已经拥有与objectref关联的监视器,它会重新进入监视器,并增加其条目计数。如果另一个线程已经拥有与objectref关联的监视器,则该线程将阻塞,直到监视器的条目计数为零,然后再次尝试获得所有权。这句话的大致意思是:每个对象都有一个监视器锁(monitor),当监视器是当它被占用时,它将处于锁定状态。当线程执行monitorenter指令时,会尝试获取monitor的所有权。过程如下:如果monitor中的条目数为0,线程进入monitor,然后设置条目数为1,线程成为monitor的所有者。或者线程已经占用了管程,刚重新进入,则进入管程的次数加1。如果其他线程已经占用管程,则线程进入阻塞状态,直到进入管程的次数monitor为0,然后重新尝试获取monitor的所有权。monitorexit:执行monitorexit的线程必须是与objectref引用的实例关联的monitor的所有者。线程递减与objectref关联的监视器的条目计数。如果结果是条目计数的值为零,则线程退出监视器并且不再是它的所有者。其他阻塞进入监视器的线程被允许尝试这样做。这段话的大概意思是:执行monitorexit的线程一定是objectref对应的monitor的owner。指令执行时,monitor的entrynumber减1,如果entrynumber减1后为0,线程退出monitor,不再是monitor的owner。被该监视器阻塞的其他线程可以尝试取得该监视器的所有权。通过这两个描述,我们应该可以清楚的看到Synchronized的实现原理。Synchronized的底层语义是通过一个监视器对象来实现的。实际上wait/notify等方法也依赖于monitor对象。这就是为什么wait/notify等方法只能在synchronized块或方法中调用,否则会抛出java.lang.IllegalMonitorStateException。我们来看看同步方式的反编译结果:源码:packagecom.paddx.test.concurrent;publicclassSynchronizedMethod{publicsynchronizedvoidmethod(){System.out.println("HelloWorld!");}}反编译结果:从反编译结果看,方法的同步并没有通过指令monitorenter和monitorexit完成(理论上也可以通过这两个指令来实现)。但是,与普通方法相比,它的常量池中多了一个ACC_SYNCHRONIZED标识。JVM根据这个标识符实现方法同步:调用方法时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否设置。如果设置,执行线程会先获取monitor,获取成功后才执行方法体,方法执行完毕后释放monitor。在方法执行期间,没有其他线程可以再次获取同一个监视器对象。其实本质上没有区别,只是方法的同步是隐式实现的,不需要通过字节码来完成。3、运行结果的解释了解了Synchronized的原理,看上面的程序就很容易解决了。1、代码段2的结果:虽然method1和method2是不同的方法,但是这两个方法是通过同一个对象同步调用的。所以在调用之前,需要在同一个对象上竞争锁(监听),只有互斥才能获取到锁。所以method1和method2只能顺序执行。2、代码段3的结果:虽然test和test2属于不同的对象,但是test和test2属于同一个类的不同实例。由于method1和method2都是静态同步方法,调用时需要获取同一个类上的monitor(每个A类只对应一个类对象),所以只能顺序执行。3、代码段4的结果:对于代码块的同步,其实需要获取Synchronized关键字后括号内对象的监听。由于这段代码中括号的内容是this,并且method1和method2是通过同一个对象调用的,所以进入同步块之前需要竞争同一个对象上的锁,所以同步块只能顺序执行.四小结Synchronized是Java并发编程中最常用的保证线程安全的方法,使用也比较简单。但是,如果我们能够深入理解它的原理,了解监视器锁等底层知识,一方面可以帮助我们正确使用Synchronized关键字。另一方面也可以帮助我们更好地理解并发编程的机制,帮助我们选择更好的并发策略来完成不同情况下的任务。也能从容应对日常生活中遇到的各种并发问题。