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

处理高并发怎么能不了解Synchronized的底层原理呢?

时间:2023-03-13 22:38:16 科技观察

Synchronized是Java中最常用的解决并发问题的方法之一,也是最简单的一种。本文作者将全面剖析Synchronized的底层原理。Synchronized的基本使用Synchronized主要有3个作用:保证线程互斥访问同步代码,保证共享变量的修改及时可见有效解决重排序问题从句法上看,Synchronized具有一共三种用法:普通方法的修饰静态方法的修饰代码块接下来我会用几个示例程序来说明这三种使用方法(为了对比,三种代码除了Synchronized的使用方法不同外其他基本相同).没有同步的代码段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同时执行:Method1startMethod1executeMethod2startMethod2executeMethod2endMethod1end普通方法同步代码段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需要等待Method1的执行线程1在开始执行方法2之前完成法律Method1startMethod1executeMethod1endMethod2startMethod2executeMethod2end静态方法(类)同步代码段3:packagecom.paddx.test.concurrent;publicclassSynchronizedTest{publicstaticsynchronizedvoidmethod1(){System.out.println("Method1start");try{System.out.println("Method1start");try{System.out.println("Method1start");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只能顺序执行,不能并发执行:Method1startMethod1executeMethod1endMethod2startMethod2executeMethod2end代码块同步代码段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正在进入在进入同步块之前,需要等待线程1中的同步块执行完成。Method1startMethod1executeMethod2startMethod1endMethod2executeMethod2endSynchronized原理如果你对上面的执行结果还有疑惑,不要着急,我们先了解一下Synchronized的原理。回过头来看上面的问题,就一目了然了。我们先反编译下面这段代码看看Synchronized是如何同步代码块的:两条指令,我们直接参考JVM规范中的描述:monitorenter:每个对象关联一个monitor。当且仅当监视器有所有者时,它才会被锁定。执行monitorenter的线程试图获得与objectref关联的监视器的所有权,如下所示:如果与objectref关联的监视器的条目计数为零,则线程进入监视器并将其条目计数设置为一。线程就是监视器的所有者。如果线程已经拥有与objectref关联的监视器,它会重新进入监视器,并增加其条目计数。如果另一个线程已经拥有与objectref关联的监视器,则该线程将阻塞,直到监视器的条目计数为零,然后再次尝试获得所有权。这句话的大致意思是:每个对象都有一个监视器锁(Monitor),当Mon当itor被占用时,它会处于锁定状态。当线程执行Monitorenter指令时,它会尝试获取Monitor的所有权。过程如下:如果Monitor中的条目数为0,线程进入Monitor,然后设置条目数为1,线程成为Monitor的所有者。如果线程已经占用了Monitor,刚重新进入,则进入Monitor的条目数加1。如果其他线程已经占用了Monitor,则线程进入阻塞状态,直到Monitor条目数为0,然后再次尝试获取Monitor的所有权。monitorexit:执行monitorexit的线程必须是与objectref引用的实例关联的monitor的所有者。线程递减与objectref关联的监视器的条目计数。如果结果是条目计数的值为零,则线程退出监视器并且不再是它的所有者。其他阻塞进入监视器的线程被允许尝试这样做。这段话的大概意思是:执行Monitorexit的线程必须是Objectref对应的Monitor的owner。指令执行时,monitor中的entry数减1,如果entry数减1后为0,线程退出monitor,不再是monitor的所有者。被此Monitor阻塞的其他线程可以尝试取得此Monitor的所有权。通过这两个描述,我们应该可以清楚的看到Synchronized的实现原理。Synchronized的底层语义是通过Monitor对象完成的。实际上,Wait/Notify等方法也依赖于Monitor对象。这就是为什么只能在synchronized块或方法中调用Wait/Notify等方法,否则会抛出java.lang.IllegalMonitorStateException。我们再来看看同步方式的反编译结果。源码如下:packagecom.paddx.test.concurrent;publicclassSynchronizedMethod{publicsynchronizedvoidmethod(){System.out.println("HelloWorld!");}}反编译结果:从反编译结果来看,方法的同步没有通过指令Monitorenter和Monitorexit完成(理论上也可以通过这两条指令实现)。但是,与普通方法相比,常量池中多了一个ACC_SYNCHRONIZED标识。JVM根据这个标识符实现方法同步:调用方法时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否设置。如果设置,执行线程会先获取Monitor,获取成功后才执行方法体,方法执行完毕后释放Monitor。在方法执行期间,没有其他线程可以再次获取同一个Monitor对象。其实本质上没有区别,只是方法的同步是隐式实现的,不需要通过字节码来完成。运行结果解读了解了Synchronized的原理,看上面的程序就很容易解决了。①代码段2结果虽然Method1和Method2是不同的方法,但是这两个方法是通过同一个对象同步调用的。所以在调用之前,需要在同一个对象上竞争锁(Monitor),只能通过互斥来获取锁。因此,Method1和Method2只能顺序执行。②代码段3结果虽然Test和Test2属于不同的对象,但是Test和Test2属于同一个类的不同实例。由于Method1和Method2都是静态同步方法,需要获取同一个类上的Monitor(每个类只对应一个Class对象),所以只能顺序执行。③CodeSegment4因此,对于代码块的同步,其实需要获取Synchronized关键字后括号内对象的Monitor。由于这段代码中括号的内容是This,并且Method1和Method2是通过同一个对象调用的,所以在进入同步块之前需要在同一个对象上竞争锁,所以同步块只能顺序执行.总结Synchronized是Java并发编程中最常用的保证线程安全的方式,使用也比较简单。但是,如果我们能够深入了解它的原理,了解监视器锁等底层知识,一方面可以帮助我们正确使用Synchronized关键字。另一方面也可以帮助我们更好地理解并发编程的机制,帮助我们选择更好的并发策略来完成不同情况下的任务。也能从容应对日常生活中遇到的各种并发问题。