synchronized 特点:
- 开始是乐观锁,如果锁冲突,就转换为悲观锁
- 开始是轻量级锁,如果锁被持有的时间较长,就转换为重量级锁
- 实现轻量级锁的时候大概率用到的是自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
synchronized 加锁过程:
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。
- 1,无锁
顾名思义:就是没有加锁,这对于synchronized 这个加锁操作来说是不是很扯,其实啊它和乐观锁有点相似,乐观锁也是最初是直接尝试访问数据,并没有加锁,如果检测到锁冲突(其他线程来竞争锁),才会转换成悲观锁。 - 2,偏向锁
这个锁很厉害:首先一个线程会对该锁进行标记,来表示它已经占有了这个锁,但是实际上它并没有加锁,如果此时还有其他线程来竞争这把锁,该线程就会真正获取到锁。偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.但是该做的标记还是得做的, 否则无法区分何时需要真正加锁. - 3,自旋锁(轻量级锁)
随着其他线程进⼊竞争, 偏向锁状态被消除, 进⼊轻量级锁状态(⾃适应的⾃旋锁).
此处的轻量级锁就是通过 CAS 来实现.- 通过 CAS 检查并更新⼀块内存 (⽐如 null => 该线程引⽤)
- 如果更新成功, 则认为加锁成功
- 如果更新失败, 则认为锁被占⽤, 继续⾃旋式的等待(并不放弃 CPU).
⾃旋操作是⼀直让 CPU 空转, ⽐较浪费 CPU 资源.
因此此处的⾃旋不会⼀直持续进⾏, ⽽是达到⼀定的时间/重试次数, 就不再⾃旋了.也就是所谓的 “⾃适应”
- 重量级锁
如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁,此处的重量级锁就是指用到内核提供的mutex- 执行加锁操作,先进入内核态
- 在内核态判定当前锁是否已经被占用
- 如果该锁没有被占用,则加锁成功,并切换回用户态
- 如果该锁被占用,则加锁失败,此时线程进入锁的等待队列,挂起,等待被操作系统唤醒
- 经历了一系列的沧海桑田,这个锁被其他线程释放了,操作系统也想起了这个挂起的线程,于是唤醒这个线程,尝试重新获取锁。
其他优化操作
一
锁消除:编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
有些应⽤程序的代码中, ⽤到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个 append 的调⽤都会涉及加锁和解锁. 但如果只是在单线程中执⾏这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了⼀些资源开销.
锁粗化:⼀段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会⾃动进⾏锁的粗化.
举个例子:假如,你和领导汇报工作,汇报三件事
我们知道两个人在通话中第三个人是不能插进来的,这也和加锁很相似。
如果你打三次电话,分三次汇报这三件事,我相信领导在接到你的第三次电话的时候,很可能就想亲切地问候你一下了。
但是如果你一次电话就将这三件事汇报完,不仅能得到领导的表扬,还能节省自己的时间。
上诉案例,你将多次加锁完成的事,都堆加在一块只用一次加锁就完成,这样的操作我们称为 “ 锁粗化 ”。
相关题目
-
- 什么是偏向锁?
偏向锁不是真正的加锁,而是在锁对象中记录一个标记,如果没有其他线程参与竞争,那么就不会真正执行加锁操作,从而降低程序开销,一旦真的涉及到其他的线程竞争,再取消偏向锁状态,进入轻量级锁状态。
- 什么是偏向锁?
-
- synchronized 实现原理 是什么?
- 开始是乐观锁,如果锁冲突,就转换为悲观锁
- 开始是轻量级锁,如果锁被持有的时间较长,就转换为重量级锁
- 实现轻量级锁的时候大概率用到的是自旋锁策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
- synchronized的加锁过程是:无锁 -> 偏向锁 -> 自旋锁 -> 重量级锁
- 之后再按照我们上面讲的,再一 一介绍一下这几种状态就行了