Java 带你学习volatile关键字(线程原子性,可见性,有序性)

先不说这个关键字,先问你们一个问题,你们知道线程:

可见性

原子性

有序性   

的这三个特行吗?

不理解,就继续看下文呗

Java 每一个线程在进行工作时,都有一个自己独有的工作内存,这个工作内存是线程独自访问的,其他线程访问不了,

每个线程进行读写数据时,都是先把主内存的数据copy一份数据副本到自己的工作内存,然后当这个线程需要读写修改这些数据时,会先在自己的工作内存读写,然后再把数据刷新到主内存去。但什么时候刷新到主内存呢?nobody can tell you......

因为涉及到的因素太多了

==============【可见性】==============

这个时候就有一个现象,比如:

主内存有一个属性isExit,

线程1读取了copy到自己工作内存,

线程2也copy变量isExit的数据副本到自己工作内存并改变了变量isExit的值。

比如下面代码:

    private boolean isExit = false;

    public void testVolati(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isExit) {
                    //do something
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                isExit = true;
                //do something
            }
        }, "线程2").start();
    }

如果这个 isExit 不需要实时性响应这个倒是没问题。如果需要一修改他的值,其他线程必须立马得知并必须刷新最新的值,那就有问题。

what???? 为什么呢?

因为线程2  虽然是修改了 isExit的值,但是线程2何时刷新到主内存,是不确定的。

如果线程1 在线程2修改值之前, 就已经 isExit (即:isExit = false ) 复制到自己的工作内存里,线程1不会再去主内存

读取这个值。所以线程1里面的值一直都是 isExit = false。所以线程1的执行功能一直在跑。虽然线程2已经改了这个isExit的值了。

这就是线程的 不可见性 ,一个线程修改了值,其他线程不一定立马得知

为了解决这个问题,Java提供了 volatile  这个一个关键字,只要被 volatile 修饰的属性,一旦被修改,所有线程都立马得知并会到主内存重新读取,进而刷新到自己的工作内存里去。比如修改一下上面的代码,对 isExit 用  volatile 

    private volatile boolean isExit = false;

    public void testVolati(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isExit) {
                    //do something
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                isExit = true;
                //do something
            }
        }, "线程2").start();
    }

这个通过 volatile  修饰了isExit了,一旦线程2改了 isExit的值,线程1会立马得知并从主内存重新读取并刷新到自己工作内存的isExit。所以,通过  volatile    修饰的变量,具备   线程的可见性

==============【原子性】==============

嗯,我们现在再看看原子性这个是啥东东,我们先暂且不说这个,上个代码看看先

    private volatile int num = 0;//累加的变量
    private int count = 0;//完成线程累加
    private int THREAD_COUNT = 20;//线程数目

    public void textVo() {
        //开启20个线程
        for (int x = 0; x < THREAD_COUNT; x++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        num++;
                    }
                    threadOK();
                }
            });
            thread.start();
        }

    }

    private void threadOK() {
        count++;
        //等待所有线程都执行完累加
        if (count == THREAD_COUNT) {
            Log.d(TAG, "num : " + num);
        }
    }

看代码,很简单,开启20个线程,每个线程对 变量num进行累加 10000,按道理20*10000,那结果应该是:200000

好,给你看下输出结果:

大概率的小于 我们期待的数目,what????? you kidding me ????

为什么会这样子的呢?来,我给你分析:

其实问题出现在 num++ 这一行代码里。我们都知道,Java语言最终还是编译成汇编语言,然后再通过汇编指令操作的。

num++  这一行代码也不例外,最终还是会编译成N条汇编指令去执行,大概反编译出来的汇编指令:

		Code:
		Stack=2,Locals=0,Args_size=0,
		0:	getstatic  #98;//
		3:	iconsts_1
		4:  iadd
		5:  putstatic  #98
		8:  return

整个过程在汇编指令里大概就是:

1:先把数据读取出来,把这个值放在操作栈的顶部  (第0行)

2:对数据累加                                                              (第4行)

3:把累加的数据返回去                                            (第8行)

那么这个时候就会出问题了,

比如:

1:我线程1 会把 num等于100的值读取出来,然后再把这个值放在操作栈的栈顶,这个时候CPU执行权被其他线程抢走

2:线程2 这个时候抢到CPU的执行权,然后 会把 num等于100的值读取出来,进行累加后把101返回去给num,此时num = 101

3:然后这个时候CPU执行权被线程1抢回去,注意,线程1操作栈栈顶的值还是100,然后对值类加后得到101,把值返回num

4:然后问题就很清晰,线程1和线程2都累加了1次,最终值只是加了1。

but,,,,你们可能会看到,num 使用了 volatile    修饰得,线2修改了之后,为什么线程1不会立马得知,再次去主内存读取新的数据呢?不是说具备  线程可见性吗?

你们有这个疑问是好的,但是很残忍的告诉你,汇编指令把值读取放在了操作栈的栈顶,你在其他线程对num这个变量怎么操作法,都和它没关系了。后面的累加操作指令都是原来栈顶的数据。

所以,这就是线程  的  原子性 

原子性,就是说虚拟机在执行一个Java指令时,是否能保证 一个线程   必须执行完  一个Java 指令  后才能被其他线程 抢走CPU执行权。注意,是Java指令 

比如简单的 num++ 这么一行 java 代码,

很明显,volatile   关键字修饰的变量是不具备的,也就是说num变量不具备线程的原子性。

因为 它连一个 num++,这么一个简单自增的指令都不保证执行完才被其他线程强周CPU执行权。

其实,换一个角度说,可以认为,线程的运算功能,是无法保证原子性的。

==============【有序性】==============

好了,最后说一下有序性。

说下Java虚拟机的执行指令的行为,上个Demo说一下

    int a = 0;//第1行
    int b = 1;//第2行
    int c = 2;//第3行
    private void test() {
        a++;      //第4行
        b++;      //第5行
        c = a + b;//第6行
    }

就这么一个简单的几行代码。虚拟机是不保证

第4行 肯定会在 第5行 前面执行。虚拟机在编译后,会对Java重新优化排序。

只保证:执行到第6六行代码时,第4行和第5行必须已经执行完了。

也就是说,只保证最终的结果输出是正确的。不保证执行顺序和你写的是一致的。

好了,我看下不加 volatile 修饰的DoubleLock 单例模式会出啥问题

来,上个 不加 volatile 的单例模式,标准 DoubleLock 单例模式,

public class ThreadDemo {

    private static ThreadDemo threadDemo;

    public ThreadDemo getInstance() {
        if (threadDemo == null) {
            synchronized (ThreadDemo.this) {
                if (threadDemo == null) {
                    threadDemo = new ThreadDemo();//第9行代码
                }
            }
        }
        return threadDemo;
    }

}

如果不加 volatile  会出现线程安全问题,为什么不加 会出现线程安全问题呢

其实线程安全会出现在 第9行代码那里,其实第9行代码默认是拆分成三步去执行的,

1:在堆内存分配一个内存空间给 ThreadDemo的实例,(就是 new ThreadDemo()

2:初始化对内存的实例      (调用 ThreadDemo 的构造方法 初始化实例 )

3: 在盏内存创建 threadDemo 变量,并指向 步骤1创建的地址(只要执行了这一步,threadDemo 就不是空

很明显,单纯 threadDemo = new ThreadDemo()  一行代码,都分了3步。

更重要,就刚才所说的,就算是默认顺序,编译器也可能会优化代码执行顺序,

优化代码后的顺序可能是:1-2-3  也有可能是:1-3-2

1-2-3到时没啥问题。

我们看下 编译器把执行顺序改成了 1-3-2会出现啥问题,

我构造一个场景:

线程1 先进来,先执行 第9行代码,在执行完了 1-3,此时 threadDemo已经非空了。此时CPU执行权被线程2夺走

此时线程2获得CPU执行权, 进来获取单例,判断threadDemo 不等于空,直接取走拿去用,

这就出现问题。因为此时:threadDemo 只是单纯指向了一个内存地址,但这个地址存的数据还没初始化。用这个实例会出现很多不可预见的问题,比如空指针等等..........

所以为了解决这个问题,Java 给出关键字volatile  ,用此关键字修饰的变量,告诉编译器:

在此变量前的代码,编译器必须保证先执行完。

在此变量后的代码,编译器必须保证后执行完。

拗口? 难理解吗?

我们写几行代码:

volatile int a = 1;
int b = 1;
int c = 1;
int b = 1;

private void test(){
    b++;//第7行
    c++;//第8行

    a++;//第9行

    b = c+b;//第11行
    c = b+;10//第12行

}

变量a 用了 volatile  修饰,编译器保证

第7行和第8行代码必须在 第9行前面执行

第11行和第12行代码必须在 第9行后面执行

但不保证 第7行和第8行哪一行先执行

当然,也不保证 第11行和第12行哪一行先执行

volatile  修饰的变量,相当于一个分割线,前面的代码必须先于自己执行,后面必须后于自己执行

好了,我们说回刚才那个单例子模式,我们如果用 volatile  修饰了 threadDemo变量,就可以保证 步骤1和 步骤2必须先于步骤3执行,所以编译后的顺序必须是: 1-2-3,所以 在threadDemo指向内存地址后的 ,该堆内存地址的数据,肯定已经初始化好的了。所以不会出现线程安全问题。

所以正确的DoubleLock单例模式应该是这样的:

public class ThreadDemo {

    private volatile static ThreadDemo threadDemo;

    public ThreadDemo getInstance() {
        if (threadDemo == null) {
            synchronized (ThreadDemo.this) {
                if (threadDemo == null) {
                    threadDemo = new ThreadDemo();//第9行代码
                }
            }
        }
        return threadDemo;
    }

}

所以我们总结下:

用 volatile  修饰了的变量,

不具备线程原子性

    具备线程可见性

    具备线程有序性

以上代码亲测无问题,有问题请留言指正。。。谢谢

猜你喜欢

转载自blog.csdn.net/Leo_Liang_jie/article/details/91465477