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

Synchronized的实现原理(一)

时间:2023-03-23 10:19:34 科技观察

synchronized是Java中解决并发情况下数据同步访问的一个非常重要的关键字。当我们要保证某个共享资源一次只能被一个线程访问时,我们可以在代码中使用synchronized关键字来锁定类或对象。所以,本文介绍一下synchronized关键字的实现原理。在阅读本文之前,建议先了解一下Java虚拟机是如何进行线程同步的。众所周知,在Java中,synchronized有两种使用形式,同步方法和同步代码块。代码如下:/***@authorHollis17/11/9.*/publicclassSynchronizedTest{publicsynchronizedvoiddoSth(){System.out.println("HelloWorld");}publicvoiddoSth1(){synchronized(SynchronizedTest.class){System.out.println("HelloWorld");}}}我们先用Javap反编译上面的代码,结果如下(过滤掉一些无用的信息):publicsynchronizedvoiddoSth();descriptor:()Vflags:ACC_PUBLIC,ACC_SYNCHRONIZEDCode:stack=2,locals=1,args_size=10:getstatic#2//Fieldjava/lang/System.out:Ljava/io/PrintStream;3:ldc#3//StringHelloWorld5:invokevirtual#4//Methodjava/io/PrintStream.println:(Ljava/lang/String;)V8:returnpublicvoiddoSth1();descriptor:()Vflags:ACC_PUBLICode:stack=2,locals=3,args_size=10:ldc#5//classcom/hollis/SynchronizedTest2:dup3:astore_14:monitorenter5:getstatic#2//Fieldjava/lang/System.out:Ljava/io/PrintStream;8:ldc#3//StringHelloWorld10:invokevirtual#4//Methodjava/io/PrintStream.println:(Ljava/lang/String;)V13:aload_114:monitorexit15:goto2318:astore_219:aload_120:monitorexit21:aload_222:athrow23:retu反编译rn后,我们可以看到Java编译器为我们生成的字节码在doSth和doSth1的处理上略有不同。那是。JVM以不同的方式对待同步方法和同步代码块。对于同步方法,JVM使用ACC_SYNCHRONIZED标志来实现同步。对于同步代码块。JVM使用monitorenter和monitorexit两条指令来实现同步。关于这部分,大家也可以在JVM规范中找到相关的描述。同步方法Java?虚拟机规范对方法级同步有介绍:方法级同步是隐式执行的,作为方法调用和返回的一部分。同步方法在运行时常量池的methodinfo结构中通过ACCSEDYNCHRONI标志进行区分,该标志由方法调用指令检查。当调用一个设置了ACC_SYNCHRONIZED的方法时,执行线程进入监视器,调用方法本身,然后退出监视器,无论方法调用是正常完成还是在异常时间内完成。执行线程拥有管程,其他线程不得进入管程。如果在调用synchronized方法时抛出异常,并且synchronized方法没有处理异常,则在异常异常重新同步方法之前自动退出对该方法的监听。主要说:方法级同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当一个线程要访问一个方法时,它会检查是否有ACC_SYNCHRONIZED。如果设置了,需要先获取监听锁,然后开始执行方法,执行完方法后释放监听锁。这时候,如果其他线程请求执行该方法,就会因为无法获得监听锁而被阻塞。值得注意的是,如果在方法执行过程中出现异常,并且在方法内部没有处理异常,那么在方法外部抛出异常之前,监听锁会自动释放。同步代码块同步代码块使用两条指令实现,monitorenter和monitorexit。爪哇?VirtualMachineSpecification中有关这两个指令的介绍:monitorenterEach对象与一个监视器相关联。当且仅当监视器有所有者时,它才会被锁定。执行monitorenter的线程试图获得与objectref关联的monitor的所有权,如下:如果与objectref关联的monitor的entrycount为0,则线程进入monitor并将其entrycount设置为1。然后该线程就是监视器的所有者。如果该线程已经拥有与objectref关联的监视器,它会重新进入监视器,并增加其条目计数。如果另一个线程已经拥有与objectref关联的监视器,则该线程将阻塞直到监视器的条目计数为零,然后再次尝试获得所有权。monitorexit执行monitorexit的线程必须是与objectref引用的实例关联的监视器的所有者。该线程递减与objectref关联的监视器的条目计数。如果作为结果条目计数的值为零,线程退出监视器并且不再是它的所有者。其他阻塞进入监视器的线程被允许尝试这样做。大致内容如下:执行monitorenter指令可以理解为Locking,执行monitorexit理解为释放锁。每个对象都维护一个计数器,记录它被锁定的次数。解锁对象的计数器为0。当一个线程获得锁(执行monitorenter)时,计数器递增为1。当同一个线程再次获得该对象的锁时,计数器再次递增。当同一个线程释放锁(执行monitorexit指令)时,计数器再次递减。当计数器为0时,锁将被释放,其他线程可以获得锁。总结synchronized方法通过ACC_SYNCHRONIZED关键字隐式地锁定方法。当线程要执行的方法被标记为ACC_SYNCHRONIZED时,在执行该方法之前需要先获取锁。通过monitorenter和monitorexit执行锁定同步代码块。当线程执行到monitorenter时,必须先获取锁,才能执行下面的方法。当线程执行到monitorexit时,必须释放锁。每个对象都维护一个计数器,记录它被锁定的次数。当计数器号为0时,表示任何线程都可以获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。您可以重新输入锁。至此,我们对Synchronized的原理有了一个大概的了解。但是,还有一些问题没有明确介绍。例如,Monitor到底是什么?对象锁的状态保存在哪里?别着急,后面我会介绍的。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文