如何理解Java中眼花缭乱的各种并发锁?( 三 )


6)线程竞争同步资源时,细节流程是否发生变化的情况JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级 。

如何理解Java中眼花缭乱的各种并发锁?

文章插图
如果多个线程中,只有一个线程能修改资源成功,其他资源只是重试,不锁住资源,称为无锁状态,其实就是乐观锁 。第一个线程访问加锁的资源自动获取锁,不存在多线程竞争的情况,资源偏向于第一个访问锁的线程,每次访问线程不需要重复获取锁,这种状态称为偏向锁 。偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入 。当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁 。但如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问的时候,轻量级锁就会膨胀为重量级锁,重量级锁会使除了当时拥有锁的线程以外的所有线程都阻塞 。升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态 。在 Java 中,synchronized 关键字内部实现原理就是这样一个锁升级的过程 。
7)最后,就是锁再设计和锁优化的一些情况先来看分段锁,它是一种锁的再次设计,并不是具体的一种锁 。分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作 。
如何理解Java中眼花缭乱的各种并发锁?

文章插图
在 Java 语言中 CurrentHashMap 底层使用分段锁Segment,来支持多线程并发操作 。另外,就是锁优化,包括锁消除、锁粗化 。锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求 。举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作 。如代码所示:
private static final Object LOCK = new Object();for(int i = 0;i < 100; i++) {synchronized(LOCK){// do some magic things}}经过锁粗化后,就变成下面这个样子:
synchronized(LOCK){for(int i = 0;i < 100; i++) {// do some magic things} }锁消除是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除 。举个例子让大家更好理解,来看这样一段代码:
public String test(String s1, String s2){StringBuffer stringBuffer = new StringBuffer();stringBuffer.append(s1);stringBuffer.append(s2);return stringBuffer.toString();}上面代码中的test() 方法,主要作用是将字符串 s1 和字符串 s2 串联起来 。test() 方法中的三个变量s1, s2,和StringBuffer都是局部变量,局部变量是存储在栈上的,而栈又是线程私有的,所以就算有多个线程访问 test() 方法也是线程安全的 。我们都知道 StringBuffer 是线程安全的类,因为append()方法是同步方法,如源码所示:
// append 是同步方法 public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;}但是 test() 方法本来就是线程安全的,为了提升效率,虚拟机自动帮我们消除了这些同步锁,这个过程就被称为锁消除 。
3、总结好了,前面讲了这么多,相信大家已经理解了各种眼花缭乱的锁 。最后,我用一张脑图完整地总结了各种锁的应用场景 。大家可以在面试前拿出来看看,只要是被问到锁相关的问题,相信你一定能够吊打面试官了 。

经验总结扩展阅读