synchronized

synchronized 的基本使用

synchronized 是 Java 中实现线程同步的关键字,它提供了三种基本的使用方式:

1 同步实例方法

public synchronized void method() {
    // 同步代码
}

这种方式锁的是当前实例对象(this),哪个对象调用这个方法,哪个对象就是锁对象

比如 user.method() user 就是锁对象

2 同步静态方法

public static synchronized void staticMethod() {
    // 同步代码
}

这种方式锁的是当前类的 Class 对象,JVM 级别全局唯一

比如 User.staticMethod() User.class 是锁对象

3 同步代码块

public void method() {
    synchronized(obj) {
        // 同步代码
    }
}

这种方式可以指定锁对象,obj 可以是普通对象也可以是 Class 对象

synchronized 底层原理

synchronized 代码对应的字节码

通过 javap 反编译 synchronized 代码,可以看到以下关键字节码指令:

对于同步方法:

  • 方法访问标志中会添加 ACC_SYNCHRONIZED 标志

对于同步代码块:

  • 使用 monitorentermonitorexit 指令包裹同步代码块

示例:

public void syncMethod() {
    synchronized(this) {
        System.out.println("Hello");
    }
}

对应的字节码:

public void syncMethod();
  Code:
     0: aload_0
     1: dup
     2: astore_1
     3: monitorenter          // 进入同步块
     4: getstatic     #2      // Field java/lang/System.out:Ljava/io/PrintStream;
     7: ldc           #3      // String Hello
     9: invokevirtual #4      // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: aload_1
    13: monitorexit           // 正常退出同步块
    14: goto          22
    17: astore_2
    18: aload_1
    19: monitorexit           // 异常退出同步块
    20: aload_2
    21: athrow
    22: return

监视器锁(Monitor)机制

概念介绍

Java 中每个对象都与一个监视器(Monitor)相关联,synchronized 的实现依赖于这个监视器锁

HotSpot 虚拟机中,监视器存在在对象头的 Mark Word 部分。监视器结构如下:

属性 含义 解释
计数器(_count) 记录线程重入次数 0 表示未被线程持有,大于0表示重入次数
所有者字段(_owner) 记录持有锁的线程 记录的是线程ID
等待集合(_WaitSet) 存储等待的线程 Object.wait()
入口集合(_EntryList) 存储阻塞等待锁的线程 1,获取锁失败
2,Object.notify()Object.notifyAll()
  • _WaitSet:当线程持有锁时调用 Object.wait() 方法,线程会释放锁并进入 _WaitSet
  • _EntryList:当其他线程调用 Object.notify()Object.notifyAll() 方法,_WaitSet 中的随机一个或全部转移到 _WaitSet
1. 线程A进入同步块,获取锁
2. 线程B尝试进入同步块,发现锁被A持有,进入 _EntryList
3. 线程A调用 wait(),释放锁并进入 _WaitSet
4. 线程B从 _EntryList 中被选中,获取锁
5. 线程B调用 notify(),线程A从 _WaitSet 移到 _EntryList
6. 线程B退出同步块释放锁
7. 线程A从 _EntryList 中获取锁继续执行

sleep() 不会进入 _WaitSet 或 _EntryList,因为不会释放锁

LockSupport.park() 也不会进入 _WaitSet 或 _EntryList,底层依靠 Unsafe 实现
park():消耗 permit(如果有立即返回,否则就阻塞)
unpark():提供 permit(如果线程已阻塞则唤醒)

AQS 的 Condition.await() 也不会进入 _WaitSet 或 _EntryList,底层依靠条件队列实现
await():进入条件队列
signal():条件队列转入同步队列

获取锁过程

当线程执行到 monitorenter 指令时

  • 如果计数器为0,表示锁未被占用,线程获取锁并将计数器置1,设置自己为所有者
  • 如果线程已经持有锁,计数器加1(重入)
  • 如果锁被其他线程持有,当前线程进入阻塞状态,直到锁被释放

释放锁过程

当线程执行到 monitorexit 指令时

  • 计数器减1
  • 如果计数器变为0,表示完全释放锁,唤醒等待线程

synchronized 内存语义

  • 进入同步块:会清空工作内存,从主内存重新读取共享变量
  • 退出同步块:会把工作内存中的修改刷新到主内存

synchronized 总结

  • 原子性保证:Monitor 机制
  • 可见性保证:JMM 和 MESI,参考 volatile
  • 有序性保证:as-if-serialhappens-before 原则和内存屏障,参考 volatile

JVM 对 synchronized 的优化

锁升级

  1. 无锁状态:初始状态
  2. 偏向锁:适用于只有一个线程访问同步块的场景
    • 在 Mark Word 中记录线程ID
    • 同一线程再次进入时不需要任何同步操作
  3. 轻量级锁:适用于线程交替执行同步块的场景
    • 通过 CAS 操作尝试获取锁
    • 如果失败,升级为重量级锁
  4. 重量级锁:适用于多线程竞争激烈的场景
    • 使用操作系统的互斥量(mutex)实现
    • 线程阻塞和唤醒需要从用户态切换到内核态,开销较大

锁粗化

// 多个同步代码块,要多次获取锁、释放锁
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();
}
posted @ 2024-08-30 13:40  CyrusHuang  阅读(35)  评论(0)    收藏  举报