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();
    }
}
  1. 主方法中创建了一个 user 对象,此时 user 只是一个普通的对象,对象头没有锁信息
  2. 调用 user 对象的 work
  3. work 是个同步方法,同步方法的锁对象是当前实例(所以 user 将会是一个锁对象)
  4. 没有别的线程调用这个同步方法,只有主线程在调用,所以这里一定能获取锁成功
    1. 把当前线程 id 写进 user 对象的对象头的 markdown 中,表示当前线程持有这个锁
    2. 对象头还维护了一个锁计数器,默认是 0,因为获取成功了,锁计数器 +1
  5. 此时如果还有别的线程获取这个锁(别的线程也调用这个 user 对象的同步方法),先对比线程 id
    1. 如果线程 id 相同,说明是同一个线程,直接进入,锁计数器+1(可重入)
    2. 如果线程 id 不相同,就看锁计数器是否是 0
      1. 如果是 0,修改对象头的线程 id 为当前线程 id锁计数器 +1
      2. 如果不是 0,当前线程 CAS 自旋,不断尝试,直到把自己的线程 id 写入 user 对象的对象头中(还未升级到重量级)
  6. 主线程调用方法结束,释放锁,锁计数器 -1,别的线程可以获取到锁了

反编译字节码文件后可以看到 monitorentermonitorexit 两个指令,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 # 设置自旋锁的自旋次数的最大值
    

锁升级

上面原理已经详细说过了,这里总结一下

  1. 对象刚创建是没有锁的,无锁状态
  2. 当对象作为锁时,jvm 先假设没有线程竞争,先升级为偏向锁
  3. 当有线程竞争时,但竞争不激烈,升级为轻量级锁(CAS锁、自旋锁)
  4. 激烈竞争时,升级为重量级锁

为什么有轻量级锁还要重量级锁?线程自旋是要消耗 CPU 的,当线程数量过多显然不适合继续自旋。重量级锁会把这些等待的线程放进一个队列,这个队列里面的线程不会消耗 CPU

重量级锁

重量级锁究竟是个啥?为什么叫重量级锁,开销大在哪里?

首先要明白用户态和内核态

用户态:目录访问、文件创建、数据库访问、http/https 等有限的权限

内核态:磁盘、网络接口、显卡等信息,还包括直接内存、驱动程序高级权限

如 java 程序要访问显卡信息,需要切换到内核态,调用操作系统的指令接触操作系统来完成,然后再把访问结果相应给用户态。总结就是不是不能访问,是要花代价才能访问,再比如 Unsafe 类简介访问内存

java 的重量级锁不是 JDK 实现的,就是通过用户态和内核态交互来完成的,是操作系统来保证的

posted @   CyrusHuang  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示