JAVA多线程之volatile 与 synchronized 的比较

一,volatile关键字的可见性html

要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图以下:java

从图中能够看出:安全

①每一个线程都有一个本身的本地内存空间--线程栈空间???线程执行时,先把变量从主内存读取到线程本身的本地内存空间,而后再对该变量进行操做多线程

②对该变量操做完后,在某个时间再把变量刷新回主内存并发

关于JAVA内存模型,更详细的可参考: 深刻理解Java内存模型(一)——基础ide

所以,就存在内存可见性问题,看一个示例程序:(摘自书上)post

 1 public class RunThread extends Thread {
 2 
 3     private boolean isRunning = true;
 4 
 5     public boolean isRunning() {
 6         return isRunning;
 7     }
 8 
 9     public void setRunning(boolean isRunning) {
10         this.isRunning = isRunning;
11     }
12 
13     @Override
14     public void run() {
15         System.out.println("进入到run方法中了");
16         while (isRunning == true) {
17         }
18         System.out.println("线程执行完成了");
19     }
20 }
21 
22 public class Run {
23     public static void main(String[] args) {
24         try {
25             RunThread thread = new RunThread();
26             thread.start();
27             Thread.sleep(1000);
28             thread.setRunning(false);
29         } catch (InterruptedException e) {
30             e.printStackTrace();
31         }
32     }
33 }

Run.java 第28行,main线程 将启动的线程RunThread中的共享变量设置为false,从而想让RunThread.java 第14行中的while循环结束。优化

若是,咱们使用JVM -server参数执行该程序时,RunThread线程并不会终止!从而出现了死循环!!this

缘由分析:spa

如今有两个线程,一个是main线程,另外一个是RunThread。它们都试图修改 第三行的 isRunning变量。按照JVM内存模型,main线程将isRunning读取到本地线程内存空间,修改后,再刷新回主内存。

而在JVM 设置成 -server模式运行程序时,线程会一直在私有堆栈中读取isRunning变量。所以,RunThread线程没法读到main线程改变的isRunning变量

从而出现了死循环,致使RunThread没法终止。这种情形,在《Effective JAVA》中,将之称为“活性失败”

解决方法,在第三行代码处用 volatile 关键字修饰便可。这里,它强制线程从主内存中取 volatile修饰的变量。

    volatile private boolean isRunning = true;

 

扩展一下,当多个线程之间须要根据某个条件肯定 哪一个线程能够执行时,要确保这个条件在 线程 之间是可见的。所以,能够用volatile修饰。

综上,volatile关键字的做用是:使变量在多个线程间可见(可见性)

 

二,volatile关键字的非原子性

所谓原子性,就是某系列的操做步骤要么所有执行,要么都不执行。

好比,变量的自增操做 i++,分三个步骤:

①从内存中读取出变量 i 的值

②将 i 的值加1

③将 加1 后的值写回内存

这说明 i++ 并非一个原子操做。由于,它分红了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有所有执行。

关于volatile的非原子性,看个示例:

 1 public class MyThread extends Thread {
 2     public volatile static int count;
 3 
 4     private static void addCount() {
 5         for (int i = 0; i < 100; i++) {
 6             count++;
 7         }
 8         System.out.println("count=" + count);
 9     }
10 
11     @Override
12     public void run() {
13         addCount();
14     }
15 }
16 
17 public class Run {
18     public static void main(String[] args) {
19         MyThread[] mythreadArray = new MyThread[100];
20         for (int i = 0; i < 100; i++) {
21             mythreadArray[i] = new MyThread();
22         }
23 
24         for (int i = 0; i < 100; i++) {
25             mythreadArray[i].start();
26         }
27     }
28 }

MyThread类第2行,count变量使用volatile修饰

Run.java 第20行 for循环中建立了100个线程,第25行将这100个线程启动去执行 addCount(),每一个线程执行100次加1

指望的正确的结果应该是 100*100=10000,可是,实际上count并无达到10000

缘由是:volatile修饰的变量并不保证对它的操做(自增)具备原子性。(对于自增操做,可使用JAVA的原子类AutoicInteger类保证原子自增)

好比,假设 i 自增到 5,线程A从主内存中读取i,值为5,将它存储到本身的线程空间中,执行加1操做,值为6。此时,CPU切换到线程B执行,从主从内存中读取变量i的值。因为线程A尚未来得及将加1后的结果写回到主内存,线程B就已经从主内存中读取了i,所以,线程B读到的变量 i 值仍是5

至关于线程B读取的是已通过时的数据了,从而致使线程不安全性。这种情形在《Effective JAVA》中称之为“安全性失败”

综上,仅靠volatile不能保证线程的安全性。(原子性)

 

此外,volatile关键字修饰的变量不会被指令重排序优化。这里以《深刻理解JAVA虚拟机》中一个例子来讲明下本身的理解:

线程A执行的操做以下:

Map configOptions ;
char[] configText;

volatile boolean initialized = false;

//线程A首先从文件中读取配置信息,调用process...处理配置信息,处理完成了将initialized 设置为true
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfig(configText, configOptions);//负责将配置信息configOptions 成功初始化
initialized = true;

 

线程B等待线程A把配置信息初始化成功后,使用配置信息去干活.....线程B执行的操做以下:

while(!initialized)
{
    sleep();
}

//使用配置信息干活
doSomethingWithConfig();

 

若是initialized变量不用 volatile 修饰,在线程A执行的代码中就有可能指令重排序。

即:线程A执行的代码中的最后一行:initialized = true 重排序到了 processConfig方法调用的前面执行了,这就意味着:配置信息还未成功初始化,可是initialized变量已经被设置成true了。那么就致使 线程B的while循环“提早”跳出,拿着一个还未成功初始化的配置信息去干活(doSomethingWithConfig方法)。。。。

所以,initialized 变量就必须得用 volatile修饰。这样,就不会发生指令重排序,也即:只有当配置信息被线程A成功初始化以后,initialized 变量才会初始化为true。综上,volatile 修饰的变量会禁止指令重排序(有序性)

 

三,volatile 与 synchronized 的比较

volatile主要用在多个线程感知实例变量被更改了场合,从而使得各个线程得到最新的值。它强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。

关于synchronized,可参考:JAVA多线程之Synchronized关键字--对象锁的特色

比较:

①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法

②volatile只能保证数据的可见性,不能用来同步,由于多个线程并发访问volatile修饰的变量不会阻塞。

synchronized不只保证可见性,并且还保证原子性,由于,只有得到了锁的线程才能进入临界区,从而保证临界区中的全部语句都所有执行。多个线程争抢synchronized锁对象时,会出现阻塞。

 

四,线程安全性

线程安全性包括两个方面,①可见性。②原子性。

从上面自增的例子中能够看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。

 

关于Synchronized底层实现原理,参考:http://www.javashuo.com/article/p-uvajwpbz-mo.html

参考:JAVA多线程之线程间的通讯方式

JAVA多线程之当一个线程在执行死循环时会影响另一个线程吗?

JAVA多线程之wait/notify

相关文章
相关标签/搜索