多线程访问相同的共享可变的变量时,通常需要使用加锁的方式实现同步,避免竞态条件发生。同步的其中一个特征就是互斥,通过使访问顺序串行化,使得并发结果合法和可预期。synchronized是java的关键字,其通过java的内置锁机制实现多线程同步。
一、synchronized的三种锁形态
synchronized在jdk1.5前只有重量级锁,开销较大。为了应对低竞争的场景在jdk1.5增加了偏向锁和轻量级锁。对象的锁状态通过对象头里的markword标识。

1. 偏向锁

1)场景:在一个时刻只有一个线程请求锁,没有竞争;
2)关键参数:
-XX:+UseBiasedLocking # 是否开启偏向锁,jdk1.6以后默认开启
-XX:BiasedLockingStartupDelay=4000 # 偏向锁延迟启动时间,
-XX:BiasedLockingDecayTime=25000 # 重置撤销次数的时间阈值
-XX:BiasedLockingBulkRevokeThreshold=40 # 偏向锁批量撤销阈值
-XX:BiasedLockingBulkRebiasThreshold=20 # 偏向锁批量重偏向阈值
Note:
- 查看jvm各参数默认值
java -XX:+PrintFlagsInitial
- 查看jvm运行参数值(其中被赋值号为
:=
的值是被修改过,与初始值不同)
java -XX:+PrintFlagsFinal
- 查看jvm运行时的参数的命令
jinfo -flags <pid>
jinfo -flag <name> <pid>
3)实现原理:markword标识出偏向锁状态,且保存当前持有锁的线程ID。然后在其线程栈上隐式保存递归次数。

4)优点:在无竞争的情况下开销较小,包括cpu(无须切换上下文和线程调度)和内存(无须分配管程对象)开销。
5)缺点:锁撤销开销较大,需要挂起锁定线程,遍历其线程栈,修改锁记录。甚至比轻量级锁和重量级锁的开销还大。目前HotSpot使用安全点的STW和启发式撤销方法,减少撤销次数,提高性能。
6)可重偏向的条件
- 线程ID为空
- epoch无效
Note1: 当类元数据中的epoch与类对象的对象头中的epoch值相等时,表示当前偏向锁有效;否则该偏向锁失效,可重偏向;通过引入epoch减小了重偏向的开销;

Note2:在调用hashcode时会撤销偏向锁来还原hashcode的占位;
Note3: 调用wait,notify会膨胀为重量级锁。触发管程对象分配和初始化;
2. 轻量级锁(也称为thin lock)

1)场景:在一段时间内请求的锁的线程较少,且持有锁的线程执行时间短,持有锁的时间短,其他等待线程在有限的自旋等待期可以得到锁。
2)实现结构——栈上锁记录
锁记录在堆栈上分配,在解释执行期间显式分配,或在编译时隐式分配。锁定记录包含一个可以放置移位标记字的位置,以及一个指向被锁定对象的指针。对象指针在分配时由解释器或编译器初始化,当锁被薄锁或胖锁时,移位的标记字被初始化。
from: www.diva-portal.org/smash/get/diva2:754541/FULLTEXT01.pdf

3)锁升级过程
当一个线程试图获取一个已经被其他线程持有的轻量级锁(CAS失败)时,非所有者线程将尝试对该锁进行膨胀。在锁膨胀期间,为对象分配和初始化重量级对象监视器结构。膨胀线程将尝试在循环中对锁进行升级,直到成功,或直到其他线程成功地升级锁为止。
from: www.diva-portal.org/smash/get/diva2:754541/FULLTEXT01.pdf
Note1: 调用wait,notify会膨胀为重量级锁。触发管程对象分配和初始化;
3. 重量级锁(也称为fat lock)

1)管程对象——monitor
通过对象关联的管程对象实现线程阻塞,唤醒机制。其主要依赖操作系统级的互斥量来实现。
2)WaitSet,EntrySet

- 准备争抢会进入临界区的线程会进入EntrySet;
- 只有一个线程会进去临界区,独占锁;
- 获取锁的线程执行wait方法会让出锁,并进去WaitSet;
- WaitSet中的线程需要被唤醒才能进入EntrySet,然后再参与竞争;
3)优点:
在多线程并发激烈的情况下性能表现好,让未获得锁的线程让出时间片和cpu使用权,减少资源消耗。
四、与juc包ReentrantLock对比
ReentrantLock优点:
- 公平锁和非公平锁
- 阻塞和非阻塞
- 超时机制
- 读写锁
- 多个条件队列
网友评论