zoukankan      html  css  js  c++  java
  • 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则可实现线程的安全性。

  • 相关阅读:
    【流量劫持】SSLStrip 终极版 —— location 瞒天过海
    【流量劫持】沉默中的狂怒 —— Cookie 大喷发
    【流量劫持】SSLStrip 的未来 —— HTTPS 前端劫持
    Web 前端攻防(2014版)
    流量劫持 —— 浮层登录框的隐患
    流量劫持能有多大危害?
    流量劫持是如何产生的?
    XSS 前端防火墙 —— 整装待发
    XSS 前端防火墙 —— 天衣无缝的防护
    XSS 前端防火墙 —— 无懈可击的钩子
  • 原文地址:https://www.cnblogs.com/huangjianping/p/8241849.html
Copyright © 2011-2022 走看看