详解 synchronize 锁的升级
Posted xiexiandong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解 synchronize 锁的升级相关的知识,希望对你有一定的参考价值。
synchronized 锁定的是一个对象,执行某段代码的时候必须锁定一个对象,不锁定就无法执行
一、概念介绍
1.1 用户态与内核态
- 内核态(kener):内核/操作系统可以做的一些操作。
- 用户态(APP):用户的程序可以做的一些操作。
- 用户态的程序要访问一些比较危险的操作的时候,比如格式化硬盘或直接访问内存网卡等,必须经过操作系统即内核的允许,这样可以保证安全性。
- 从指令来讲,用户态只能执行某些指令,而内核态可以执行所有指令。
- 对于 JVM 虚拟机来说就是一个普通程序,即属于用户态。
- 早期的 synchronized 叫重量级锁,因为早期使用 synchronized 加锁的时候要结果内核态的允许,即要经过操作系统线程的调度才能拿到锁,所以称为重量级锁。
- 后期经过了优化在某些特定情况下不需要结果操作系统,在用户态就可以解决,即使轻量级锁,比如 CAS 只是一个对比和交换,不需要经过操作系统是轻量级锁(锁的升级)。
1.2 CAS
CAS :compare and swap/compare and exchange
- 举个例子:
- A 线程获取变量 a 的值此时 a = 1,然后 A 线程对变量 a 进行 a++ 操作,操作完成要写回内存。
- 此时会再次获取当前时间下变量 a 的值,如果此时 a 依旧为0,就认为没有线程操作过 a,就正常将 a=1 写入。
- 如果发现 a 的值已近变了比如 a = 3了,说明有线程对 a 做了操作,那就不写入。
- 此时重新获取 a 的值,在进行 ++ 操作,操作完在判断当前 a 的值和 ++ 前的值是否一致。这样一致循环下去。
- 上面说的这种情况不用上锁, CAS 也称为自旋锁/无锁。无锁不是没有锁,是没有内核状态的锁。
对图中的 ABA 问题做一下解释:
还是上面的例子,A 线程执行完 a++ 操作后,要将新的 a 值写入内存,此时会再次获取当前时间下变量 a 的值,如果此时 a 依旧为0,就认为没有线程操作过 a,就正常将 a=1 写入。但是可能存在这种情况,就是 B 线程将变量 a 改为3,然后 C 线程又将变量 a 改为了0,实际上此时变量 a 已经发生了变化。这就是 ABA 问题。
解决方法:可以给变量 a 增加一个版本号
再举个例子:
public static void main(String[] args) throws InterruptedException {
AtomicInteger integer = new AtomicInteger(0);
Thread[] threads = new Thread[10];
// 等待线程结束
CountDownLatch downLatch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for (int j = 0; j < 5; j++) {
// 如果是 integer++ 的话就要加锁
integer.incrementAndGet();
}
downLatch.countDown();
}
});
threads[i] = thread;
}
Arrays.stream(threads).forEach(f -> f.start());
downLatch.await();
System.out.println(Thread.currentThread().getName() + " " + integer);
}
上面的代码中如果采用 integer++ 这种方式就要进行加锁,采用 integer.incrementAndGet() 就不需要加锁,因为 incrementAndGet 方法底层就是采用的 CAS 实现的,是汇编的一条指令lock cmpxchg 指令。cmpxchg 指令不是原子的,所以需要 lock 指令给 CPU 加锁,只让一个线程操作。
1.3 对象在内存中的分布
二、锁的升级
偏向锁、自旋锁都是在用户空间完成
重量级锁都需要向内核空间申请
偏向锁:
- 向 markword 上记录自己的线程指针,实际上没有上锁,只是标记,此时只有一个线程执行,没有竞争的概念。
- 为何会有偏向锁:因为经过统计大多数情况下 synchronized 方法只有一个线程在执行(如:stringbuffer的一些sync方法,vector的一些sync方法),此时没必要申请锁,节约资源
- JVM 中偏向锁是默认打开的,但是有延迟 4S,可以设置参数修改 1.-XX:BiasedLockingStartupDelay=0。对应的就是锁升级图中 new 一个对象后会有两种情况。
- 偏向锁默认打开原因是:JVM 虚拟机自己有一些默认启动的线程,里面有好多 sync 代码,这些 sync 代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
- 偏向锁是否一定比自旋锁效率高:不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁不涉及锁撤销,效果高。
自旋锁:
- 有偏向锁升级而来,当有多个线程执行(>= 2)的时候,此时就会有竞争不能在采用偏向锁了。
- 多个线程通过竞争,某一个线程会将自己的线程指针写入 markword,标记自己占有,其他线程只能等待。
- 怎么等待呢,就是采用 CAS 的方式,不停的去获取 markword 上记录的指针信息,看是不是被占有,如果没有被占有就把自己的指针写进去。这种方式下等待的线程会占用 CPU 资源
- 所以自旋锁也没有经过内核态的操作,是轻量级锁。
- 每个线程有自己的 LockRecord 在自己的线程栈上,用 CAS 去争用 markword 的 LR 的指针,指针指向哪个线程的 LR,哪个线程就拥有锁。
重量级锁:
- 可以是自旋锁升级而来,自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。
- 重量级锁有等待队列 竞争队列,所有拿不到锁的进入等待队列,不需要消耗 CPU 资源。
- JDK6之前,一个线程自旋超过10次,或者等待的线程数超过 CPU 核数的1/2,升级为重量级锁,如果太多线程自旋 CPU 消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)。自旋次数和等待的线程数都可以通过参数控制。-XX:PreBlockSpin。
- 自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
- 自适应自旋锁意味着自旋的时间(次数)不再固定,根据历史情况由 JVM 来管理。
- 偏向锁耗时过长,或有 wait时也会进入重量级锁。
以上是关于详解 synchronize 锁的升级的主要内容,如果未能解决你的问题,请参考以下文章
详解synchronized和锁升级,以及偏向锁和轻量级锁的升级
Java多线程系列:深入详解Synchronized同步锁的底层实现
Java多线程系列:深入详解Synchronized同步锁的底层实现