目录
对于内存模型的三大特性:有序性、原子性、可见性。
大家都知道volatile能保证可见性和有序性但是不能保证原子性,但是为什么呢?
一、原子性、有序性、可见性
1、原子性:
(1)原子的意思代表着——“不可分”;
(2)在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。例如 a=1
是原子性操作,但是a++
和a +=1
就不是原子性操作。
2、可见性
线程执行结果在内存中对其它线程的可见性。
变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,加了这个指令后,会引发两件事情:
发生修改后强制将当前处理器缓存行的数据写回到系统内存。
这个写回内存的操作会使得在其他处理器缓存了该内存地址无效,重新从内存中读取。
3、有序性
在本线程内观察,所有操作都是有序的(即指令重排不会导致单线程程序执行结果与排序前有任何差别)。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
二、线程安全的两个问题,执行控制和内存可见
执行控制(synchronize):控制代码只能顺序执行(执行一次只能被一个线程执行)或者可以多线程并发执行。
内存可见控制(volatile):线程执行结果在内存中对其它线程的可见性。线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。
volatile和synchronize两个关键字就是上述两种作用。
synchronize关键字使得同一时刻只有一个线程可以获得当前变量、方法、类的锁,其他线程无法访问,也就无法同步并发执行,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作,保障有序性、可见性、原子性;
volatile通过强制将当前线程修改后的值写回内存并使得其他线程中该值无效的方式保证其可见性,通过禁止指令重排的方式保证有序性,具体为何不能保证原子性在下一部分讨论。
三、为什么volatile不能保证原子性
对于i=1
这个赋值操作,由于其本身是原子操作,因此在多线程程序中不会出现不一致问题,但是对于i++
这种复合操作,即使使用volatile关键字修饰也不能保证操作的原子性,可能会引发数据不一致问题。
private volatile int i = 0;
i++;
如果启了500条线程并发地去执行i++
这个操作 最后的结果i是小于500的.
解释:
原子性意味着一个操作是不可中断的,即使在多线程环境下,某个操作一旦开始,就会一直执行到结束,中间不会被其他线程的操作打断。volatile 确保的是变量的可见性,但对于复合操作(如 count++)并不能保证原子性。
示例:
count++ 不是原子操作,count++ 实际上由三步组成:
- 读取变量的当前值(从内存中读取 count 到寄存器或缓存中)。
- 将值加一(在寄存器或缓存中对 count 的值进行加法运算)。
- 将结果写回变量(将加一后的值存回内存)。
由于这些步骤是分开执行的,在多线程环境下,线程之间可能发生交错执行,导致竞态条件。例如,一个线程读取了 count 的值为 5,另一个线程在此时也读取了同样的值,然后两个线程都对 count 进行加一操作,最后两个线程分别将结果 6 写回内存,导致 count 的最终值是 6 而不是预期的 7。
假设某一时刻i=5
,此时有两个线程同时从主存中读取了i
的值,那么此时两个线程保存的i的值都是5, 此时A线程对i进行了自增计算,然后B也对i进行自增计算,此时两条线程最后刷新回主存的i的值都是6(本来两条线程计算完应当是7)所以说volatile保证不了原子性。
四、volatile和synchronized的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
五、如果要保证变量的原子性该怎么做
在 Java 中,如果希望保证变量的原子性(即在多线程环境下对变量的操作是不可分割的)
1. 使用 synchronized
关键字
通过 synchronized
关键字来锁定对某个对象的访问,确保同一时刻只有一个线程能够执行被 synchronized
修饰的代码块。虽然这不是直接保证变量的原子性,但它可以确保对变量的读写操作是安全的。
public class AtomicExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
2. 使用 java.util.concurrent.atomic
包中的原子类
java.util.concurrent.atomic
包提供了一些类,用于在多线程环境下以原子方式更新变量。这些类提供了原子操作的方法,避免了显式同步的需要。
AtomicInteger
:用于整型的原子操作。AtomicLong
:用于长整型的原子操作。AtomicBoolean
:用于布尔值的原子操作。AtomicReference
:用于引用类型的原子操作。
例如,使用 AtomicInteger
实现一个线程安全的计数器:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子地增加1
}
public int getCount() {
return count.get(); // 原子地获取当前值
}
}
3. 使用 java.util.concurrent.locks
包中的 ReentrantLock
ReentrantLock
提供了比 synchronized
更细粒度的锁定控制,可以显式地锁定和解锁,以保证变量的原子性。
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
- 对于简单的整数操作,
AtomicInteger
和其他java.util.concurrent.atomic
包中的类通常是最简单和最有效的方法。 - 对于复杂的同步场景,
synchronized
关键字和ReentrantLock
提供了更灵活的控制。
参考文献: