synchronized当两个或多个线程试图同时访问同一个资源时,为了保证代码的正确执行,防止并发编程导致的数据不一致,应该使用类或对象锁的同步关键字。如何使用publicclassSynchronizedTest{publicvoidtest(){synchronized(this){//要执行代码,必须先获取当前实例对象的锁才能锁定当前实例对象//TODO}}publicsynchronizedvoidtest1(){//相当于synchronized(this)//TODO}publicvoidstaticTest(){synchronized(SynchronizedTest.class){//执行代码必须获取类对象的锁是类对象//TODO}}publicsynchronizedstaticvoidstaticTest1(){//相当于synchronized(SynchronizedTest.class)//TODO}}从上面可以看出,synchronized的使用有两种方式,分别是同步方法和同步代码块。(??注意:加锁的不是一些具体的代码,而是一个对象。比如代码中的第一个和第二个方法锁定了实例对象;第三个和第四个方法锁定了类对象)。可重入synchronized不仅可以加锁,还有一个更重要的特性就是可重入。什么是重入?重入是指一个线程已经拥有了一个对象的锁,再次申请仍然可以获得该对象的锁。publicclassReentry{publicsynchronizedvoidm1(){System.out.println("m1start");m2();System.out.println("m1结束");}publicsynchronizedvoidm2(){System.out.println("enterm2");}publicstaticvoidmain(String[]args){newReentry().m1();}}//outm1startenterm2m1end实现原理下面我们把javap中的代码如何使用反解析对应的汇编指令。(使用IDEA插件(jclasslib),可以直接在IDE中查看。)是javap的结果如下:------------------------------------------public无效测试();描述符:()V标志:ACC_PUBLIC代码:stack=2,locals=3,args_size=10:aload_01:dup2:astore_13:monitorenter4:aload_15:monitorexit6:goto149:astore_210:aload_111:monitorexit12:aload_213:throw14:return-------------------------------------publicsynchronizedvoidtest1();descriptor:()Vflags:ACC_PUBLIC,ACC_SYNCHRONIZEDCode:stack=0,locals=1,args_size=10:return从上面可以看出同步代码块是使用2条JVM命令monitorenter和monitorexit实现的;同步方法使用标志实现:ACC_SYNCHRONIZED。我们先看看monitorenter和monitorexit。根据JVM虚拟机规范第6章monitorenter/monitorexit的介绍,monitor可以理解为一个监视器,每个对象关联一个监视器,每个监视器关联一个加锁的计数器。当监视器被锁定时,只有一个所有者。试图执行monitorenter的线程必须满足以下条件:如果与监视器关联的锁定计数器为0,则线程持有监视器并将计数器递增1;如果尝试进入的线程已经拥有关联的监视器,则重新进入监视器,并增加计数器;如果另一个线程已经拥有监视器,试图进入的线程将阻塞,直到监视器的计数器达到零,然后再次尝试获取所有权。执行monitorexit的线程必须是monitor的原属主,每执行一次计数器就减1,直到计数器为0,退出monitor。(如果试图进入的线程已经有关联的监视器,则重新进入监视器并将计数器加1。这句话解释了synchronized的可重入性。)我们看一下flags:ACC_SYNCHRONIZED。根据JVM虚拟机规范第2章Synchronization的介绍,synchronized方法会有一个flags:ACC_SYNCHRONIZED标志。调用该方法时,会检查该标志是否可以获取监听器。当调用synchronized方法时,ACC_SYNCHRONIZED会被设置为1,执行线程会持有监视器,只要执行线程拥有监视器,其他线程就无法进入。如果在synchronized方法调用期间抛出异常并且synchronized方法没有处理异常,则在异常重新抛入方法之前,方法的监视器将自动退出synchronized。可以看出,两者本质上都需要获取monitor,如果获取不到,则不会阻塞,直到获取到monitor。然后这里有一点需要注意,就是如果synchronized方法抛出异常没有处理,方法监视器会自动退出。这将导致其他线程在发生异常时访问数据。锁优化在JDK1.6之前,synchronized的实现直接调用monitorenter和monitorexit。而这两个操作都需要去操作系统申请锁,中间会有一个从用户态-->内核态的过程。(_Java线程映射到操作系统原生线程_)所以synchronized的效率比较低。JDK1.6对锁的实现引入了大量的优化,例如自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术,以减少锁操作的开销。锁消除锁消除是JIT编译器在动态编译同步块时,使用一种叫做逃逸分析(EscapeAnalysis)的技术来判断同步块使用的锁对象是否只能被一个线程访问而没有被释放。到其他线程。也就是说,消除了不太可能竞争的共享数据上的锁。如下代码所示:publicStringtest2(){StringBuffersb=newStringBuffer();for(inti=0;i<99;i++){sb.append(i);}returnsb.toString();}我们都知道StringBuffer的append方法会被锁住,但是从上面的例子我们可以看出StringBuffer对象是在方法内部的,不会被其他线程访问到,而这个时候lock就会被淘汰。锁粗化锁粗化是JIT发现如果在一段代码中对同一个对象反复加锁和解锁,是比较耗费资源的。这种情况下,可以适当放宽加锁的范围,以降低性能消耗。如下代码所示:for(inti=0;i<99;i++){synchronized(this){do();}//锁粗化synchronized(this){for(inti=0;i<99;i++){do();}}自旋锁自旋锁的出现是因为每次线程被阻塞或者被唤醒,都需要操作系统的帮助。这种状态转换非常耗时。比较短,显然这样的状态切换不值得。自旋锁的思想是让想要持有锁的线程等待,做一个循环获取锁,看持有锁的线程会不会很快释放锁。如果一旦释放,则立即获得锁。这个过程称为纺纱。但这种旋转不是无限的。达到一定次数后,如果仍然没有获取到锁,就会被挂起。自旋锁也有优点和缺点。如果持有锁的线程快速释放锁,这种方案的效率是非常好的,因为它避免了线程切换的开销;但是如果持有锁的线程没有快速释放锁,自旋线程也同时占用了处理器时间,这也是一种浪费。如果自旋线程很多,对处理器也是一种负担。因此,自旋锁的使用场景更为重要。一般持有锁的执行时间较短,在竞争锁的线程数较少的场景下使用自旋锁比较合适。自旋锁在JDK1.4.2引入,默认关闭,但可以通过-XX:+UseSpinning开启,JDK1.6默认开启。同时默认旋转次数为10次,可以通过参数-XX:PreBlockSpin进行调整。在自适应自旋锁JDK1.6中,默认的自旋次数是10次,所以如果自旋线程多自旋几次,就可以一直等到锁被释放。古人云:半百里之外。JDK1.6也为此做了一些优化,即自适应自旋锁。自适应自旋锁是指自旋次数不是一个固定值,而是由同一把锁上一次自旋的时间和锁拥有者的状态决定的。如果线程自旋成功,则下一个线程的自旋会增加,因为虚拟机认为上次自旋成功,那么本次大概率会成功,所以增加线程数;如果永久自旋失败,则下一次自旋次数会减少甚至省略,以免浪费处理器资源。偏向锁偏向锁的意思是会偏向第一个访问锁的线程。大多数情况下,锁是不存在多线程竞争的,总是同一个线程多次获取,才会让线程获取到锁。价格更低。当线程访问同步块并获取锁时,检查MarkWord是否处于偏向状态,即是否为偏向锁1,锁标志位为01;如果处于可偏置状态,则测试线程ID是否为当前线程ID,如果是则执行同步代码块;否则,竞争锁由CAS操作,如果竞争成功,将MarkWord的线程ID替换为当前线程ID,否则,CAS竞争锁失败,证明当前存在多线程竞争情况。当到达全局安全点时,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后阻塞在安全点的线程继续执行同步代码块。只有当其他线程试图竞争偏向锁时,持有偏向锁的线程才会释放锁,该线程不会主动释放偏向锁。偏向锁的取消需要等待全局安全点(此时没有字节码在执行),会先挂起拥有偏向锁的线程,判断锁对象是否处于锁定状态,取消偏向锁后恢复无偏向锁。锁的状态(标志位为“01”)或轻量级锁(标志位为“00”)。启用偏向锁:-XX:+UseBiasedLocking-XX:BiasedLockingStartupDelay=0禁用偏向锁:-XX:-UseBiasedLocking轻量级锁当禁用偏向锁功能或多个线程竞争偏向锁时,将偏向锁升级为轻量级锁.轻量级锁并不会取代重量级锁,但是在大多数情况下不会出现严重的同步块竞争,所以轻量级锁的引入可以减少重量级锁阻塞线程带来的开销。当代码进入同步块后,如果同步对象的锁状态为无锁状态(锁标志位为“01”,是否为偏向锁为“0”),虚拟机会先创建一个名为锁记录(LockRecord)的空间用于存放锁对象当前MarkWord的副本,官方称之为DisplacedMarkWord。然后虚拟机会使用CAS操作尝试更新对象的MarkWord为指向LockRecord的指针,并将Lockrecord中的owner指针指向对象头的markword。如果成功则表示锁已被竞争,则将锁标志更改为00(表示该对象处于轻量级锁状态),并进行同步操作;如果更新操作失败,虚拟机会判断对象的MarkWord是否指向当前线程的Stackframe,如果是,说明当前线程已经拥有了这个对象的锁,那么可以直接进入同步块继续执行。否则,表示多个线程竞争锁,轻量级锁将扩展为重量级锁,锁标志的状态值变为“10”,指向重量级锁(mutex)的指针存放在标记单词。等待锁的线程也进入阻塞状态。当前线程尝试使用自旋来获取锁。经过几次自旋,如果还没有获取到锁,则挂起。轻量级解锁时,会使用原子CAS操作将DisplacedMarkWord替换回对象头。如果成功,则意味着没有竞争。如果失败,则说明当前锁处于竞争状态,该锁将扩展为重量级锁。(因为之前在获取锁的时候复制了锁对象头的markword,如果在释放锁的时候返回对象头发现替换失败,说明有其他线程在获取锁的同时尝试获取锁)holdingthelockYes,andthethreadhasmodifiedthemark.)lockexpansionsynchronized同步锁执行过程中有四种状态:无锁、偏向锁、轻量级锁、重量级锁。他们会随着比赛的形势逐渐升级。这个过程是不可逆的。(这个策略是为了提高获取和释放锁的效率)。所以synchronized锁的扩展过程其实就是一个无锁→偏向锁→轻量级锁→重量级锁的过程。锁的状态与java对象中存储的标记有关。标志是java对象数据结构的一部分。标记数据的长度在32位和64位虚拟机中分别为32bit和64bit(不启用压缩指针)。锁状态标志的后2位用于标记当前对象的状态;对象的状态决定了标记中存储的内容,如图所示。下图是很多博客中非常流行的一张锁展开图,也比较详细的还原了整个锁展开过程。当一个锁对象被创建并且没有被任何线程访问时,它就处于无锁状态。同时,它也是可偏向的,即当第一个线程访问它时,会认为有且只有第一个线程会访问它,默认会偏向本线程。锁标志为01,线程(a)获取到时,会先检查锁标志是否为01,再检查是否为偏向锁。如果不是,则修改为偏向锁(即线程(a)最先获取锁),同时将MarkWord的线程id改为自己的线程id。后面另一个线程(b)获取到它时,发现是偏向锁,然后判断当前MarkWord中的threadid是否是自己的threadid,如果是,则获取偏向锁,执行同步代码.如果不是,则使用CAS操作尝试替换MarkWord中的threadid。如果成功,线程(b)表示它已经获得了偏向锁并执行同步代码。(线程a在使用完偏向锁后不会主动解除,等待其他线程竞争后才释放锁)。如果失败,说明当前存在锁竞争,将执行偏向锁的取消。直到全局安全点(此时没有字节码在执行)才会执行取消偏向锁的操作,然后挂起持有偏向锁的锁线程,同时查看线程的状态,如果线程不活跃或者已经退出同步代码块,则设置为无锁状态(线程ID为空,是否为偏向锁为0,锁标志为01)重新偏向在恢复线程时。如果线程还处于活动状态,就会遍历线程栈帧中的锁记录,查看锁记录的使用情况。如果还需要持有偏向锁,取消偏向锁,升级为轻量级锁。在升级为轻量级锁之前,持有偏向锁的线程(a)被挂起,JVM会在原先持有偏向锁的线程(a)的栈帧中创建一个LockRecord(锁记录),然后复制对象头部的MarkWord内容,传递给原来持有偏向锁的线程(a)的LockRecord,对象的MarkWord更新为指向LockRecord的指针。此时线程(a)获取轻量级锁,MarkWordWord的锁标志位为00。线程(a)获取轻量级锁后,JVM会唤醒线程(a),线程(a)执行后会释放轻量级锁。同时,对于其他线程,也会在各自的栈帧中创建LockRecord,用于存储锁对象的MarkWord的副本。JVM通过CAS操作尝试将锁对象的MarkWord更正为当前线程的LockRecord。如果成功,表示锁竞争完成,执行同步代码块。如果失败,则线程尝试使用自旋方法等待持有轻量级锁的线程释放锁。旋转次数有限制。如果超过限制,则升级为重量级锁,阻塞所有未获取锁的线程,等待释放锁唤醒。轻量级锁的释放会使用CAS操作将之前复制的MarkWord内容替换回对象头。如果成功,则说明没有竞争,直接放行。如果失败,说明锁对象存在竞争关系。这时轻量级锁会升级为重量级锁,然后释放锁,唤醒挂起的线程,开始新一轮的锁竞争。注意此时的锁是重量级Lock。参考链接Java工程师的成神之路深入理解多线程(五)——Java虚拟机锁优化技术【致命Java并发】——深入解析synchronized的实现原理【致命Java并发】——synchronized锁扩展过程Java锁---偏向锁,轻量级锁,自旋锁,重量级锁
