JAVA并发包的Volatile和CAS如何不用锁保证线程安全?

本文详细探讨了Java并发编程中的volatile关键字和CAS(Compare And Swap)机制,阐述了Java内存模型(JMM)的概念,以及它们如何确保线程安全。文章通过实例解释了JMM的内存重排序问题,强调了volatile保证的可见性和有序性,但不保证原子性。此外,介绍了CAS如何在无锁情况下保证原子性,以实现线程安全。最后指出,volatile+CAS是Java并发包中实现线程安全的重要手段。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

从JDK1.5以后,引入了java.util.concurrent并发包,其中java.util.concurrent.atomic包,方便在无锁的情况下,进行原子操作。在JUC中大部分都是利用volatile关键字+CAS在不用锁的情况来保证线程安全的。本篇文章把这两个知识点给大家一个清晰的解析,只有掌握了关键字volatile和CAS机制,你才能对JUC包有一个彻底的理解。

 

 

 

Java的内存模型JMM

1.1、Java的内存模型(JMM)

要想彻底明白volatile到底是干什么的,你必须知道Java的内存模型(JMM)。网上有很多关于对JMM定义的描述,如果我在按照他们的列出来,那么这一篇文章就变了味道,所以我用自己理解的去阐述Java内存模型,不会用长篇大论去介绍概念,而是依据例子去阐述,我觉得更有意义。

我们知道,共享变量属于所有的线程共享的,为了提高性能,每一个线程都会保存一份共享变量的副本,就是说每一个线程都会从主存中复制一份共享变量到自己的工作内存中去。举例说明:

 

有一个全局变量count=0,线程1和线程2同时将count+1

上面是一个非常简单的例子,如果对JMM不熟悉的同学很容易脱口而出最终结果为2,但是在多线程下的环境下真的就是我们期望的结果吗?答案是不一定,可能就会出现不同的现象了。

第一个现象:线程1首先获取到CPU的执行权,

 

1:线程1首先获取CPU的执行权,所以从主存中获取count=0,然后复制一份到自己的工作内存中去。
2:线程1将工作内存中的count+1,此时工作内存count=1,还未来得及刷新到主存中,这时线程2获取了CPU的执行权
3:线程2获取CPU的执行权,所以也从主存中获取count=0,然后复制一份到自己的工作内存中去。
4:线程2将工作内存中的count+1,此时工作内存count=1。
5:是线程1首先刷新到主存中,还是线程2首先刷新到主存中,这个不确定。

上面线程1和线程2两个线程的工作内存的count都是1,但是它们什么时候刷新到主存中,无法确定,可能是线程1首先将count=1刷新到主存中,也可能是线程2首先将count=1刷新到主存中,不管哪一个线程首先将它的工作内存中count刷新到主存中,那此时主存也会count=1,这个结果与我们想象的不一样。

第二个现象:线程2首先获取到CPU的执行权,

 

1:线程2首先获取CPU的执行权,所以从主存中获取count=0,然后保存到自己的工作内存中
2:线程2的count副本+1,此时count=1,但是还未来得及刷新到主存中,线程1获取了CPU的执行权。
3:线程1获取CPU执行权后,会从主存中拷贝一份count=0,到自己的工作内存中去。
4:线程1的count副本+1,此时count=1.
5:是线程2首先刷新到主存中,还是线程1首先刷新到主存中,这个不确定

上面两种现象不管是线程1首先获取CPU执行权,还是线程2获取CPU执行权,最终的结果是一样的,那就是count=1。这个结果并不是我们要的结果,导致出现这个结果的原因就是并不知道工作内存中的值什么时间才会刷新到主存中去

第三种现象:线程1首先获取到CPU执行权,然后count+1,并刷新到主存中后线程2才获取CPU的执行权。

 

1:线程1首先获取CPU的执行权,从主存中复制一份count=0到自己的工作内存中去。
2:线程1将工作内存的count+1,此时count=1
3:在线程2获取CPU执行权之前,线程1就将自己工作内存count=1刷新到主存中去。
4:此时主存中的count=1
5:线程2获取CPU的执行权,从主存中复制一份count=1到自己的工作内存中去。
6:线程2将工作内存count+1,此时count=2
7:在适当的某个时候,线程2把count=2刷新到主存中去。

