当前位置:   article > 正文

Java多线程volatile关键字

Java多线程volatile关键字

一、volatile介绍

1、什么是volatile

volatile是Java提供的一种轻量级的同步机制。Java语言包含两种同步机制:1、同步块(或同步方法) 2、volatile变量。相比于synchronized加锁同步,volatile关键字比较轻量级,开销更低,因为他不会引起线程上下文的切换调度。

2、volatile定义

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

3、volatile关键字的作用

volatile关键字保证了共享变量在多个线程中的可见性。当一个线程修改了共享变量的值时,其他的线程能够看见这个值的改变,并且将重新读取该值。

二、多线程中如何操作共享变量

在这里插入图片描述
说明: 共享变量一般是存放在主存中,在多线程中每一个要使用到共享变量的线程都需要将主存中的共享变量拷贝一份放入到自己的工作内存中(线程私有,高速缓存)。每一个线程中都是读取的自己工作内存里共享变量的副本值来进行相关操作,而不会每次都会去访问主存取值(CPU直接访问主存效率低下)。
问题: 如果现在有一个线程(线程A)修改了自己的值。但是别的线程都不知道该值已经改变了,所以还是继续延用他们各自从主存中拷贝过来的共享变量值的副本。这样就会造成一些多线程相关的问题。

三、代码演示

1、示例代码
public class TestVolatitle1 {

	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		Thread threadA = new Thread(td);
		threadA.start();
		
