synchronized vs CAS
虽然标题为synchronized vs CAS,但从大的范围来说其实是:悲观锁 vs 乐观锁,本文使用synchronized和CAS来进行介绍,并对比。
首先锁的出现是为了,保证同一时间内,只有一个线程进入临界区。
什么是悲观锁和乐观锁?
悲观锁:对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
乐观锁:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
synchronized实现原理
static final Object lock = new Object();
static int cnt = 0;
public static void main(String[] args) {
synchronized (lock) {
cnt++;
}
}
查看对应的字节码会发现是这样的:【1】
0 getstatic #2 <com/optimjie/Main.lock : Ljava/lang/Object;>
3 dup
4 astore_1
5 monitorenter
6 getstatic #3 <com/optimjie/Main.cnt : I>
9 iconst_1
10 iadd
11 putstatic #3 <com/optimjie/Main.cnt : I>
14 aload_1
15 monitorexit
16 goto 24 (+8)
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return
解释字节码前先看一下synchronized的底层实现monitor
synchronized的底层依赖一个叫ObjectMonitor模式实现,这是JVM内部C++实现的机制。【2】
重要的几个字段:
- _owner:指向持有ObjectMonitor对象的线程地址。
- _WaitSet:存放调用wait方法,而进入等待状态的线程的队列。
- _EntryList 这里是等待锁block状态的线程的队列。
- _recursions 锁的重入次数。
- _count 线程获取锁的次数。
加锁与解锁过程:
- 如果_owner为空,则将_owner改为当前线程,这里是通过CAS
- 其他线程再来获取锁时,会进入到_EntryList中去
- 当_owner线程释放锁时,会唤醒_EntryList中的线程,这里竞争是非公平的
看到这里其实会发现AQS和这里的monitor的作用是很类似的:( [1] synchronized和ReentrantLock对比)
- 变量
- 挂起和唤醒线程
- 使用一个集合来管理线程
回到上面的字节码中,monitorenter其实就是将lock对象的mark word中的重量级锁的指针指向monitor对象并且将_owner置为当前线程,而monitorexit就是将mark word中的指针重置,并且唤醒_EntryList中等待的线程。而这里有两处monitorexit,是因为19-22这几个字节码是临界区内发生异常时来执行的命令,说白了就是异常情况下也要释放锁。
JVM中对于synchronized的优化
简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。【3】
主要升级过程为(只能升,不能降):
-
偏向锁:只有一个线程加锁时,没有竞争
- 当某个线程尝试获取锁时,查看thread是否为当前线程(这是对象头中应该是无锁或者偏向锁的状态),如果没有的话,就是用CAS修改mark work,将thread改为当前线程(OS),执行完临界区的代码后,并不会清空thread,因为下一次该线程再进来时可以查看thread是不是当前的线程
-
轻量级锁:有多个线程加锁时,没有竞争
- 当上面的线程执行结束后,另一个线程再进入临界区时会升级为轻量级锁。
加锁过程:
- 当前栈帧中创建一个lock record,同时在lock record中分配mark word的空间
- 通过cas将mark word拷贝到lock record中,同时将lock的mark word中ptr_to_lock_record指向栈帧中的lock record,锁标记改为00
- 如果cas失败的话有两种情况,一种是重入了那么会在当前栈帧中新建一个lock record,只不过cas拷贝的这里是null,第二种情况就是存在竞争力,这种情况下轻量级锁会失效,锁会升级为重量级锁,此时锁标记为10,同时指针指向重量级锁,也就是monitor
解锁过程:
- 从栈帧冲弹出lock record,如果是null就直接弹出吧,如果不为null,就通过cas将lock record恢复给mark word,这样lock的mark word就和加锁前是一样的了。当然这里cas也有可能失败,这里我期望当前lock的mark word中锁标记应该是00,但是有可能发生了竞争导致升级为重量级锁,锁标记就成10了,此时就应该走重量级锁的解锁过程了
-
重量级锁:有多个线程加锁时,有竞争
- 上面提到了当一个线程拿到锁之后,另一个线程再想通过cas将对象头的mark word拷贝到栈帧中的lock word的话就会失败
- 为lock对象申请Monitor锁,让lock指向重量级锁的地址
- 自己进入到Monitor的_EntryList中,这个时候Monitor的_owner为线程A,即之前通过轻量级锁的方式拿到锁的线程
- 当线程A执行完同步代码块中的内容后,执行轻量级的解锁过程,即通过cas的方式,将mark word的值拷贝给lock对象,这时候会失败,之后走重量级锁的解锁过程(这个上面也说到了)
- 将Monitor的_owner置为null,唤醒_EntryList中BLOCKED的线程(这里是非公平的),Monitor中还有WaitSet,这里面存的是之前获得过锁,但是条件不满足进入到WAITING的状态
到这里就会发现这个过程其实就是对上面monitor加锁和解锁的细化
上面的问题
- synchronized和ReentrantLock对比
参考
- 黑马程序员深入学习Java并发编程,JUC并发编程全套教程
- https://www.cnblogs.com/minikobe/p/12123065.html
- https://www.pdai.tech/md/java/thread/java-thread-x-key-synchronized.html

浙公网安备 33010602011771号