一,volatile关键字的可见性

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

从图中可以看出:

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

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

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

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

 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线程并不会终止!从而出现了死循环!!

原因分析:

现在有两个线程,一个是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则可实现线程的安全性。

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

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

JAVA多线程之wait/notify

JAVA Synchronized (三) volatile 与 synchronized 的比较的更多相关文章

  1. java多线程中 volatile与synchronized的区别-阿里面试

    volatile 与 synchronized 的比较(阿里面试官问的问题) ①volatile轻量级,只能修饰变量.synchronized重量级,还可修饰方法 ②volatile只能保证数据的可见 ...

  2. java中的volatile和synchronized

    关于volatile和同步相关的东西,网上有太多错误和解释不清的东西, 所以查阅相关书籍和文章后总结如下, 如果还是也存在不正确的内容,请一定要指出来, 以免误人子弟:) 1. 原子性与可视性 原子性 ...

  3. Java中的Volatile和synchronized的区别

    Synchronized和Volatile四个不同点: 1.粒度不同,前者锁对象和类 ,后者针对变量2.syn阻塞,volatile线程不阻塞3.syn保证三大特性,volatile不保证原子性4.s ...

  4. Java 并发编程——volatile与synchronized

    一.Java并发基础 多线程的优点 资源利用率更好 程序设计在某些情况下更简单 程序响应更快 这一点可能对于做客户端开发的更加清楚,一般的UI操作都需要开启一个子线程去完成某个任务,否者会容易导致客户 ...

  5. java多线程(三)——锁机制synchronized(同步语句块)

    用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法之行一个长时间的任务,那么B线程必须等待比较长的时间,在这样的情况下可以使用synchronized同步语句快来解 ...

  6. 从JAVA看C#中volatile和synchronized关键字的作用

    最近一直在想C#中 volatile关键字到底是用来干什么的?查了很多.NET的文章都是说用volatile修饰的变量可以让多线程同时修改,这是什么鬼... 然后查到了下面这篇JAVA中关于volat ...

  7. Java内存可见性volatile

    概述 JMM规范指出,每一个线程都有自己的工作内存(working memory),当变量的值发生变化时,先更新自己的工作内存,然后再拷贝到主存(main memory),这样其他线程就能读取到更新后 ...

  8. java面试记录二:spring加载流程、springmvc请求流程、spring事务失效、synchronized和volatile、JMM和JVM模型、二分查找的实现、垃圾收集器、控制台顺序打印ABC的三种线程实现

    注:部分答案引用网络文章 简答题 1.Spring项目启动后的加载流程 (1)使用spring框架的web项目,在tomcat下,是根据web.xml来启动的.web.xml中负责配置启动spring ...

  9. Java 线程 — synchronized、volatile、锁

    线程同步基础 synchronized 和volatile是Java线程同步的基础. synchronized 将临界区的内容上锁,同一时刻只有一个进程能访问该临界区代码 使用的是内置锁,锁一个时刻只 ...

随机推荐

  1. 蓝桥java 入门训练 Fibonacci数列

    import java.util.Scanner; public class Main{ public static void main(String[] args) { int maxn=10000 ...

  2. Java成长之路

    怎样学习才能从一名Java初级程序员成长为一名合格的架构师,或者说一名合格的架构师应该有怎样的技术知识体系,这是不仅一个刚刚踏入职场的初级程序员也是工作三五年之后开始迷茫的老程序员经常会问到的问题.希 ...

  3. (转) go Cron的使用

    Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式: Seconds Minutes Hours DayofMonth Month ...

  4. Java抓屏程序代码

    原文:http://www.open-open.com/code/view/1422262655200 import java.awt.Dimension; import java.awt.Recta ...

  5. 超级强大的淘宝开源平台(taobao-code)

    今天发现了一个免费又高级的开源SVN服务器,taobao,阿里云CODE.迫不及待的注册了一个.感觉不错,分享给大家. 先说说我们用过的几个SVN服务器吧: google code oksvn(感觉不 ...

  6. PostgreSQL触发器的使用

    原文: https://www.yiibai.com/postgresql/postgresql-trigger.html -------------------------------------- ...

  7. 关于android 使用bitmap的OOM心得和解决方式

    android开发,从2010年開始学习到如今的独立完毕一个app,这漫长的四年,已经经历了非常多次bug的折磨.无数次的加班训练.然而,自以为自己已经比較了解android了,却近期在一个项目上.由 ...

  8. mysql 同样内容的字段合并为一条的方法

    从两个表中内联取出的数据,当中category_name字段有同样内容,想将具有同样内容的字段进行合并,将amount字段进行加法运算,变成下表中的内容 url=http%3A%2F%2Fdev.my ...

  9. (转)gcc学习笔记

    1.gcc -Wall hello.c -o hello //编译源文件,显示警告信息 2../a.out   //运行程序 3.gcc -Wall calc.c /usr/lib/libm.a -o ...

  10. appium 控件定位

    转自:http://www.2cto.com/kf/201410/340345.html AppiumDriver的各种findElement方法的尝试,尝试的目标应用是SDK自带的Notepad应用 ...