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

由浅入深逐步了解Synchronized

时间:2023-03-18 19:26:39 科技观察

转载本文请联系sowhat1412公众号。使用synchronized关键字是Java并发编程中常用的线程同步手段之一。它具有三个功能:互斥:保证线程互斥访问同步生成,锁自动释放,多个线程操作同一个代码块或函数必须排队才能获取锁,可见性:保证共享变量的修改可以被及时看到,获取锁的线程在操作完成后会将数据刷新到共享内存区[1]有序性:有效解决重排序问题Synchronized的用法有三种:Modification实例方法修改静态方法修改代码block1.修饰实例方法synchronized关键字作用在方法前面,用于对方法加锁。事实上,默认情况下这个对象是锁定的。publicclassThread1implementsRunnable{//共享资源(关键资源)staticinti=0;//如果没有synchronized关键字,则输出小于20000publicsynchronizedvoidincrease(){i++;}publicvoidrun(){for(intj=0;j<10000;j++){increase();}}publicstaticvoidmain(String[]args)throwsInterruptedException{Thread1t=newThread1();Threadt1=newThread(t);Threadt2=newThread(t);t1.start();t2.start();t1.join();//主线程等待t1执行完毕t2.join();//主线程等待t2执行完毕System.out.println(i);}}2.修改后staticmethodsynchronized还是在方法上修饰,但是修饰的是Staticmethod,相当于给Class对象加锁,publicclassThread1{//共享资源(临界资源)staticinti=0;//如果没有synchronized关键字,则输出小于20000run(){for(intj=0;j<10000;j++){increase();}}});Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){for(intj=0;j<10000;j++){increase();}}});t1.start();t2.start();t1.join();//主线程等待t1完成t2.join();//主线程waitsfort2执行完System.out.println(i);}3.修改后的代码块的用法是在函数体内部对要修改的参数interval使用synchronized修改,相对于加锁函数,这个范围更小,可以指定加锁什么对象publicclassThread1implementsRunnable{//共享资源(临界资源)staticinti=0;@Overridepublicvoidrun(){for(intj=0;j<10000;j++){//得到String的类锁synchronized(String.class){i++;}}}publicstaticvoidmain(String[]args)throwsInterruptedException{Thread1t=newThread1();Threadt1=newThread(t);Threadt2=newThread(t);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}}总结:同步修改实例方法,多线程并发访问时,只一个线程可以进入并获取对象的内置锁,而其他线程则阻塞等待,但在此期间该线程仍然可以访问其他方法。synchronized修饰的静态方法,当多个线程并发访问时,只有一个线程可以进入,获得类锁,其他线程阻塞等待,但该线程在此期间仍然可以访问其他方法。同步修改代码块,当多个线程并发访问时,只有一个线程可以进入,根据括号中的对象或类,获取对应的对象内置锁或类锁。每个班级都有一个班级锁,每个班级都有一个班级锁。一个对象也有一个内置的锁,它们之间互不干扰。也就是说,一个线程既可以获取类锁,也可以获取该类实例化对象的内置锁。当一个线程访问非同步修饰的方法时,它不需要获取锁。因此不会发生阻塞。Monitor监视器[2](英文:Monitors,又称监视器)是操作系统中一个非常重要的概念。监视器实际上是指管理共享变量和管理共享变量的操作过程。这有点像中介。monitor管理着一堆对象,多个线程同时访问这些东西只能有一个线程。监视器可以看作是一个软件模块。它封装了共享变量和对这些共享变量的操作,形成一个具有一定接口的功能模块。进程可以调用监视器来实现进程级的并发控制。进程只能使用互斥的监视器,即一个进程使用监视器时,另一个进程必须等待。当一个进程结束使用监视器时,它必须释放监视器并唤醒等待监视器的进程之一。监视器解决互斥问题比较简单。共享变量和对共享变量的操作被封装在一个类中。当线程A和线程B需要获取共享变量count时,需要调用get和set方法,get和set方法是互斥保证的,一次只能有一个线程访问。日常生活中的一个例子是管程。例如,链家店长指定每个中介管理一部分二手房,多个客户通过中介进行房屋买卖。中介是监视器。多个二手房由一个中介管理,即一个管理者管理多个系统资源。多个客户端相当于多个线程。Synchronzied对象头分析底层原理我们知道,在JavaJVM内存区[3]中的堆区会创建一个对象,创建的对象由三部分组成。这三部分的作用如下:填充数据:由于虚拟机要求对象的起始地址必须是8字节的整数倍。不需要填充数据,只是为了字节对齐。实例变量:存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐。对象头:主要包括两部分KlassPoint和MarkWordKlassPoint(类型指针):是一个对象指向其类元数据的指针,虚拟机通过这个指针来判断该对象是哪个类实例。MarkWord(标记字段):这部分用来存放对象本身的运行时数据,比如哈希码、GC分代年龄、锁状态标志、锁指针等。这部分数据在32bit中的大小而64bit的虚拟机分别是32bit和64bit,考虑到虚拟机的空间效率,MarkWord被设计成一个非固定的数据结构,在很小的空间里存储尽可能多的信息,它会复用自己的根据对象的状态存储空间(类似于ConcurrentHashMap中的flags),具体如下:修改对象的锁,同步锁对象为对象头MarkWord。其中,轻量级锁和偏向锁是Java6优化同步锁后新增的。这里主要分析重量级锁,通常是同步对象锁。锁标志为10,指针指向监控对象。(也称为监视器或监视器锁)起始地址。每个对象都有一个与之关联的监视器[4]。在拆解查看分析对象的监视器之前,我们先拆解看看同步方法和同步方法块在汇编语言层面是什么样的指令。publicclassSynchronizedTest{publicsynchronizedvoiddoSth(){System.out.println("HelloWorld");}publicvoiddoSth1(){synchronized(SynchronizedTest.class){System.out.println("HelloWorld");}}}javacSynchronizedTest.java然后javap-cSynchronizedTest反编译后汇编指令如下:publicsynchronizedvoiddoSth();descriptor:()Vflags:ACC_PUBLIC,ACC_SYNCHRONIZED//这是关键方法lockCode:stack=2,locals=1,args_size=10:getstatic#23:ldc#35:invokevirtual#48:returnpublicvoiddoSth1();descriptor:()Vflags:ACC_PUBLICode:stack=2,locals=3,args_size=10:ldc#52:dup3:astore_14:monitorenter//进入同步方式5:getstatic#28:ldc#310:invokevirtual#413:aload_114:monitorexit//正常时退出同步方法15:goto2318:astore_219:aload_120:monitorexit//异常时退出同步方法21:aload_222:athrow23:return我们可以看到java编译器为我们生成字节码。doSth和doSth1的处理略有不同。那是。JVM以不同的方式对待同步方法和同步代码块。对于同步方法,JVM使用ACC_SYNCHRONIZED标志来实现同步。对于同步代码块。JVM使用monitorenter和monitorexit两条指令来实现同步。ACC_SYNCHRONIZED方法级同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当一个线程要访问一个方法时,它会检查是否有ACC_SYNCHRONIZED。如果设置了,需要先获取监听锁,然后开始执行方法,执行完方法后释放监听锁。这时候,如果其他线程请求执行该方法,就会因为无法获得监听锁而被阻塞。值得注意的是,如果在方法执行过程中出现异常,并且在方法内部没有处理异常,那么在方法外部抛出异常之前,监听锁会自动释放。monitorenter和monitorexit可以将执行monitorenter命令解释为加锁,执行monitorexit解释为释放锁。每个对象都维护一个计数器,记录它被锁定的次数。未锁定对象的计数器为0。当一个线程获得锁(执行monitorenter)时,计数器递增为1。当同一个线程再次获得该对象的锁时,计数器再次递增。当同一个线程释放锁(执行monitorexit指令)时,计数器再次递减。当计数器为0时,锁将被释放,其他线程可以获得锁。结论:同步方式和同步代码块底层都是通过monitor同步的。两者的区别:同步方法是通过在方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现的,同步代码块是通过monitorenter和monitorexit来实现的。监视器分析每个对象都关联一个监视器,监视器可以由线程拥有或释放。在Java虚拟机(HotSpot)中,监视器是由ObjectMonitor实现的。其主要数据结构如下(位于HotSpot虚拟机源代码中的ObjectMonitor.hpp文件,用C++实现)。ObjectMonitor(){_count=0;//记录数_recursions=0;//锁数reentries_owner=NULL;//指向持有ObjectMonitor对象的线程_WaitSet=NULL;//调用wait后,线程会被添加to_WaitSet_EntryList=NULL;//等待获取锁的线程会加入到列表中}监听运行图如下:对于一个synchronized修饰的方法(代码块):当多个线程同时访问该方法时,则这些线程将首先放入_EntryList队列。此时线程处于阻塞状态。当一个线程获得了对象的监听器后,就可以进入运行状态,执行方法块。此时ObjectMonitor对象的_owner指向当前Thread,_count加1表示当前对象锁被线程获取。当处于运行状态的线程调用wait()方法时,当前线程释放monitor对象,进入等待状态。ObjectMonitor对象的_owner变为null,_count减1。同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒线程后,线程进入_EntryList队列,竞争获取锁然后进入_owner区域。如果当前线程执行完毕,monitor对象也被释放,ObjectMonitor对象的_owner变为null,_count减1。因为monitor锁(monitor)是依赖底层操作系统的MutexLock实现的,操作系统在线程切换时需要从用户态切换到核心态(详见CXUAN写的OS)。这种状态之间的转换需要比较长的时间,时间成本比较高,这也是早期synchronized效率低下的原因。幸运的是,在Java6之后,Java官方从JVM层面对synchronized的优化做了比较大的改进。Java6之后,为了减少获取和释放锁带来的性能消耗,引入了锁升级的概念。锁升级同步锁有四种状态,无锁、偏向锁、轻量级锁、重量级锁。这些状态会随着竞争状态逐渐升级。锁可以升级不能降级,但是偏向锁状态可以重置为无锁状态。在科学的讲这些锁之前,我们先来看一个简单通俗的例子来加深一下印象。通俗的讲,各种锁——偏向锁、轻量级锁和重量级锁之间的关系,我们先打个比方[5]:假设厕所只有一个位置,每个用户都有一把钥匙可以开门锁。必须打开门锁才能使用厕所。其中小明和小红理解为两个线程。上厕所理解为执行同步码,门锁理解为同步码的锁。小明今天吃了不好的东西,需要反复上厕所。于是门锁记录了小明的脸(假设锁是智能锁),下次小明再来时,门锁会自动识别小明在这里,然后自动解锁,这样就省了小明从拿钥匙开门,此时的门锁是偏向锁,对于小明也可以理解为偏向锁。接下来,小红又去了厕所,试图将厕所的门锁设置成偏向她的锁,却发现门锁无法偏向她,因为门锁已经偏向了小明的锁。于是小红很生气,要求门锁收回对小明的偏爱。当然,小明也不同意门锁对小红的偏爱。所以在小明上完厕所后,门锁取消了对任何人的偏向(只要有竞争,偏向锁就会被取消)。这个过程就是撤销偏向锁。此时门锁升级为轻型锁。小明出来后,轻量级锁就正式生效了。下次小明和小红同时来厕所,谁跑得快就先到门口,开门拿门锁进厕所,打开门锁后拿进厕所,锁上门,所以把门锁在门外的位置放上某人的标志(这个标志可以理解为指向门锁的指针,也可以理解为锁的Java对象头的MarkWord值).这时,小红见了人就很着急,催促出来的人马上进去,于是他们就不停地敲门问小明什么时候出来。这个过程称为纺纱。反复敲了几次,小明实在受不了了,冲着小红喊,说你别敲了,等我上完厕所再告诉你,小红就到一旁等着(线性阻塞)。此时门锁升级为重量级锁。升级为重量级锁的后果是小红不再反复敲门,小明上厕所后必须告诉小红,否则小红将永远等下去。结论:偏向锁在只有一个人上厕所的时候效率很高,省去了开门的过程。轻量锁在上厕所的人多但大家用的很快的时候效率更高,因为会出现这种现象,小红敲门的时候正好赶上小明出来,这样就省得小明出来告诉小红小红只能晚点进去,但这可能会导致小红敲门失败(即小明敲门时还没用完)。与轻量级锁相比,重量级锁让小明给小红打电话多了一步,但是省去了小红反复敲门的过程,但是可以保证小红上厕所的时候马桶一定是空的。经过HotSpot作者的大量研究,发现大部分时间是不存在锁竞争的。通常,一个线程会多次获取同一个锁。因此,如果每次都必须竞争锁,会增加很多不必要的成本。为了减少获取锁的成本,引入了偏向锁。核心思想:如果一个线程获得了锁,则锁进入偏向模式。这时MarkWord的结构也变成了偏向锁结构。当线程再次请求锁时,不需要做任何同步操作,即获取锁的过程,省去了很多申请锁相关的操作,从而提高了程序的性能。因此,对于没有锁竞争的场合,偏向锁有很好的优化效果。毕竟同一个线程很有可能连续多次申请同一个锁。但是对于锁竞争比较激烈的情况,偏向锁是无效的,因为这种情况下很有可能每次申请锁的线程都不一样,所以这种情况下不应该使用偏向锁,否则收益会很大得不偿失,需要注意的是,偏向锁失效后,不会马上扩容为重量级锁,而是先升级为轻量级锁。具体过程:当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向锁的threadID,因为偏向锁不会主动释放锁,所以当线程1获取以后再次上锁,需要比较当前线程的threadID和Java对象头中的threadID是否一致。如果一致(线程1仍然获取锁对象),就不需要使用CAS加锁和解锁;如果不一致(其他线程,比如线程2需要竞争锁对象,偏向锁不会主动释放,所以仍然是线程1存储的线程ID),那么需要检查是否Java对象头中记录的线程1是alive的,如果不是,则将锁对象重置为无锁状态,其他线程(Thread2)可以竞争将其设置为偏向锁;如果存活,则立即查找线程(线程1)的栈帧信息,如果还需要继续持有锁对象,则挂起当前线程1,撤销偏向锁,升级是轻量级的锁。如果线程1不再使用锁对象,则将锁对象的状态设置为无锁状态,重新偏向新线程。轻量级锁轻量级锁考虑的是竞争锁对象的线程不多,线程不会长时间持有锁。因为阻塞一个线程需要高耗时的从用户态切换到内核态,如果阻塞后不久就释放锁,代价是得不偿失的,所以这个时候干脆不要阻塞线程,让它自旋并等待锁被释放。《原理与升级》:线程1在获取轻量级锁时,会先将锁对象的对象头MarkWord复制到线程1的栈帧中创建的存放锁记录的空间(称为DisplacedMarkWord),然后使用CAS替换对象头中的内容与线程1存储的锁记录(DisplacedMarkWord)的地址;如果线程1复制对象头(在线程1CAS之前),线程2也准备获取锁并将对象头复制到线程2的锁记录空间,但是当线程2CAS时,发现线程1改变了objectheader,"线程2的CAS失败,然后线程2尝试使用自旋锁等待线程1释放锁。"自旋锁简单的说就是让线程2在循环中不断的CAS去尝试获取锁对象。但是如果自旋时间“太长”,就不行了,因为自旋很耗CPU,所以“自旋次数是有限的”,比如10次或者100次,如果自旋次数达到线程1,就没有了释放锁,或者线程1还在执行,而线程2还在自旋等待,那么此时轻量级锁就会扩展为重量级锁。重量级锁阻塞除了拥有锁的线程之外的所有线程,防止CPU空闲。LockElimination锁消除是另一种针对虚拟机的锁优化。这个优化更彻底。Java虚拟机在JIT编译期间扫描运行上下文,以移除不太可能发生共享资源竞争的锁。除了不必要的锁之外,它还可以节省请求锁的无意义时间。我们知道StringBuffer是线程安全的,并且包含锁的存在,但是如果我们在函数内部使用StringBuffer,代码会在JIT之后自动释放锁哦。对比如下:加锁状态的优缺点适用场景偏加锁和解锁,无额外消耗,与非同步方式的时间差为纳秒级。如果竞争线程较多,会带来锁取消的额外消耗。基本上不存在其他线程竞争的同步场景。竞争量级锁的线程不会阻塞而是自旋,可以提高程序响应速度。如果无法一直获取它们,它们将自旋并消耗CPU。少量线程竞争,短时间持有锁,追求响应速度。重量级锁线程不会竞争导致CPU自旋和消耗CPU资源的线程被阻塞,响应时间长。很多线程争锁,锁持有时间长。在追求吞吐量的时候,PS:ReentrantLock的底层实现依赖于特殊的CPU指令,比如发送加锁指令和解锁指令。无需在用户态和内核态之间切换,效率高。synchronized底层是一个监视器锁(monitor),依赖于底层操作系统的MutexLock,需要在用户态和内核态之间切换,所以效率会比较低。参考[1]监控:https://www.zhihu.com/question/30641734[2]监控:http://www.hollishuang.com/archives/2030[3]例子:https://blog.csdn。net/xyh930929/article/details/84571805[4]unbelievableme:https://www.cnblogs.com/kundeg/p/8422557.html[5]头条大佬谈syn:https://blog.csdn.net/javazejian/article/details/72828483[6]AliHollissyn:http://www.hollishuang.com/archives/2030