• JAVA多线程之volatile 与 synchronized 的比较


    一,volatile关键字的可见性

    要想理解volatile关键字,得先了解下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关键字的作用是:使变量在多个线程间可见(可见性)

     可见性的特性总结为以下2点:
    1. 对volatile变量的写会立即刷新到主存
    2. 对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

    比较:

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

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

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

    四,线程安全性

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

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

  • 相关阅读:
    跨域调用webapi web端跨域调用webapi
    如何通过js跨域调用ASP.NET Web API (请问如何实现在javascript中通过http get的方式跨域调用ASP.NET Web API?)
    MVC中使用SignaIR入门教程
    Axure RP是一个专业的快速原型设计工具
    angularJS 学习演示
    说说Angular中的$timeOut定时器
    镜像服务器文件实时监控同步程序
    服务器文件同步管理
    MySQL 数据库双向镜像、循环镜像(复制)
    vs2015密钥 企业版 专业版 (vs.net)
  • 原文地址:https://www.cnblogs.com/huangjianping/p/8241849.html
Copyright © 2020-2023  润新知