java 下单 锁_Java 中的各种锁

Java 中的各种锁

锁的类型

乐观锁

适用于读多写少的情景,通过类似于版本号的形式,先读入版本号 value(即旧的值),进行计算得出新的版本号 new,待更新的时候,重新取一个旧的版本号 expected(重新从内存中取出这个数据),如果 value == expected,则证明在计算期间,其他线程未对其值进行改变,则原子化的更新到 new

Java 中使用 CAS 实现乐观锁,基本形式为比较-更新,整个操作原子化进行。CAS 存在ABA问题,需要通过 AtomicStampedReference 解决,即在每次更新原始数据时,引入一个额外的标记位 (stamp),记录这次更改

悲观锁

适用于并发写多的情景。使用 synchronized 关键字实现。AQS 框架下的锁则是先尝试CAS乐观锁获得锁,如果获取不到,再转换位悲观锁,比如 RetreenLock

常见的锁

java OOP 的结构

3abffb11bf03df12bb42541871e08fbd.png

81e34d40b38ea8a1761224b0e45d7909.png

自旋锁

偏向锁

偏向锁,即它会偏向第一个获得锁的线程。如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁

如果在运行过程中,遇到其他线程抢占锁,则持有偏向锁的线程会被挂起,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

![Lock Record](https://img-blog.csdn.net/20170420102716139?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenF6X3pxeg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

2. 拷贝对象头中的 Mark Word 到锁记录中

3. 拷贝成功后,vm 将尝试 CAS 更新对象的 Mark Word,使其指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向 object mark word。如果更新成功,则执行步骤4,否则执行步骤5

4. 更新成功后,意味着这个线程拥有了该对象的锁。将对象的 Mark Word 的锁标志位设置位 00 .

![线程成功获得轻量级锁](https://img-blog.csdn.net/20170420102754608?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenF6X3pxeg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

5. 如果更新操作失败了,vm 首先检查对象的 Mark word 是否指向当前线程的栈帧,如果是说明当前线程已经拥有了这个对象的锁,那么就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为 10,Mark word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试自旋来获取锁。自旋就是为了不让线程阻塞,而采用循环去获取锁的过程

#### 轻量级锁的升级

当轻量级锁自旋超过10次(-XX:PreBlockSpin)或者当前竞争轻量级锁的线程数目超过cpu核数的一半时,会升级到重量级锁。在jdk 1.6 后,自旋锁可以进行适应性升级,即 adapative self spinning,由 vm 自动升级到重量级锁。那么为什么要升级到重量级锁呢?归根结底在于自旋锁虽然效率高(停留在用户态,不用经过到内核态的切换),但**不断的消耗cpu**。如果轻量级锁竞争剧烈,会导致cpu使用率飙升。而锁升级后,切换到内核态,由操作系统进行mutex(互斥量)的申请,线程挂起,进入等待队列(**不消耗CPU**),等待操作系统的调度,然后再进入用户空间

### 重量级锁

![synchronized 实现](https://img-blog.csdn.net/20170418221917277?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenF6X3pxeg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

### 锁的降级

锁通常只在 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


版权声明:本文为weixin_32012539原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。