JAVA多线程之volatile 与 synchronized 的比较(主 volatile)
一,volatile关键字的可见性
就存在内存可见性问题,看一个示例程序:
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 }
我们期待的结果是在main线程将启动的线程RunThread中的共享变量设置为false,从而想让RunThread.java 第14行中的while循环结束。
然而,我们使用JVM -server参数执行该程序时,RunThread线程并不会终止!从而出现了死循环!!
原因分析:
现在有两个线程,一个是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关键字的非原子性
关于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 }
期望的正确的结果应该是 100*100=10000,但是,实际上count并没有达到10000
原因是:volatile修饰的变量并不保证对它的操作(自增)具有原子性。(对于自增操作,可以使用JAVA的原子类AutoicInteger类保证原子自增)
综上,仅靠volatile不能保证线程的安全性。(原子性)
三,volatile关键字修饰的变量不会被指令重排序优化
线程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主要用在多个线程感知实例变量被更改了场合,从而使得各个线程获得最新的值。它强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。
比较:
①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。

浙公网安备 33010602011771号