java多线程中 volatile与synchronized的区别-阿里面试 java中volatile关键字的含义

volatile 与 synchronized 的比较(阿里面试官问的问题)  html

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

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

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

volatile本质是在告诉jvm当前变量在寄存器中的值是不肯定的,须要从主存中读取,synchronized则是锁定当前变量,只有当前线程能够访问该变量,其余线程被阻塞住.
volatile仅能使用在变量级别,synchronized则可使用在变量,方法.
volatile仅能实现变量的修改可见性,但不具有原子特性,而synchronized则能够保证变量的修改可见性和原子性.
volatile不会形成线程的阻塞,而synchronized可能会形成线程的阻塞.
volatile标记的变量不会被编译器优化,而synchronized标记的变量能够被编译器优化. 安全

二,线程安全性 多线程

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

仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。 jvm

三,volatile关键字的可见性 ide

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

 

从图中能够看出: 

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

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

在Java中,为了保证多线程读写数据时保证数据的一致性,能够采用两种方式: 

(a)如用synchronized关键字,或者使用锁对象.

(b)使用volatile关键字,用一句话归纳volatile,它可以使变量在值发生改变时能尽快地让其余线程知道. 

synchronized 

全部加上synchronized 和 块语句,在多线程访问的时候,同一时刻只能有一个线程可以用synchronized 修饰的方法 或者 代码块。 

volatile

首先咱们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操做会先在寄存器或者是CPU缓存上进行,最后才写入内存.

而在这个过程,变量的新值对其余线程是不可见的.而volatile的做用就是使它修饰的变量的读写操做都必须在内存中进行! 

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操做。

对于volatile修饰的变量,JVM虚拟机只保证从主内存加载到线程工做内存的值是最新的。 

所以,就存在内存可见性问题,看一个示例程序: 

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

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没法终止 

解决方法,在第三行代码处用 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 修饰的变量会禁止指令重排序(有序性)  

在 java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每个线程运行时都有一个线程栈,

线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先经过对象的引用找到对应在堆内存的变量的值,而后把堆内存

变量的具体值load到线程本地内存中,创建一个变量副本,以后线程就再也不和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,

在修改完以后的某一个时刻(线程退出以前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图

描述这写交互 

java volatile1

  

read and load 从主存复制变量到当前工做内存
use and assign  执行代码,改变共享变量值 
store and write 用工做内存数据刷新主存相关内容

其中use and assign 能够屡次出现

可是这一些操做并非原子性,也就是 在read load以后,若是主内存count变量发生修改以后,线程工做内存中的值因为已经加载,不会产生对应的变化,因此计算出来的结果会和预期不同

对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工做内存的值是最新的

例如假如线程1,线程2 在进行read,load 操做中,发现主内存中count的值都是5,那么都会加载这个最新的值

在线程1堆count进行修改以后,会write到主内存中,主内存中的count变量就会变为6

线程2因为已经进行read,load操做,在进行运算以后,也会更新主内存count的变量值为6

致使两个线程及时用volatile关键字修改以后,仍是会存在并发的状况。 

下面看一个例子,咱们实现一个计数器,每次线程启动的时候,会调用计数器inc方法,对计数器进行加一 

执行环境——jdk版本:jdk1.6.0_31 ,内存 :3G   cpu:x86 2.4G

public class Counter {
     public static int count = 0; 
    public static void inc() { 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        } 
        count++;
    } 
    public static void main(String[] args) { 
        //同时启动1000个线程,去进行i++计算,看看实际结果 
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        } 
        //这里每次运行的值都有可能不一样,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }
}

输出为:

运行结果:Counter.count=995

实际运算结果每次可能都不同,本机的结果为:运行结果:Counter.count=995,能够看出,在多线程的环境下,Counter.count并无指望结果是100 

不少人觉得,这个是多线程并发问题,只须要在变量count以前加上volatile就能够避免这个问题,那咱们在修改代码看看,看看结果是否是符合咱们的指望 

public class Counter { 
    public volatile static int count = 0; 
    public static void inc() { 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        } 
        count++;    } 
    public static void main(String[] args) { 
        //同时启动1000个线程,去进行i++计算,看看实际结果
 
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        }
 
        //这里每次运行的值都有可能不一样,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }
}

运行结果:

运行结果:Counter.count=992

 运行结果仍是没有咱们指望的1000

参考:java中volatile关键字的含义 

相关文章
相关标签/搜索