		while (true) {
			if (td.isFlag()) {
				System.out.println("------------------");
				break;
			}
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
class ThreadDemo implements Runnable {

	private boolean flag = false;
	@Override
	public void run() {
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {}
		flag = true;
		System.out.println("flag=" + isFlag());
	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

代码解释:
1、在以上代码中,一共有两个线程,一个是我们手动开启的线程threadA,一个是主线程。ThreadDemo td = new ThreadDemo(); 是创建一个共享变量td。
2、在线程threadA中会执行 run() 方法,先休眠200毫秒,然后将flag的值改变为true。
3、在主线程中while循环会一直不停的判断共享变量的flag值是否为true,若为true则结束程序。
运行结果:
在这里插入图片描述
点击运行该程序以后发现,运行结果并不是我们想象的那样。主线程中的while循环一直不会结束。
问题产生原因:
首先线程threadA和主线程都去主存中拷贝一份共享变量的值,放入到自己的工作内存中。线程threadA将自己工作内存中的flag值改变成了true。但是主线程工作内存中的值并没有改变(依然是false),所以while循环就会一直循环不会结束。
问题解决:
当我们给共享变量flag加上volatile关键字修饰以后,程序正常运行并结束了。

四、volatile关键字详解

1、详解

从以上的代码示例中,我们看见给共享变量添加了volatile关键以后程序就正常运行结束了。那么volatile关键字到底是如何工作的呢?通过上面的代码示例来说明一下加入了volatile关键字以后的程序流程。
在这里插入图片描述
1、给共享变量添加了volatile关键字修饰后。首先,线程threadA和主线程都会去主存中拷贝一份共享变量的值。
2、然后在线程A中将flag的值改变了,同时主线程里面的while循环也一直在执行。线程A将共享变量的flag值改变为true了。然后线程A就将flag改变后的值刷写到主存中。
3、当主存中共享变量的值更改以后,会导致其他线程中的共享变量副本失效,这时候其他线程需要重新从主存中读取一次共享变量的值到线程的工作内存中。
4、当主线程重新从主存读取共享变量的值以后,读取到的flag的值为true,此时while循环就结束了。

2、volatile底层实现

volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情呢?

当有volatile变量修饰时会出现汇编指令lock addl $0×0,(%esp),Lock前缀的指令在多核处理器下会引发了两件事情

1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

3、指令重排序

重排序分为编译重排序和运行重排序两种。下面主要记录编译重排序!

编译期重排: 编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以之更适合于CPU的并行执行。

运行期重排: CPU在执行过程中,动态分析依赖部件的效能,对指令做重排序优化

3.1、什么是指令重排序

JVM为了尽可能的使CPU得到最大的利用率,Java虚拟机在不影响单线程程序执行结果的前提下,尽可能的提高并行度,所以JVM会按照自己的一些规则将程序编写的顺序打乱,比如:写在后面的代码在时间顺序上可能先执行,写在前面的代码在时间顺序上可能会后执行。

举例说明:

double r = 2.1; //(1) 
double pi = 3.14;//(2) 
double area = pi*r*r;//(3)
  • 1
  • 2
  • 3

在我们程序中编写了如下代码,代码中定义的执行顺序为(1)—>(2)—>(3),在单线程环境中顺序(1)—>(2)—>(3)和顺序(2)—>(1)—>(3)对执行结果并无影响。(1)和(2)中并不存在着 **数据依赖关系。**所以JVM可能会对(1)、(2)两步打乱顺序执行来优化程序效率。

所以,编译时指令重排序可以理解为JVM为了提高程序效率而可能会将不存在数据依赖关系的代码进行执行顺序的调整的过程。

3.2、什么情况可能会造成指令重排序呢

1、一个不是原子的操作,可能会给JVM留下重排序的可能。

2、不存在数据依赖关系的代码语句可能会给JVM留下重排序的可能。

3.3、指令重排序带来的影响

指令重排序在单个线程中不会引发问题,但是到了多线程中由于指令重排序的原因可能就会引发一系列的异常。 在Java内存模型(JMM)中有这么一句话很好的描述了重排序的影响:如果在本线程内观察,所有操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的。

有序性是指对于单线程的执行代码,执行是按顺序依次进行的。但在多线程环境中,则可能出现乱序现象,因为在编译过程会出现“指令重排”,重排后的指令与原指令的顺序未必一致。因此,前半句指的是线程内保证串行语义执行,后半句则指指“令重排现”象和“工作内存与主内存同步延迟”现象。

一个经典的重排序案例:

public class SingleTon {

	private static SingleTon singleTon;

	private SingleTon() {}

	public SingleTon getInstance() {

		if (singleTon == null) {
			synchronized (SingleTon.class) {
				if (singleTon == null) {
					//非原子操作
					singleTon = new SingleTon();
				}
			}
		}
		return singleTon;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

问题分析:

上面是一个双重检查的单例模式,由于使用了双重检查保证了整个上下文环境中只有一个 SingleTon对象,但是在多线程环境中,由于 singleTon = new SingleTon();不是一个原子操作,所以经过JVM的指令重排序以后仍然会引发问题。

singleTon = new SingleTon();语句分为三步:

(1)、在堆内存开辟一块空间

(2)、SingleTon对象初始化

(3)、栈中的 singleTon指向刚刚分配的内存地址

在上面三步中(2)依赖于(1),但是(3)不依赖于(2),所以JVM就可以将这三个步骤进行重排序。假如经过JVM一番重排序后,上面的顺序变成了(1)—>(3)—>(2)。此时有两个线程,线程A和线程B需要获取该单例对象, A线程抢到锁执行同步块中的语句,A线程正在执行(1)—>(3)—>(2)中的(3),此时B线程开始执行了,B线程在锁外面开始做第一次判断 if (singleTon == null),判断结果为false,然后直接返回singleTon进行使用,但是这时候singleTon还没有初始化,导致出错。

解决办法:
如果使用volatile关键字修饰singleTon变量,禁止JVM进行重排序,使得singleTon在读、写操作前后都会插入内存屏障,避免重排序。

3.4、小结
  1. 所以,当使用了volatile关键字修饰共享变量以后。每一个线程对共享变量做的更改操作都会被重新刷写到主存中,并且当主存中共享变量的值改变以后,其他线程中的共享变量副本就失效了,需重新从主存中读取值。
  2. 用volatile关键字的变量的值只要一经改变就会自动刷写到主存中,而不会等待改变该值的线程执行完毕再刷写。
  3. 当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile;

五、volatile的优缺点

  • 优点: volatile是轻量级同步机制。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,是一种比synchronized关键字更轻量级的同步机制。
  • 缺点:volatile只能保证变量的可见性,无法保证原子性。除此之外,由于volatile屏蔽掉了VM中必要的代码优化,所以在效率上会稍微低点。这是两个缺点。volatile禁止了JVM底层的指令重排序优化,造成了一定的性能损耗,但是在多线程环境中提高了安全性。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/266085?site
推荐阅读
相关标签
  

闽ICP备14008679号