volatile变量与普通变量的区别

来源:互联网 发布:淘宝签署开店无法同意 编辑:程序博客网 时间:2024/05/16 07:09
Java团长 2017-10-06 22:46

我们通常会用volatile实现一些需要线程安全的代码(也有很多人不敢用,因为不了解),但事实上volatile本身并不是线程安全的,相对于synchoronized,它有更多的使用局限性,只能限制在某些特定的场景。本篇文章的目的就是让大家对 volatile 在本质上有个把握,为了达到这个目的,我们会从java 的内存模型及变量操作的内存管理来说明(不用怕,你会发现很简单)。

一、内存模型

volatile变量与普通变量的区别

可以将内存简单分为两种:工作内存和主内存。所有的数据最终都需要存储在主内存,工作内存是线程独有的,线程之间无任何干扰。java的内存模型主要就是定义工作内存和主内存的交互,即工作内存如何从主内存拷贝数据,以入如何写数据。java 定义了8种原子性操作来完成工作内存与主内存的交互:

  • lock 将对象变成线程独占的状态

  • unlock 将线程独占状态的对象的锁释放出来

  • read 从主内存读数据

  • load 将从主内存读取的数据写入工作内存

  • use 工作内存使用对象

  • assign 对工作内存中的对象进行赋值

  • store 将工作内存中的对象传送到主内存当中

  • write 将对象写入主内存当中,并覆盖旧值

这些操作也是有一定的条件限制的:

read 和load,store和write 必须成对出现,即从主内存中读取的数据数据在工作内存必须接受;传递到主内存的数据,也不可以被拒绝写入。

assign后的对象必须回写到缓存

未进行新赋值的对象不允许回写到主内存

新的变量只能在主内存产生,且未完成初始化的对象不允许在工作内存中使用

对象只允许被一条线程锁定,且可以被此线程多次锁定

未被锁定的对象不允许执行unlock操作

对一个对象执行unlock之前,必须将对象回写到主内存

java的8种原子性操作,相互之前有一定的约束条件,但并没有严格限制任意两个操作必须连续出现,只是表示成对出现,这也是为什么会产生线程不安全性的原因。

介绍了上述的背景知识,那我们就来看一下volatile变量到底和普通变量有啥差别吧

二、volatile变量与普通变量

2.1 volatile 的安全性

下面我们用一个例子来说明volatile变量与普通变量的区别。

假设有两个线程操作一个主内存的对象,且线程1早于线程2开始(如下例如示一个a++操作))

public class ThreadSafeTest { public static int a = 0; public static void increase() { a++; } public static void main (String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 100; j++) { increase(); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 100; j++) { increase(); } } }); t1.start(); t2.start(); }}

线程2读取主内存对象(a)时,可能发生在几个时期:read之前、read之后、load之后、use之后、assign之后、 store之后、write之后(如下图所示);

volatile变量与普通变量的区别

假设线程1执行了a++,a从0变成了1,还未来得及写回主内存对象,线程2从主内存对象中读取的数据a=0;此时线程1写入主内存a=1,而线程2仍然执行完了a++ ,此时仍然 等于1(应该等于2),实际上,这就上相当于线程2 读入了一个过期的数据,导致线程不安全。

那如果将a变成volatile对象是否就正确了呢?

volatile对对象的操作做了更严格的限制:

  • use之前不进行read和load

  • assign之后必须紧跟store和write

    实际相当于将read load use 三个原子操作变成一个原子操作;将assign-store-write变成一个原子操作。很多文章上都讲volatile对所有的线程是可见的,指的就是执行完了assign之后立即就会回写主内存;在任意一个线程读取主内存对象时,都会刷新主内存。在主内存中表现是数据一致性的,但是各线程内存当中却不一定是一致性的。

    同样是上面的代码,换成volatile

  • public class ThreadSafeTest {
  • public static volatile int a = 0;
  • public static void increase() { a++;}
  • public static void main (String[] args) { 
  • Thread t1 = new Thread(new Runnable() { 
  • @Override 
  • public void run() { for (int j = 0; j < 100; j++) { increase(); } } }); 
  • Thread t2 = new Thread(new Runnable() { 
  • @Override 
  • public void run() { for (int j = 0; j < 100; j++) { increase(); } } }); 
  • t1.start(); 
  • t2.start();}}

运行后发现,也拿不到正确的结果(如果拿到请把j的数值调大)。操你妈,不是说是线程安全的变量吗?为啥也不正确?

这是因为线程内部的数据仍然有可能存在不一致性,比如,如果线程2读取数据时,处在线程1use之后,但线程1此时还未来得及回写主缓存,这时候线程2使用到的数据仍然是0,两个线程同时对0++,得到的结果只会是1,而不是理想中的2。

volatile变量与普通变量的区别

2.2 volatile 的线程安全是有条件的

即然volatile 是非线程安全的,那要它还有什么用呢?如果你看过我写过的“线程安全”的文章应该知道,所有的对象都是相对线程安全的,也就是有条件的。volatile的线程安全当然也是有条件的,它是对synchronized这一重量级线程同步的一种补充,其整体性能上优于synchronized。那volatile的线程安全的条件是什么呢?适合使用在哪些场景?

《java虚拟机》给出两个条件:

  • 运算结果并不依赖变量的当前值(即结果对产生中间结果不依赖),或者能够确保只有单一的线程修改变量的值

  • 变量不需要与其它的状态变量共同参与不变约束(我认为此条多此一举,这个其它变量也必须得是线程安全的才行)

那适合哪些场景呢?这个我就不一一举例了

volatile变量与普通变量的区别

原文作者

阅读全文
0 0
原创粉丝点击