volatile: 用来声明变量的关键字之一,它的主要作用是确保多个线程能够正确地处理共享变量。在多线程编程中,如果一个变量被多个线程共享并且这些线程可能同时修改该变量的值,那么就需要使用 volatile 关键字来保证线程之间对该变量的操作是可见的、有序的。
特点:
1、可见性(Visibility)
当一个变量被 volatile 关键字修饰时,如果一个线程修改了这个变量的值,那么其他线程将立即看到这个修改。这是因为 volatile 会告诉 JVM 不要将该变量缓存在线程的工作内存中,而是直接从主内存中读取和写入变量的值。
1.JMM(Java Memory Model)
一种规范,用于定义 Java 程序中多线程并发访问共享内存时的行为。JMM 主要关注的是多线程之间如何访问共享变量以及如何同步它们的访问。
线程工作内存是拷贝系统主内存的数据,然后对于数据操作,写入线程。当一方改变主内存数据时,另一方不可见。
八种指令操作
lock(锁定): 作用于主内存的变量,它把一个变量标识为一条线程独占的状态;
unlock(解锁): 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
read(读取): 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
load(载入): 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
use(使用): 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作;
assign(赋值): 作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
store(存储): 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用;
write(写入): 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
2.代码展示
//当我们不加volatile,程序就不会结束,主线程修改主内存数据,不会通知线程a
private static volatile int a = 0;
public static void main(String[] args) {
new Thread(()->{
while (a == 0){
}
},"a").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
a = 1;
System.out.println("q =>" + a);
}
2.不能保证原子性
原子性是指一个操作是不可分割的,要么全部执行成功,要么全部不执行,不存在中间状态。在并发编程中,原子性是保证多线程并发操作共享变量时的一个重要特性。
1.代码示例
//volatile不保证原子性
private volatile static int a = 0;
private static Lock lock = new ReentrantLock();
//synchronized lock 可以保证原子性
public static void add() {
lock.lock();
try {
a++;
}catch (Exception e){
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j=0;j<1000;j++){
add();
}
}).start();
}
//上面线程执行完执行主线程
while (Thread.activeCount() > 2){
Thread.yield();
}
//输出结果期待值是2W
System.out.println("a=>" + a);
}
2.并发编程保证原子性操作
1.synchronized 关键字: 使用 synchronized 关键字可以保证某个代码块或方法在同一时刻只能被一个线程执行,从而确保了对共享资源的原子性访问。
public static synchronized void add() {
a++;
}
2.Lock 接口: 使用 Lock 接口及其实现类(如 ReentrantLock)也可以实现对共享资源的原子性访问。
public static void add() {
lock.lock();
try {
a++;
}catch (Exception e){
}finally {
lock.unlock();
}
}
3.原子类(Atomic Classes): Java 并发包中提供了一系列原子类,如 AtomicInteger、AtomicLong 等,它们提供了一种线程安全的方式来更新共享变量的值,保证了原子性操作。
private static AtomicInteger a = new AtomicInteger();
public static void add(){
a.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j=0;j<1000;j++){
add();
}
}).start();
}
//上面线程执行完执行主线程
while (Thread.activeCount() > 2){
Thread.yield();
}
//输出结果期待值是2W
System.out.println("a=>" + a);
}
3.禁止指令重排序(Preventing Instruction Reordering)
关键字还可以禁止 JVM 对指令进行重排序优化,保证指令执行顺序与程序中的代码顺序一致。这样可以避免由于指令重排序导致的线程安全问题。
1.编译器优化重排序: 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2.指令级并行的重排序: 现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3.内存系统的重排序: 处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行(强调的是内存缓存)。