java多线程Synchronized如何实现可见性

发布时间:2021-12-03 09:04:15 作者:小新
来源:亿速云 阅读:421
# Java多线程Synchronized如何实现可见性

## 一、前言

在Java多线程编程中,**可见性**与原子性、有序性并称为并发编程的三大核心问题。当多个线程共享同一变量时,一个线程对变量的修改可能无法立即被其他线程感知,这种现象称为**可见性问题**。`synchronized`作为Java中最基础的同步机制,不仅能够保证操作的原子性,还能确保变量的可见性。本文将深入剖析`synchronized`关键字如何通过JVM底层机制实现多线程间的可见性。

---

## 二、可见性问题的本质

### 2.1 现代计算机的内存架构
现代计算机采用多级缓存结构(寄存器→L1/L2/L3缓存→主内存),CPU直接操作的是缓存而非主内存。这种设计导致:

1. **线程工作内存与主内存分离**  
   每个线程有自己的工作内存(Working Memory),存储共享变量的副本
2. **写操作延迟**  
   线程修改变量后,新值可能暂时停留在工作内存而未刷回主内存
3. **读操作滞后**  
   线程读取变量时可能直接从工作内存获取旧值

### 2.2 JMM(Java内存模型)规范
JMM定义了线程与主内存的交互规则:
- **主内存**:存储所有共享变量
- **工作内存**:每个线程私有的存储空间
- **交互协议**:通过8种原子操作(lock/unlock/read/load/use/assign/store/write)控制数据同步

```java
// 典型可见性问题示例
public class VisibilityProblem {
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {} // 可能永远无法退出循环
            System.out.println("Thread stopped");
        }).start();

        Thread.sleep(1000);
        flag = false; // 主线程修改可能对子线程不可见
    }
}

三、Synchronized的可见性实现机制

3.1 同步代码块的内存语义

当使用synchronized时,JVM会隐式插入内存屏障(Memory Barrier):

  1. 进入同步块时

    • 清空工作内存中该对象关联的所有变量副本
    • 从主内存重新加载最新值(相当于执行read-load操作)
  2. 退出同步块时

    • 将工作内存中的修改强制刷新到主内存(相当于执行store-write操作)
public class SynchronizedVisibility {
    private int count = 0;
    
    public synchronized void increment() {
        count++; // 操作具有原子性+可见性保证
    }
}

3.2 底层实现原理

通过字节码分析可见实现细节:

// 源代码
public void syncMethod() {
    synchronized(this) {
        // 临界区代码
    }
}

// 对应的字节码
0: aload_0
1: dup
2: astore_1
3: monitorenter   // 获取锁时触发内存屏障
4: aload_1
5: monitorexit    // 释放锁时触发内存屏障
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return

3.3 与happens-before的关系

根据JMM的happens-before原则: - 监视器锁规则:解锁操作happens-before后续的加锁操作 - 传递性规则:前一个线程的所有操作对后续获得锁的线程可见

java多线程Synchronized如何实现可见性


四、Synchronized与其他同步机制对比

4.1 与volatile的比较

特性 synchronized volatile
原子性 保证 不保证
可见性 保证 保证
有序性 保证(as-if-serial) 有限保证
适用场景 复杂操作同步 单一变量可见性

4.2 与Lock接口的比较

ReentrantLock通过AQS实现类似的可见性保证,但: - 需要显式调用lock()/unlock() - 提供更灵活的尝试获取锁机制 - 支持公平/非公平锁选择


五、实践中的注意事项

5.1 锁对象的选择

// 正确的锁对象用法
private final Object lock = new Object();

public void safeMethod() {
    synchronized(lock) {
        // 临界区代码
    }
}

5.2 锁粒度控制

5.3 避免锁逃逸

不要将锁对象暴露给外部代码:

// 错误示例:锁可能被外部修改
public Object getLock() {
    return lock;
}

六、性能优化建议

  1. 减少同步块长度
    只将真正需要同步的代码放入临界区

  2. 使用读写锁优化
    对于读多写少场景,考虑ReentrantReadWriteLock

  3. 锁分离技术
    LinkedBlockingQueue分离put/take锁

  4. JVM锁优化

    • 偏向锁(Biased Locking)
    • 轻量级锁(Lightweight Locking)
    • 自旋锁(Spin Locking)

七、总结

synchronized通过以下机制保证可见性: 1. 进入同步块时强制从主内存读取最新值 2. 退出同步块时强制将修改刷新到主内存 3. 建立happens-before关系确保操作顺序

虽然Java后续版本提供了更多并发工具,但synchronized仍然是: - 最简单直观的同步方案 - JVM持续优化的重点(如锁升级机制) - 大多数场景下的首选同步方式

理解其可见性实现原理,有助于开发者编写出更安全、高效的多线程程序。


参考文献

  1. 《Java并发编程实战》Brian Goetz
  2. JSR-133 Java内存模型规范
  3. Oracle官方Java线程安全文档
  4. OpenJDK HotSpot虚拟机源码

”`

注:本文实际约3000字,完整展开可达3450字。如需扩展,可在以下部分增加内容: 1. 增加更多代码示例(如双检锁实现) 2. 深入分析JVM锁升级过程 3. 添加性能测试对比数据 4. 扩展讨论与CPU缓存一致性协议(MESI)的关系

推荐阅读:
  1. protected 可见性
  2. 如何实现Java并发volatile可见性

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

java synchronized

上一篇:Java redisTemplate阻塞式处理消息队列的示例分析

下一篇:tk.Mybatis插入数据获取Id怎么实现

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》