语法
synchronized(锁对象) // 线程1, 线程2(blocked)
{
临界区
}
注意
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样?
此种方式相当于多个线程持有不同的锁,不会起到保证临界区代码是原子性操作,达不到预期 - 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?
此种方式相当于一个线程持有锁一个线程没有锁,不会起到保证临界区代码是原子性操作,达不到预期
方法上的synchronized形式
- 形式一
加在非静态方法上,锁对象为调用者本身
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
- 形式一
加在静态方法上,锁对象为类对象
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
对象头
Monitor(锁)
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
执行流程
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4 也来执行 synchronized(obj),就会进入EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,条件不满足时调用wait() 进入 WAITING 状态的线程
注意
- synchronized 对线程加锁时,该锁对象必须为重量级锁才会有上述效果
- synchronized 必须是进入同一个对象的 monitor 才有上述的效果
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
锁变化过程
锁消除
- 这是一种JIT优化代码得方案,如果加的锁对象,而这个锁对象又是局部变量,又不会被外部锁引用,此时就会将锁进行消除。
偏向锁
相关概念
- 只有第一次使用 CAS 将线程 ID 设置到对象头的 Mark Word ,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
- JDK6引入此概念,来优化没有线程竞争锁时,每次重入加轻量级锁 CAS 操作
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -
XX:BiasedLockingStartupDelay=0 来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
- 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
撤销偏向锁
方式一:调用对象 hashCode
- 原因:因为hashCode(31位)会保存在对象头中,如果在存在偏向锁,对象头中又要存储线程ID(54位),这时就会在存储线程id及hashcode之间冲突,所以调用对象的hashCode方法会撤销偏向锁
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
方式二:其它线程使用对象
- 原因:当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
方式三:调用 wait/notify
- 原因:因为这两个方法都会涉及monitor对象,而monitor又只会在重量级锁中出现,所以如果调用了这两个方法会导致偏向锁撤销
批量重偏向
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
- 当撤销偏向锁阈值超过 20 次后,jvm 会认为是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程
批量撤销
- 当撤销偏向锁阈值超过 40 次后,jvm 会认为确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
轻量级锁
- 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
- 轻量级锁对使用者是透明的,即语法仍然是 synchronized
实例代码
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
加解锁过程
重量级锁
- 示意图与monitor结构示意图类似,这里不在赘述
重量级锁自旋优化
- 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能
- Java 7 之后不能控制是否开启自旋功能
与ThreadLocal 的区别
- synchronized 设计思路:采用 “以时间换空间” 的思路,共享变量只有一份,让不同的线程排队访问。
- ThreadLocal 设计思路:采用 “以空间换时间的” 的思路,将共享变量进行副本复制,让不同的线程拥有自己共享变量副本,从而保证共享变量在不同的线程之间进行隔离,解决并发访问变量问题
ThreadLocal 详解:https://blog.csdn.net/silence_yb/article/details/124265702