synchronized锁机制
synchronized的执行过程:
-
检测Mark Word里面是否已存在某一个线程ID,若Mark Word里面不存在某一个线程ID,则CAS将当前线程的ID替换Mark Word,如果成功则表示当前线程获得了偏向锁,可执行同步代码块;如果失败,则说明发生竞争,跳转到步骤3。
-
若Mark Word里面已存在某一个线程ID,且为当前线程的ID,表示当前线程已获取了偏向锁,无需进行CAS操作来加锁解锁,直接执行同步代码块即可。
-
若Mark Word里面已存在某一个线程ID,但不是当前线程ID,则说明发生竞争,跳转到步骤3。
-
发生竞争,此时先撤销偏向锁,然后升级为轻量级锁。
-
升级为轻量级锁的过程是这样的:线程在执行同步代码块之前,JVM先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。官方称之为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则当前线程获得轻量级锁。
-
如果失败,表示其他线程竞争锁,则当前线程尝试使用自旋来获取锁。
-
如果自旋失败,则轻量级锁将膨胀为重量级锁。
-
上述几种锁都是JVM自己内部实现,当我们执行synchronized同步块的时候,JVM会根据都哪些锁可用和当前线程的竞争状态,决定如何执行同步操作。
-
当所有的锁都可用的情况下,线程进入临界区时会先获取偏向锁,如果已经存在偏向锁,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步代码块后再唤醒它们。
-
偏向锁是在无锁争用的情况下使用的,一旦有第二个线程争用,偏向锁就会升级为轻量级锁,轻量级锁时如果未争取到锁的线程自旋到达阈值后,仍没有获取到锁,则轻量级锁就会升级为重量级锁。
-
如果线程竞争激烈,那么应该禁用偏向锁。
-
Java SE 1.6中,锁一共有四种状态,级别依次为:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。级别随竞争情况逐渐升级,但不可降级。
-
宏观上讲,锁分为悲观锁与乐观锁。乐观锁,认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是每次更新的时候会去判断在此期间是否有别人更新了这个数据(利用版本号,时间戳等)。乐观锁失败了需要重复进行“读-比较-写”的操作。悲观锁,认为写多读少,遇到并发写的可能性高,每次拿数据的时候都认为别人大概率会修改,所以每次拿数据的时候都会上锁。这样别人想再拿这个数据并改写时,就会被block,直至拿到锁。
-
Java乐观锁基本都是通过CAS实现的,自旋锁,轻量级锁,偏向锁属于乐观锁;而重量级锁属于悲观锁。
-
Java线程阻塞的代价:
Java线程是映射到操作系统原生线程上的,如果要阻塞或唤醒一个线程,需要操作系统介入,需要在用户态与核心态之间切换,因为用户与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递许多变量和参数给内核,内核也需要保护好用户态在切换时的一些寄存器值与变量等,以便内核态调用结束后切换回用户态继续工作,因此切换会消耗大量系统资源。因此对于那些需要同步的简单代码块,获取锁挂起操作所消耗的时间比代码块执行的时间还要长,这种同步策略是非常糟糕的。因此synchronized是重量级同步操作,是重量级锁。为了缓解上述问题,进而引入了偏向锁、轻量级锁,自旋锁这些乐观锁。
-
Mark Word是Java对象数据结构中的一部分,其最后两比特是锁状态标志位,用来标记当前对象所处的状态。
具体结构如下
-
偏向锁
偏向锁的获取方式是将对象头的MarkWord部分中,标记上该线程ID。在偏向锁功能可用时,若对象头存在线程ID,则为“已偏向状态”,否则为“可偏向状态”。
如果发生竞争,原持有偏向锁的线程将释放偏向锁。偏向锁的撤销需要在全局安全点执行(在这个时间点上没有正在执行的字节码,stop the world),之后锁将会升级为轻量级锁。
注意,偏向锁只有遇到竞争的时候才会撤销并升级,线程是不会主动去释放(撤销)偏向锁,并升级为轻量级锁的。
偏向锁适合很少存在竞争的情况,毕竟在有锁竞争时,偏向锁会做很多额外的操作,尤其是偏向锁的撤销会stop the world,导致性能下降。在多竞争情况下,应手动设置,禁用偏向锁。
- 轻量级锁
线程在执行同步代码块之前,JVM先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称之为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。
如果这个更新动作成功了,那么该线程就拥有了该对象的锁,可继续执行同步代码块;如果更新动作失败,说明多个线程竞争该对象的锁,此时当前线程将进行自旋,若超过自旋阈值后,当前线程仍未成功获取到该对象的锁(即已获得轻量级锁的线程仍未解锁释放资源),则轻量级锁将会膨胀为重量级锁。
轻量级锁的解锁:轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头。
-
自旋锁
如果持有锁的线程在很短时间内释放锁资源,那么等待竞争的线程就不需要做内核态和用户态之间的切换并阻塞挂起。它们只需要等一等(自旋),等待有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程与内核的切换消耗。
但线程自旋是需要消耗CPU资源的,让CPU做无用功,所以自旋往往有时间或者次数的阈值,一旦超过阈值,则停止自旋,线程阻塞挂起,轻量级锁升级为重量级锁。
-
偏向锁与轻量级锁的区别:
1.偏向锁只会在第一次请求的时候使用CAS,并在锁对象的标记字段中记录当前线程ID。在此后的运行过程中,持有偏向锁的线程无需加锁操作。针对的是锁仅会被同一线程持有的状况。
2.轻量级锁会多次使用CAS,将对象头中的Mark Word替换为指向锁记录的指针。针对的是多个线程在不同时间段申请同一把锁的情况。
-
锁优化:
以上介绍的锁不是我们代码中能够控制的,但是借鉴上面的思想,我们可以优化我们自己线程的加锁操作。
减少锁的时间:不需要同步执行的代码,尽量不放在同步代码块里,让锁尽快释放。
锁粗化:假设有一个循环,循环内的操作需要加锁,可以考虑把锁放在循环外面,避免每次进出循环都加解锁,这样效率很差。
非原创,仅为个人整理,感谢以下大佬的文章!
(参考Java并发编程的艺术)