- synchronized 是 Java 提供的保证临界区域并发安全的关键字,synchronized 有互斥性、阻塞性、可重入性等特点
1、对象头
-
每个对象都有一个对象头,包括Mark Word和Klass Word
- Klass Word 指向该对象的类对象
- Mark Word
2、偏向锁
-
Java 6 中引入了偏向锁,第一次加锁时使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
- 偏向锁默认是开启的,对象创建后,Mark Word 值最后 3 位为 101,它的thread、epoch、age 都为 0
- 偏向锁默认是延迟的,不会在程序启动时立即生效,程序启动一段时间后创建的对象才会生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
- 可以加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁,对象创建后,Mark Word 值为最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
(1)偏向撤销
-
调用了对象的 hashCode
- 因为偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode,MarkWord 无法同时存储线程 id 和 hashCode(存不下)
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
-
其他线程对对象加锁
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁,解锁后对象是正常状态,而不是偏向状态
-
调用 wait/notify
- wait/notify 只有重量级锁才有
(2)批量重偏向
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程1的对象仍有机会重新偏向线程2,重偏向会重置对象的 Thread ID
- 当撤销偏向锁阈值超过 20 次后,jvm 会在给这些对象加锁时重新偏向至加锁线程
(3)批量撤销
- 当撤销偏向锁阈值超过 40 次后,jvm 会觉得根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
3、轻量级锁
-
当线程执行 synchronized(obj) 加锁时,在栈针中创建一个 Lock Record 对象
- Lock Record 对象包括指针(指针指向 obj)和 Mark Word (锁记录地址和锁状态00)
-
使用 cas 尝试将 Lock Record 对象的 Mark Word 和 obj 的 Mark Word 替换
-
cas 成功代表加锁成功
-
cas 失败
-
如果是其它线程已经持有了该 obj 的轻量级锁,这时表明有竞争,进入锁膨胀过程
-
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 此时的 Lock Record 指针还会指向 obj,但是 Mark Word 为 null
-
-
-
执行完代码释放锁
-
如果 Lock Record 对象的 Mark Word 为null,表示冲入,直接去掉(重入计数减一)
-
如果 Lock Record 对象的 Mark Word 不为null,使用 cas 恢复 obj 的 Mark Word
- cas 成功,解锁成功
- cas 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
-
4、重量级锁
-
Monitor 对象包括ower(当前加锁线程)、count(加锁次数)、waitSet(处于wait状态的线程)、entryList(处于等待锁状态的线程)等
- 线程1调用 synchronized(obj) 方法时,obj 对象头的 Mark Word 设置一个指针,指向一个 Monitor 对象
-
将 Monitor 的 ower 设置为线程1,计数器count++
- 同一个线程可多次加锁,计数器从0,加一次锁 count 加1
- 此时线程2(线程3、线程4 ... 线程n)调用 synchronized(obj) 方法,会发现 Monitor 对象的计数器 count 不为0,且 ower 也不是自己,然后将自己加入 entryList,此时等待锁的线程状态为 BLOCKED
-
当线程1执行完毕,释放锁时,会将计数器 count--,ower 设置为空,唤醒 entryList 中的线程竞争锁
- 可重入锁,每释放一次 count 减1,最后 count 为0
- 唤醒 entryList 中的线程竞争锁是非公平的
5、自旋优化
锁竞争的时候,还可以使用自旋(循环尝试获取锁)来进行优化
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
6、锁粗化
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化
- 线程1循环 synchronized(obj),会优化成 synchronized(obj) 循环
7、锁消除
jvm 会对多次执行的代码会使用 JIT (即使编译器)进行优化
- 线程1执行 synchronized(obj) ,但是 obj 对象只在线程1中使用,其他线程不会用到,此时就会消除 synchronized
- 可以通过 -XX:-EliminateLocks 参数,取消锁消除优化