synchronized
用法
-
同步方法
public synchronized void instanceMethod() { // 锁是当前实例对象(调用哪个对象的这个方法,哪个对象就是锁对象) } public static synchronized void staticMethod() { // 锁是当前类的 Class 对象 }
-
同步代码块
public void method() { synchronized (lockObject) { // 锁是 lockObject(可以指定为一个 Class,也可以指定为一个对象) } } public void method2() { synchronized { // 锁是当前实例(this,可以省略,哪个对象调用这个方法,哪个对象就是锁对象) } }
原理
class User {
public synchronized void work() {
// ...
}
}
public class App {
public static void main(String[] args) {
User user = new User();
user.work();
}
}
- 主方法中创建了一个 user 对象,此时 user 只是一个普通的对象,对象头没有锁信息
- 调用 user 对象的 work
- work 是个同步方法,同步方法的锁对象是当前实例(所以 user 将会是一个锁对象)
- 没有别的线程调用这个同步方法,只有主线程在调用,所以这里一定能获取锁成功
- 把当前线程 id 写进 user 对象的对象头的 markdown 中,表示当前线程持有这个锁
- 对象头还维护了一个锁计数器,默认是 0,因为获取成功了,锁计数器 +1
- 此时如果还有别的线程获取这个锁(别的线程也调用这个 user 对象的同步方法),先对比线程 id
- 如果线程 id 相同,说明是同一个线程,直接进入,锁计数器+1(可重入)
- 如果线程 id 不相同,就看锁计数器是否是 0
- 如果是 0,修改对象头的线程 id 为当前线程 id锁计数器 +1
- 如果不是 0,当前线程 CAS 自旋,不断尝试,直到把自己的线程 id 写入 user 对象的对象头中(还未升级到重量级)
- 主线程调用方法结束,释放锁,锁计数器 -1,别的线程可以获取到锁了
反编译字节码文件后可以看到 monitorenter
、monitorexit
两个指令,monitorenter
是用于获取锁,monitorexit
是用于释放锁。需要注意有两个 monitorexit
指令,一个是正常释放,一个是发生异常时释放,这算 synchronized 的方便之处吧,不用显示释放锁并且一定会释放锁
锁优化
jdk6 对 synchronized 做了一系列优化,比如锁粗化、锁消除、偏向锁、轻量级锁 等来减少开销,不到万不得已不会升级为重量级锁,举一些简单的例子来描述这些优化技术
-
锁粗化:减少不必要的紧连在一起的 lock、unlock,操作,将多个连续的锁扩展成一个范围更大的锁
// 多个同步代码块,要多次获取锁、释放锁 public void method() { synchronized (this) { doService1(); } synchronized (this) { doService2(); } synchronized (this) { doService3(); } } // 但是锁对象都是同一个对象,优化后可能成为下面的样子 public void method() { synchronized (this) { doService1(); doService2(); doService3(); } }
-
锁消除:取消没必要的锁开销
// 无用的锁,因为每次调用这个方法,都会创建一个新的 user 对象,所以每次的锁对象都不一样,其实完全没必要加锁 public void method2() { User user = new User() synchronized(user) { doService(); } } // 优化后 public void method2() { doService(); }
-
偏向锁:直接把线程 id 记录到对象头中,表示该线程持有这个锁,如果一直没有别的线程竞争,那么这个锁一直是这个线程持有
线程安全实现起来又复杂又麻烦,好不容易花大代价实现了,其实可能真实场景中压根不会出现多线程竞争锁的问题
-
轻量级锁:其他线程 CAS 自旋,在指定的次数里不断尝试把当前线程的线程id写入锁对象的对象头中
偏向锁是假设没有多线程问题,如果真有多线程来竞争锁呢?偏向锁就兜不住了,这时竞争的线程就 CAS 自旋,在指定的次数中不断尝试将自己的线程 id 写入锁对象的对象头中
到底多少个线程竞争锁会从偏向锁升级为轻量级锁呢?CAS 自旋几次呢,是无限次自旋直至成功吗?只要有超过一个线程就会升级为轻量级锁,自旋次数是可由 jvm 参数配置
# 生产环境需要自己搭配下面的参数,不要一股脑都配上,并且还有没有罗列的参数 -XX:UseSpinning=1 # 启用自旋锁 -XX:UseSpinning=0 # 禁用自旋锁 -XX:+UseAdaptiveSpinning # JVM 会根据锁竞争情况动态调整自旋次数 -XX:MinSelfSpin=0 # 设置自旋锁的自旋次数的最小值 -XX:MaxSelfSpin=10 # 设置自旋锁的自旋次数的最大值
锁升级
上面原理已经详细说过了,这里总结一下
- 对象刚创建是没有锁的,无锁状态
- 当对象作为锁时,jvm 先假设没有线程竞争,先升级为偏向锁
- 当有线程竞争时,但竞争不激烈,升级为轻量级锁(CAS锁、自旋锁)
- 激烈竞争时,升级为重量级锁
为什么有轻量级锁还要重量级锁?线程自旋是要消耗 CPU 的,当线程数量过多显然不适合继续自旋。重量级锁会把这些等待的线程放进一个队列,这个队列里面的线程不会消耗 CPU
重量级锁
重量级锁究竟是个啥?为什么叫重量级锁,开销大在哪里?
首先要明白用户态和内核态
用户态:目录访问、文件创建、数据库访问、http/https 等有限的权限
内核态:磁盘、网络接口、显卡等信息,还包括直接内存、驱动程序高级权限
如 java 程序要访问显卡信息,需要切换到内核态,调用操作系统的指令接触操作系统来完成,然后再把访问结果相应给用户态。总结就是不是不能访问,是要花代价才能访问,再比如 Unsafe 类简介访问内存
java 的重量级锁不是 JDK 实现的,就是通过用户态和内核态交互来完成的,是操作系统来保证的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具