1. 写在前面
本章节主要介绍 synchronized 的一些内部优化机制,这些机制存在的目的呢就是让 synchronized 这把锁更高效更好用!
2. 锁升级/锁膨胀
JVM 将 synchronized 锁分为以下四种状态:
无锁,偏向锁,轻量级锁,重量级锁
在 synchronized 进行加锁的时候,首先会进入到偏向锁的状态,偏向锁不是真正的加锁,而是占个位置,有需要再加锁,没有需要就不加锁,这样一来则减少了加锁解锁的开销,一旦在使用过程中,另一个线程也尝试加锁,那么在另一个线程加锁前,持有偏向锁状态的线程会迅速的把偏向锁升级为真正的加锁状态。
如果在使用过程中,没有其他线程尝试加锁,也就是没有出现锁竞争,那么在 synchronized 执行完后,取消偏向锁即可。
当 synchronized 发生锁竞争时,就会从偏向锁升级成轻量级锁,此时 synchronized 相当于是通过自旋的方式来进行加锁的。
升级成轻量级锁后,如果其他线程很快的释放锁,自旋的方式是很划算的,如果迟迟拿不到锁,一直自旋占用 CPU 资源其实并不划算,而 synchronized 并不是无休止的自旋,自旋到一定程度,发现还是获取不到锁,就会再次升级成重量级锁(挂起等待锁)。
在 synchronized 内部的自旋循环中,有一个计数器,记录循环了多少次,循环多久了,达到一定程度就会执行重量级锁的逻辑,如果线程进行了重量级加锁,并且发生了锁竞争,此时未获取到锁的线程就会被放入阻塞队列中,暂时不参与 CPU 调度了,直到锁释放,才有机会被调度到,才有机会获取到锁。
注意:在 JVM 主流实现中,没有锁降级,当前锁只能升级,只要指定的锁对象,已经被升级了,就回不了头了!
3. 锁消除
锁消除是由编译器智能判定的,看当前的代码是否有必要加锁,如果当前的场景不需要加锁,程序猿加了也是白加,编译器就会自动把锁给消除掉。
比如 StringBuffer 很多关键方法都带有 synchronized,但是如果在单线程中使用 StringBuffer,此时加锁与不加锁完全没有任何区别,而且加锁还有更多的开销,于是编译器就会把这些加锁操作给自动取消了,这就是锁消除机制。
4. 锁粗化
这里就涉及到一个术语,锁的粒度。
锁的粒度:synchronized 包含的代码越多,粒度就越粗,包含的代码越少,粒度就越细。
举个例子:
public void test() {synchronized (this) {// 10w 行代码...// ...}
}
这里的写法就相当夸张了,开发中基本不存在,但这样显而易见一个 synchronized 包裹的代码块中有 10w 行代码,这里的粒度是非常粗的,我们要尽量避免这种情况,在通常情况下,锁的粒度越小是越好的。
因为加锁部分的代码是不能并发执行的,粒度越细,能并发的代码就越多了。
但是在有些情况下,锁的粒度真的越细越好吗?其实也不一定,比如:
public void test() {synchronized (this) {// 10 行代码...}// 2 行代码...synchronized (this) {// 10 行代码...}// 2 行代码...synchronized (this) {// 10 行代码...}
}
此时两次相邻加锁之间,间隙非常少,此时还不如用一个 synchronized 包裹起来!
为什么,因为加锁解锁也是有开销的!
这里试想一下,有一天领导给你安排了三个任务,领导要求你做完后打电话进行汇报。
做法1:
每当完成一个任务就打电话给领导汇报一次:
第一次打电话:领导,我任务一完成了
第二次打电话:领导,我任务二完成了
第三次打电话:对不起,您拨打的电话正在通话中,请稍后再拨...
最后领导不耐烦,你被炒鱿鱼了。
做法2:
把三个任务都完成了,一次性跟领导汇:
打电话:领导,我任务一,二,三都完成了!领导:小伙子不错啊!
最后领导满意,你升职加薪。
所以我们要结合代码来适当的调整锁的粒度
5. 常见锁策略相关面试题
5.1 你是如何理解乐观锁和悲观锁的?
乐观锁认为多个线程访问同一个变量冲突的概率不大,所以乐观锁也不会真正的加搜,会直接尝试访问数据,在访问的同时去识别当前数据是否出现访问冲突,也就是引入一个版本号,借助版本号来识别当前的数据访问是否冲突了
悲观锁的实现就是先加锁,他认为多个线程访问同一个变量的冲突率很大,每次都会真正的加锁,比如借助操作系统提供的mutex,只有获取到了锁,才会操作数据,获取不到锁就会阻塞等待
5.2 介绍下读写锁
读写锁就是把读操作和写操作分别进行加锁
-
读锁和读锁之间不存在互斥
-
写锁和写锁之间存在互斥
-
写锁和读锁之间存在互斥
读写锁最主要用在"频繁读,不频繁写"的场景中
5.3 什么是自旋锁,为什么要使用自旋锁策略呢?缺点是什么?
自旋锁如果获取锁失败,就会立即尝试获取锁,无限循环,获取到锁位置,这样的好处是,一旦锁被释放,就能在第一时间发现,也就是能第一时间获取锁,但如果其他线程锁持有的时间太长,就会浪费CPU资源,所以自旋锁更适合在锁持有时间短的场景下使用
5.4 synchronized 是可重入锁吗?
是可重入锁,可重入锁指的是一个线程对同一个对象连续加锁两次,如果没有出现死锁,就是不可重入锁
具体实现是在锁中记录该锁持有的线程身份,以及一个计数器(记录加锁次数),如果发现当前加锁的线程是持有锁的线程,则直接计数自增。
下期预告:【多线程】JUC的常见类