synchronized
synchronized
对象锁,保证了临界区内代码的原子性
采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
互斥和同步都可以采用 synchronized 关键字来完成,但也是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
使用
同步块
锁对象:理论上可以是任意的唯一对象
synchronized是可重入、不公平的重量级锁
原则上:
- 锁对象建议使用共享资源
- 在实例方法中使用 this 作为锁对象,锁住的 this 正好是共享资源
- 在静态方法中使用类名 .class 字节码作为锁对象,因为静态成员属于类,被所有实例对象共享,所以需要锁住类
synchronized (锁对象) {
// 访问共享资源的代码
}同步方法
把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问。
- 用法:直接给方法加上一个修饰符 synchronized
注意-同步方法锁对象是不同的
- 锁普通方法时,锁对象是 this,也就是当前实例对象
- 锁静态方法时,锁对象是类的字节码对象,也就是当前类的 Class 对象
:::
//同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
//同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}锁原理-核心
Monitor
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中。
如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁。Mark Word 结构-最后两位是锁标志位:

工作流程
开始时 Monitor 中 Owner 为 null
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,
obj 对象的 Mark Word 指向 Monitor,把对象原有的 MarkWord 存入线程栈中的锁记录中
- 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行
synchronized(obj),就会进入 EntryList BLOCKED(双向链表)里面 - Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
- 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞
- WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制)

补充说明
synchronized必须是进入同一个对象的 Monitor 才有上述的效果- 不加
synchronized的对象不会关联监视器,不遵从以上规则
字节码层面
在编译阶段,synchronized 代码块会被编译成两个字节码指令:
- monitorenter:进入同步块时执行,尝试获取锁。
- monitorexit:退出同步块时执行,释放锁。
- 为了保证异常也能释放锁,编译器会在异常路径中也插入 monitorexit 指令。
锁升级
因为 synchronized 是可重入、不公平的重量级锁,所以需要对其进行优化。
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级
偏向锁
被废弃
在JDK18彻底被废弃。废弃原因:
- 性能收益不明显。
- JVM 内部代码维护成本太高。
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作。
- 引入偏向锁的目的:在没有多线程竞争的情况下,尽量减少不必要的轻量级锁的执行。轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只依赖一次CAS原子指令
轻量级锁
当偏向锁被另一个线程竞争时,偏向锁会升级为轻量级锁。轻量级锁的实现是通过 CAS 操作来尝试获取锁,如果获取失败,则会进入自旋状态,继续尝试获取锁。