阅读 1797

volatile变量与普通变量的区别

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

一、内存模型

可以将内存简单分为两种:工作内存和主内存。所有的数据最终都需要存储在主内存,工作内存是线程独有的,线程之间无任何干扰。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之后(如下图所示);

假设线程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。

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

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

  • 运算结果并不依赖变量的当前值(即结果对产生中间结果不依赖),或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其它的状态变量共同参与不变约束(我认为此条多此一举,这个其它变量也必须得是线程安全的才行)

那适合哪些场景呢?这个我就不一一举例了,一个哥门总结得很好,参考如下:www.ibm.com/developerwo…

关注下面的标签,发现更多相似文章
评论