玩转java并发-volatile

本文深入解析Java中的volatile关键字,探讨其轻量级同步机制,对比synchronized的重量级锁,阐述volatile的原子性、可见性和有序性特点。通过实例演示volatile在多线程环境下的工作原理,包括内存屏障、happens-before原则及其对Java内存模型的影响。

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

volatile概述

volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,系统开销比较小。如果一个变量用volatile修饰了,则所有线程看到这个变量的值是一致的,如果某个线程对这个变量进行了更新,则其他的线程可以立马看到这个更新,这就是线程可见性。
在其他的情况下,系统通常会对我们的对象进行缓存,当线程想要获取某个变量时,会先向缓存查找。如果某个变量已经更新了,但是在缓存中仍然是之前未改变的值,这样其他的线程访问这个变量的值时,得到的就不是最新的数据了。引入volatile,告知系统,这个值一旦被更新,相对应在缓存中的数据也更新,而不是等待某个时间节点再去更新。通过volatile修饰的数据可以实现线程安全。

volatile的特性

原子性

先解释下原子性是什么。原子性其实就是一个操作或多个操作,要么全部执行且执行的时候不被打断,要么就不执行。这个在数据库中的事务中有很好的体现,事务中如果某一步执行失败,则进行回滚,将已经执行过的操作撤回。
看看下面这个栗子中哪些是原子性的

i=0;//1
j=i;//2
i++; //3
i=j+1; //4

在java中,对基本数据类型的变量定义和赋值是原子操作。因此 1是原子性的;2其实经历了两步,读取i,将i赋值给j;3经历了三步操作,读取i,i+1,将i+1赋给i;4同3。
在单线程中,我们可以认为上面的操作都是原子性的,但是在多线程下,就只有1符合原子性。volatile无法保证复合操作的原子性,只有锁和synchronized可以保证复合操作的原子性。
举个栗子:

public class VolatileTest {
	private static volatile int initvalue=1;
	private final static int MAX=10; 
	
	public static void main(String[] args) {
		Thread reader = new Thread() {
			@Override
			public void run() {
				while(initvalue<=MAX) {
						System.out.println(Thread.currentThread().getName()+" :"+initvalue++);	
				}
			}
		};
		
		Thread updater = new Thread() {
			@Override
			public void run() {
				while(initvalue<=MAX) {
						System.out.println(Thread.currentThread().getName()+" :"+initvalue++);
				}
			}
		};
		reader.start();
		updater.start();
	}
}

打印结果

Thread-0 :1
Thread-1 :2
Thread-0 :3
Thread-1 :4
Thread-1 :6
Thread-0 :5
Thread-1 :7
Thread-0 :8
Thread-1 :9
Thread-0 :10

我们可以发现并不是按1-10的顺序输出的。
initvalue++这已操作可分为三步:

  1. 因为initvalue是被volatile修饰的,所以从主存中读取initvalue->假设此时为4
  2. initvalue=4+1;
  3. 将initvalue读入主存
    因为volatile不满足原子性,线程可能执行到一半就失去了控制权,可能当线程A正在执行initvalue=4+1,还没有读入主存时,线程B又从主存读取了initvalue,这样就会发生不符合逻辑的错误。
可见性

volatile可以实现线程可见性

有序性

在java内存模型中,为了效率,允许编译器和处理器对执行进行重排序。重排序不会影响单线程的执行结果,但是多线程会受影响。volatile可以保证有序性。volatile可以禁止指令重排序若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
volatile禁止重排序规则:

  1. 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
  3. .当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序

volatile的内存语义及其实现

在JVM中,java之间的通信时通过共享内存实现的。volatile的内存语义是:

  1. 当写一个volatile变量时,会将该线程对于的本地内存中的共享变量立即刷新到主内存中
  2. 当读一个volatile变量时,设置当前线程下的本地内存中的共享变量无效,直接从主内存中读取变量

volatile的底层实现是通过插入内存屏障的:

  1. 在每一个volatile写操作前面插入一个StoreStore屏障
  2. 在每一个volatile写操作后面插入一个StoreLoad屏障
  3. 在每一个volatile读操作后面插入一个LoadLoad屏障
  4. 在每一个volatile读操作后面插入一个LoadStore屏障

volatile与happens-before

看个栗子

public class Test{
	int i=0;
	volatile boolean flag=false;
public void write(){  //Thread A
	i=1;  //1
	flag=true; //2
}
public void read(){  //Thread B
	if(flag){ //3
	system.out.println("i:"+i); //4
		}
	}
}

回顾上面的重排序规则:

  1. 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
  3. .当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序
    在这里插入图片描述通过这个栗子结合volatile的重排序规则,我们将这个重排序规则讲得更简单点:
  4. volatile读之后不能和普通写和普通读重排序
  5. volatile写之前不允许和普通写重排序,volatile写之后不允许和普通写/读重排序。

依据happens-before原则

  1. 按照happens-before程序顺序原则:1 happens before 2;3 happens before 4
  2. 按照happens-beforevolatile原则:2 happens-before 3
  3. 按照传递性原则:1 happends before 4
    因为 1 happens before 4 那么1对4是可见的。因此ThreadB执行的read()打印出的值就是ThreadA中的值

Java内存模型以及CPU缓存不一致

我们大家都知道,CPU的执行速度比内存的读取速度快,因此引入Cache缓存来改善这一问题。在这里插入图片描述将一部分数据从主存写入Cache。CPU先从Cache中读取数据,如果读取不到再从主存中读取,然后将数据放入Cache,这就是JMM的大致模型。但是由于引入Cache,从而引入了另一个问题。在多CPU多线程下,会将数据缓存到各自CPU对应的位置,也就是各自的虚拟机栈,读数据时也从自己的虚拟机栈读取,这样导致不同线程间数据不同步。再严谨一点说,如果JVM发现我们的某个线程操作只有读,没有任何写操作,JVM就会使这个线程一直读取缓存的数据,而不更新主存到缓存的数据;如果线程中有写的操作,就会发生缓存数据的更新。
看个栗子:

public class VolatileTest {
	private static int initvalue=0;
	private final static int MAX=5; 
	
	public static void main(String[] args) {
		Thread reader = new Thread() {
			@Override
			public void run() {
				int localvalue=initvalue;
				while(initvalue<MAX) {
					if(localvalue!=initvalue) {
						System.out.println(Thread.currentThread().getName()+" :"+initvalue);
						localvalue=initvalue;
					}
				}
			}
		};
		
		Thread updater = new Thread() {
			@Override
			public void run() {
				while(initvalue<MAX) {
					try {
						initvalue++;
						System.out.println(Thread.currentThread().getName()+" :"+initvalue);
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					
				}
			}
		};
		reader.start();
		updater.start();
	}

打印结果:
Thread-1 :1
Thread-1 :2
Thread-1 :3
Thread-1 :4
Thread-1 :5

我们可以发现在线程0中initvalue的值一直恒等于localvalue。由此证明:在线程只有读操作的情况下,JVM不会更新主存数据到缓存上。

解决Cache缓存一致性方案,
  1. 给数据总线枷锁-串行读
  2. CPU高速缓存一致性协议
    一致性协议的核心思想(volatile):当CPU写入数据时,如果发现该变量被共享(其他CPU也存在该变量的副本),会发出一个信号,通知其他CPU该变量的缓存无效了,其他CPU要读取该变量就只能去主存读取了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值