1.CAS(Compare and Swap /比较再交换)
CAS是一种无锁算法,也叫自旋锁,它以不上锁的方式实现线程同步。
操作逻辑为:1.读取变量值 E
2.对该值进行操作,改变后的值为V
3.回写数据之前,再次取出E的值,判断与第一次取出时的值是否相同
4.如果相同,表示无人修改过E的值,那么当前线程就可以将修改后的值V写入E
5.如果不相同,表示E的值已经被修改,那么,当前线程的此次修改就无效了,需要重新读取E的值
但是,只用上面的逻辑,会遇到一个问题:
1. 线程1读取到变量x的旧值为0,开始对其修改...
2.线程2抢占,将变量x值改为了1
3.线程3抢占,将变量x的值又改回了0
4.线程1把0修改成了1,准备回写新值
5.线程1发现,x的值为0,与读取时的值一致,回写x的值为1
以上问题被称为ABA问题。那么如何解决ABA呢?
其实很简单,即给变量x增加一个版本号。每个线程在修改x的值时,更新版本号,这样就能判断出此变量是否被修改过。
2.下图演示了synchronized的锁升级过程
过程大概如下:
jvm中有关于偏向锁的参数,可以设置是否开启偏向锁。
java -XX:+PrintFlagsFinal //该指令可以在控制台打印出所有的系统参数
java -XX:+PrintFlagsFinal | grep -i 'BiasedLock' //查看所有的偏向锁相关参数
下图是执行结果。 其中 -UseBiasedLocking 参数表示默认开启偏向锁.
-BiasedLockingStartupDelay 表示延迟4秒开启
为什么要延迟开启偏向锁呢? 因为java 虚拟机中,存在许多synchronize的方法,而这些方法经过统计后,发现竞争比较激烈,开启偏向锁会产生多余的切换动作,消耗资源,因此在虚拟机启动时,关闭偏向锁能提高速度。
如果不开启偏向锁,则会直接由无锁状态转变为轻量级锁。
下面讨论的是默认开启偏向锁的情况。
在讲锁升级之前,还要引入一个东西:Mark Word,共64位,不同的位置标示不同的含义。如图:
在线程访问一个对象时,首先给线程上一个偏向锁。如果有其他线程来竞争此对象,升级为轻量级锁(自旋锁)。
当处于自旋锁状态的竞争激烈时(特定条件:如等待线程的个数超过N个,锁自旋的次数超过n次等),会升级成重量级锁。
3.偏向锁
HotSpot 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁)
如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
详细步骤:
1.线程1首先获得了偏向锁
2.线程2来竞争锁对象;
3.判断当前对象头是否是偏向锁;
是:判断拥有偏向锁的线程1是否还存在
存在:
4.暂停线程1,修改锁标志位,将锁升级成轻量级锁
5.线程1继续执行
6.线程2自旋,竞争锁对象
不存在:
4.使用cas替换偏向锁线程ID为线程2,锁不升级,仍为偏向锁(线程1执行完毕后,不会主动去释放偏向锁)
否:线程2自旋来获取锁对象;
4.轻量级锁(自旋锁)
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换替换为自己的线程id。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,就会开始自旋。自旋一定次数后轻量级锁会膨胀为重量级锁。
5.重量级锁
重量级锁依赖于底层操作系统的Mutex Lock,所有线程都会被阻塞住,线程之间的切换需要从用户态到内核态,切换成本非常高。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁,这种锁升级却不能降级的策略,是为了提高获得锁和释放锁的效率