Java 中的各种锁
锁的类型
乐观锁
适用于读多写少的情景,通过类似于版本号的形式,先读入版本号 value(即旧的值),进行计算得出新的版本号 new,待更新的时候,重新取一个旧的版本号 expected(重新从内存中取出这个数据),如果 value == expected,则证明在计算期间,其他线程未对其值进行改变,则原子化的更新到 new
Java 中使用 CAS 实现乐观锁,基本形式为比较-更新,整个操作原子化进行。CAS 存在ABA问题,需要通过 AtomicStampedReference 解决,即在每次更新原始数据时,引入一个额外的标记位 (stamp),记录这次更改
悲观锁
适用于并发写多的情景。使用 synchronized 关键字实现。AQS 框架下的锁则是先尝试CAS乐观锁获得锁,如果获取不到,再转换位悲观锁,比如 RetreenLock
常见的锁
java OOP 的结构
自旋锁
偏向锁
偏向锁,即它会偏向第一个获得锁的线程。如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁
如果在运行过程中,遇到其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的锁,将锁回复到标准的轻量级锁
偏向锁的获取过程
访问 Mark Word 中偏向锁的标识是否设置位1, 锁标志位是否位 01 ,确认是否位可偏向状态
如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3
如果线程ID并未指向当前线程,则通过 CAS 操作竞争锁。如果竞争成功,则将 Mark Word 中线程ID设置为当前线程 ID,然后指向步骤 5; 如果竞争失败,执行步骤 4
如果 CAS 获取偏向锁失败,则表示有竞争。当到达全家安全点(safepoint)时获得偏向锁的线程会被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续执行往下的同步代码。 (撤销偏向锁会导致 STW )
执行同步代码
偏向锁的释放
偏向锁只有遇到其他线程尝试竞争时,持有线程锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点美欧字节码执行),它会首先判断锁对象是否处于被锁定状态,撤销偏向锁后回复到未锁定状态
偏向锁的使用场景
始终只有一个线程,在没有执行完同步块释放锁之前,没有其他线程取执行同步块。在有大量的锁竞争时,偏向锁的撤销会导致进入安全点时 STW,引起性能下降。默认情况下,偏向锁是有延时的,默认是4 s。这就有可能导致 vm 自身启动时,为了 sync,产生大量的偏向锁的撤销和升级,从而使效率降低
可以通过 ```shell
-XX:BiasedLockingStartupDelay=0
关闭这个延时
### 轻量级锁
轻量级锁是由偏向锁升级带来的。偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争时
#### 轻量级锁的加锁过程
1. 代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为 01, 是否为偏向锁位 0),vm 首先将在当前线程的栈帧中建立一个名为**锁记录**(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,官方称为 Displaced Mark Word

2. 拷贝对象头中的 Mark Word 到锁记录中
3. 拷贝成功后,vm 将尝试 CAS 更新对象的 Mark Word,使其指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向 object mark word。如果更新成功,则执行步骤4,否则执行步骤5
4. 更新成功后,意味着这个线程拥有了该对象的锁。将对象的 Mark Word 的锁标志位设置位 00 .

5. 如果更新操作失败了,vm 首先检查对象的 Mark word 是否指向当前线程的栈帧,如果是说明当前线程已经拥有了这个对象的锁,那么就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为 10,Mark word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试自旋来获取锁。自旋就是为了不让线程阻塞,而采用循环去获取锁的过程
#### 轻量级锁的升级
当轻量级锁自旋超过10次(-XX:PreBlockSpin)或者当前竞争轻量级锁的线程数目超过cpu核数的一半时,会升级到重量级锁。在jdk 1.6 后,自旋锁可以进行适应性升级,即 adapative self spinning,由 vm 自动升级到重量级锁。那么为什么要升级到重量级锁呢?归根结底在于自旋锁虽然效率高(停留在用户态,不用经过到内核态的切换),但**不断的消耗cpu**。如果轻量级锁竞争剧烈,会导致cpu使用率飙升。而锁升级后,切换到内核态,由操作系统进行mutex(互斥量)的申请,线程挂起,进入等待队列(**不消耗CPU**),等待操作系统的调度,然后再进入用户空间
### 重量级锁

### 锁的降级
锁通常只在 GC 时才会发生降级,这时通常是被 GC thread 访问
### 锁的消除 (lock eliminate)
```java
public void add(String str1, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
sb 这个引用只有在 add 方法内部使用,是栈私有的,因此sb 是不可能被共享使用的资源,因此 vm 会自动消除 StringBuffer 内部的锁
锁的粗化 (lock coarsening)
public void test(String str) {
StringBuffer sb = new StringBuffer();
for (int i=0; i<100; i++) {
sb.append(str);
}
}
在上面的例子中,vm会对锁优化,将加锁的范围移到for循环外面
synchronized 底层实现
public class T {
static volatile int i;
public static void n() { i++;}
public static void m() {}
public static void main(String[] args) {
for (int i=0; i < 1000_1000; i++) {
n();
m();
}
}
}
使用如下命令查看具体生成的汇编指令(关注 lock cmpxchg 指令)
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly T