A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

一、volatile

在java中,volatile关键字解决的是变量在多个线程之间的可见性,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

(2)禁止进行指令重排序。
注:不了解“原子性,可见性和有序性”的同学可以看下笔者之前的博客

1、volatile保证可见性

先看下下面的例子:

public class RunThread extends Thread {
        private boolean isRunning = true;
        public boolean isRunning(){
                return isRunning;
        }
        public void setRunning(boolean isRunning){
                this.isRunning = isRunning;
        }
       
        @Override
        public void run(){
                System.out.println("进入run了");
                while(isRunning){
                }
                System.out.println("线程被停止了");
        }
}
public class TestRunThread {
        public static void main(String[] args){
                try{
                        RunThread thread = new RunThread();
                        thread.start();
                        Thread.sleep(1000);
                        thread.setRunning(false);
                        System.out.println("已经将Running设置为false");
                }catch(InterruptedException e){
                        e.printStackTrace();
                }
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
运行结果如下:


线程一直在私有堆栈中取得isRunning的值是true,而代码thread.setRunning(false);虽然被执行,但其更新的却是公共堆栈的isRunning变量值为false,所以出现了死循环的状态,代码System.out.println(“线程被停止了”);从未被执行。如下图:

这个问题其实就是私有堆栈(上图的的“工作内存”)中的值和公共堆栈(上图中的“主内存”)中的值不同步造成的。解决这样的问题就需要使用volatile关键字了,它的作用就是当线程访问isRunning这个变量是,强制性从公共堆栈中取值。
将RunThread.java的变量isRunning用volatile关键字修饰,其他不变:

private volatile boolean isRunning = true;
1
重新运行,结果如下:

通过使用volatile关键字,强制从公共内存中读取变量:

这个例子说明了volatile关键字可以保证可见性(这也是volatile最重要的功能)

2、volatile不保证原子性

volatile关键字虽然保证了变量在多线程之间的可见性,但它却不具备同步性,所以也就不具备原子性。看下面这个例子:

public class MyThread extends Thread{
        public volatile static int count;//volatile 修饰count
        private static void addCount(){
                for(int i=0; i<100; i++){
                        count++;
                }
                System.out.println("count=" + count);
        }
       
        @Override
        public void run(){//实现run方法
                addCount();
        }
}
public class TestMyThread {
        public static void main(String[] args) {
                MyThread[] threadArray = new MyThread[100];
                for(int i=0; i<100; i++){
                        threadArray = new MyThread();//创建100个MyThread进程
                }
                for(int i=0; i<100; i++){
                        threadArray.start();
                }
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
运行结果:

更改MyThread.java的addCount方法,将其用synchronized修饰,其他不变,

private synchronized static void addCount()
1
重新运行,结果如下:


由此可以看出,volatile不保证原子性,否则运行结果应与synchronized修饰的结果一样。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。代码中的i++这种自增操作并不是一个原子操作,也就是非线程安全的。i++的操作步骤分解如下:
(1)从内存中取出 i 的值
(2)计算 i 的值
(3)将 i 的值写到内存中
假如在第(2)步计算值的时候,有另外一个进程修改了 i 的值,那么这个时候就会出现脏读(这就是上面运行结果count始终小于10000的原因)。解决的办法其实就是使用synchronized,所以说volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存。

3、volatile在一定程度上保证有序性

volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5
1
2
3
4
5
6
7
8
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

二、volatile的实现原理

volatile是怎么保证可见性和禁止指令重排序的?

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写入主存;

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

三、总结

下面将volatile与synchronized做一下比较总结:
(1)volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好

(2)volatile只能修饰变量,而synchronized可以修饰方法、代码块和类,新版本的jdk也对synchronized做了许多优化,开发中使用synchronized的比率比较大

(3)多线程访问volatile不会发生阻塞,而synchronized会发生阻塞

(4)volatile能保证可见性,但不能保证原子性,而synchronized都可以保证

(5)volatile解决的是变量在多线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性。

四、参考

《深入理解java虚拟机》 周志明
《Java多线程编程核心技术》高洪岩
---------------------
【转载】
作者:Felix_ar
原文:https://blog.csdn.net/Felix_ar/article/details/84441938


2 个回复

倒序浏览
奈斯
回复 使用道具 举报
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马