第四种线程:线程2首先获取到CPU的执行权,然后count+1,并刷新到主存中后线程1才获取到CPU的执行权。

 
<think>嗯,用户想知道Java中实现线程安全的核心要素方法。首先,我需要回忆一下线程安全的定义,根据引用[2],线程安全是指当多个线程访问某个类时,不管运行时环境如何调度,都不需要额外的同步,这个类都能表现正确行为。那核心要素应该包括原子性、可见性、有序性这些概念吧。比如原子性,操作要么全执行,要么不执行,不会被打断。可见性则涉及变量的修改对其他线程立即可见,而有序性是关于指令重排的问题。 接下来是方法,常见的synchronized关键字,引用[1]提到了用它来同步方法或代码块。然后是volatile关键字保证可见性禁止指令重排,但无法保证原子性。还有Lock接口,比如ReentrantLock,提供更灵活的机制。不可变对象,比如用final修饰,这样状态不可变,自然线程安全。ThreadLocal让每个线程有自己的变量副本。线程安全容器,比如ConcurrentHashMap,内部已经处理好同步。原子类如AtomicInteger,用CAS机制保证原子操作。 要注意synchronizedLock的区别,前者是隐式,后者需要显式加volatile适合一写多读的场景。不可变对象虽然安全,但可能影响灵活性。ThreadLocal适用于线程间数据隔离的情况。用户可能还需要知道如何选择合适的方案,比如高并发时用原子类,需要细粒度控制用Lock等。还要提醒死的问题,比如引用[4]提到的多线程编程需考虑线程安全。代码示例的话,可以举synchronized方法、volatile变量、ReentrantLock的使用例子。总结时强调根据具体场景选择方法,并考虑性能维护成本。</think>### Java 实现线程安全的核心要素及方法 #### 一、线程安全的核心要素 1. **原子性** 确保操作不可分割,要么全部执行成功,要么完全不执行。例如对共享变量的修改需要保证中间状态不会被其他线程观察到[^2]。 - 示例:`i++`是非原子操作(包含读取、修改、写入三步),需通过或原子类保证原子性。 2. **可见性** 一个线程对共享变量的修改对其他线程立即可见。多线程环境下,由于缓存机制可能导致数据不一致[^4]。 - 示例:使用`volatile`关键字强制读写直接作用于主内存。 3. **有序性** 禁止指令重排序优化,保证代码执行顺序符合预期。编译器或处理器可能调整指令顺序,导致多线程逻辑错误。 - 示例:`synchronized`或`volatile`通过内存屏障限制重排序。 --- #### 二、实现线程安全的7种核心方法 1. **使用`synchronized`关键字** - **作用**:通过机制保证代码块或方法的原子性、可见性有序性。 - 代码示例: ```java public class Counter { private int count = 0; public synchronized void increment() { // 同步方法 count++; } } ``` - 适用场景:简单同步逻辑,如单例模式、资源竞争控制[^1]。 2. **`volatile`关键字** - **作用**:确保变量修改的可见性,禁止指令重排序。 - 代码示例: ```java public class VolatileExample { private volatile boolean flag = false; public void toggle() { flag = !flag; // 非原子操作,需结合其他机制保证原子性 } } ``` - 适用场景:状态标记(如开关控制)、单例模式的双重检查。 3. **显式(`Lock`接口)** - **作用**:提供更灵活的控制(如超时、可中断)。 - 代码示例: ```java public class LockExample { private final ReentrantLock lock = new ReentrantLock(); private int value = 0; public void add() { lock.lock(); try { value++; } finally { lock.unlock(); } } } ``` - 适用场景:需要细粒度控制的复杂同步逻辑。 4. **不可变对象(Immutable Objects)** - **作用**:对象状态在创建后不可修改,天然线程安全。 - 实现方式: ```java public final class ImmutablePoint { private final int x; private final int y; public ImmutablePoint(int x, int y) { this.x = x; this.y = y; } // 无setter方法 } ``` - 适用场景:配置类、数据传输对象。 5. **线程封闭(`ThreadLocal`)** - **作用**:为每个线程创建独立的变量副本,避免共享。 - 代码示例: ```java public class ThreadLocalExample { private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); } ``` - 适用场景:数据库连接、日期格式化工具[^4]。 6. **线程安全容器** - **作用**:直接使用并发包(`java.util.concurrent`)中的线程安全集合。 - 示例: ```java ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); ``` - 适用场景:高并发读写的集合操作。 7. **原子类(`Atomic`系列)** - **作用**:基于CAS(Compare-And-Swap)实现无线程安全操作。 - 代码示例: ```java public class AtomicExample { private AtomicInteger counter = new AtomicInteger(0); public void increment() { counter.incrementAndGet(); // 原子递增 } } ``` - 适用场景:计数器、状态标记等高频次原子操作。 --- #### 三、方法对比与选择 | 方法 | 性能 | 灵活性 | 适用场景 | |--------------------|------|--------|------------------------------| | `synchronized` | 中 | 低 | 简单同步逻辑 | | `volatile` | 高 | 低 | 状态标记 | | 显式(`Lock`) | 中 | 高 | 复杂控制(如超时、公平性) | | 不可变对象 | 高 | 中 | 只读数据 | | 原子类 | 高 | 低 | 高频次原子操作 | --- #### 四、注意事项 1. **避免死**:确保的获取释放顺序一致,必要时使用`tryLock`设置超时[^3]。 2. **性能权衡**:过度使用会降低并发性能,优先考虑无设计(如CAS)。 3. **上下文选择**:根据场景选择最简方案(如`ConcurrentHashMap`优于手动同步`HashMap`)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值