前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发编程:volatile关键字解析

Java并发编程:volatile关键字解析

作者头像
用户6182664
发布2019-09-05 17:45:44
5300
发布2019-09-05 17:45:44
举报

volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。

一. volatile关键字是什么?

当一个变量定义为volatile之后,它将具备两种特性:

①保证此变量对所有线程的可见性
当一条线程修改了这个变量的值,新值对于其他线程可以说是可以立即得知的。Java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存保存了该线程使用到的变量在主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读取主内存中的变量。《深入理解Java虚拟机第二版》P363
②禁止指令重排序优化

普通的变量仅仅会保证在该方法的执行过程中所有依赖该赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一直。在一个线程的方法执行过程中无法感知到这点,故Java内存模型描述所谓的“线程内表示为串行的语义”《深入理解Java虚拟机第二版》P369

二. volatile两种特性的体现

①保证此变量对所有线程的可见性

示例1:验证可见性

代码语言:javascript
复制
public class VolatileTest {
	public static int count=0;
    public static void main(String[] args) {
    	
        for(int i=0;i<100000;i++){
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
            
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        			    add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        }
    }
    public static synchronized int add(){
    	count = count + 1;//保证count加一的操作是原子的
    	return count;
    }
    
    public static synchronized int sub(){
    	
    	//保证count加一的操作是原子的
    	count = count - 1;
    	return count;
    }
    
    public static synchronized int get(){
    	return count;
    }
}
代码语言:javascript
复制
运行结果:
    ...
    499220 
    499219 
    499218
结果说明:预期值是500000,结果比预期值小。在count未使用volitail修饰时,count的值,在内存中存在高速缓存区,主存区。当线程更新count值时,由于其他线程不知道count值已修改,因此高速缓存区中的值存在旧数据值的可能,会导致取到的数据不正确。由此示例,可以体现内存模型存在缓存一致性问。

示例2:验证原子性

代码语言:javascript
复制
public class VolatileTest {
	public volatile static int count=0;
    public static void main(String[] args) {
    	
        for(int i=0;i<100000;i++){
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
            
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        }
    }
    public static int add(){
    	count = count + 1;//保证count加一的操作是原子的
    	return count;
    }
}
代码语言:javascript
复制
运行结果 :
    ...
    499934 
    499935 
    499936
由于volitail是不能保证原子性的,当程序的add()方法的锁给去除时,由于线程不是安全的,存在并发,count++又非原子操作,因此会导致结果不正确。

示例3:

代码语言:javascript
复制
public class VolatileTest {
	public volatile static int count=0;
    public static void main(String[] args) {
    	
        for(int i=0;i<100000;i++){
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
            
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        }
    }
    public static synchronized int add(){
    	count = count + 1;//保证count加一的操作是原子的
    	return count;
    }
}
代码语言:javascript
复制
运行结果:
    ...
    499997 
    499997 
    499998 
    499999 
    500000

首先count使用volatile修饰,保证变量的可见性,在调用add()方法时,给方法加锁,保证原子性,因此结果多次运行也未发现问题,验证volatile的可见性、非原子性。

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

首先当线程执行这个语句时,会先从主存当中读取count的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,就可能存在缓存不一致的问题,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。
指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,靠什么呢?处理器会考虑指令之间的数据依赖性
验证volatile的有序性:
代码语言:javascript
复制
public class VolatileTest {
	public volatile static int count=0;
	public volatile static int count1=0;
	public volatile static int count2=0;
	public volatile static int count3=0;
	public volatile static int count4=0;
	public volatile static int countTrue=0;
	public volatile static int countFalse=0;
	public static int x, y;
	public volatile static boolean flag = false;
	public static void main(String[] args) {
    	
		volatileSortTest();
    }
	
	 public static void volatileSortTest(){
		
		for (int i = 0; i < 10000; i++) {
			new Thread(new Runnable() {
				public void run() {
					x = 1;
					y = 2;
					flag = true;
					x = 3;
					y = 4;
					count(x);
					count(y);
					countBool(flag);
				}
			}).start();
			new Thread(new Runnable() {
				public void run() {
					x = 1;
					y = 2;
					flag = true;
					x = 3;
					y = 4;
					count(x);
					count(y);
					countBool(flag);
				}
			}).start();
		}
	 }
    
    public void volatileWatchTest(){
    	for(int i=0;i<100000;i++){
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        }
    }
    
    public static synchronized int add(){
    	count = count + 1;//保证count加一的操作是原子的
    	return count;
    }
    
    public static synchronized void count(int count){
    	
    	//保证count加一的操作是原子的
    	if (count == 1) {
    		count1++;
    	}
    	if (count == 2) {
    		count2++;
    	}
    	if (count == 3) {
    		count3++;
    	}
    	if (count == 4) {
    		count4++;
    	}
    	System.out.println("1="+count1);
		System.out.println("2="+count2);
		System.out.println("3="+count3);
		System.out.println("4="+count4);
    }
    
    public static synchronized void countBool(boolean count){
    	
    	//保证count加一的操作是原子的
    	if (count) {
    		countTrue++;
    	}
    	if (!count) {
    		countFalse++;
    	}
    	System.out.println("true="+countTrue);
		System.out.println("false="+countFalse);
    }
}
代码语言:javascript
复制
运行结果:
    1=0
    2=1
    3=20000
    4=19999
    true=20000
    false=0

由于x,y没有volatile修饰,不能保证有序性,赋值的时候可能存在线程赋值顺序不正确,因此,统计值的结果也可能导致不一致。

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。  并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

当x,y调整为volatile 修饰时,运行结果为:
代码语言:javascript
复制
public class VolatileTest {
	public volatile static int count=0;
	public volatile static int count1=0;
	public volatile static int count2=0;
	public volatile static int count3=0;
	public volatile static int count4=0;
	public volatile static int countTrue=0;
	public volatile static int countFalse=0;
	public static int x, y;
	public volatile static boolean flag = false;
	public static void main(String[] args) {
    	
		volatileSortTest();
    }
	
	 public static void volatileSortTest(){
		
		for (int i = 0; i < 100000; i++) {
			new Thread(new Runnable() {
				public void run() {
					testCount();
				}
			}).start();
			
			new Thread(new Runnable() {
				public void run() {
					testCount();
				}
			}).start();
			
			new Thread(new Runnable() {
				public void run() {
					testCount();
				}
			}).start();
		}
	 }
    
    public static void volatileWatchTest(){
    	for(int i=0;i<100000;i++){
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        	
        	//开启10个线程
        	new Thread(new Runnable() {
        			public void run() {
        				for (int j = 0; j < 1; j++) {
        					
        					//每个线程执行加100
        					add();
        				}
        				System.out.print(count+" \n");
        			}
        		
            }).start();
        }
    }
    
    public static synchronized void testCount(){
    	x = 1;
		y = 2;
		flag = true;
		x = 3;
		y = 4;
		count(x);
		count(y);
		countBool(flag);
    }
    
    public static synchronized int add(){
    	count = count + 1;//保证count加一的操作是原子的
    	return count;
    }
    
    public static synchronized void count(int count){
    	
    	//保证count加一的操作是原子的
    	if (count == 1) {
    		count1++;
    	}
    	if (count == 2) {
    		count2++;
    	}
    	if (count == 3) {
    		count3++;
    	}
    	if (count == 4) {
    		count4++;
    	}
    	System.out.println("1="+count1);
		System.out.println("2="+count2);
		System.out.println("3="+count3);
		System.out.println("4="+count4);
    }
    
    public static synchronized void countBool(boolean count){
    	
    	//保证count加一的操作是原子的
    	if (count) {
    		countTrue++;
    	}
    	if (!count) {
    		countFalse++;
    	}
    	System.out.println("true="+countTrue);
		System.out.println("false="+countFalse);
    }
}

运行结果为:

第一次:

代码语言:javascript
复制
    1=0
    2=0
    3=20000
    4=20000
    true=20000
    false=0

注意:在使用volatile时,需要保证是在原子操作上处理。因此,testCount()需要加锁。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java程序员那些事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一. volatile关键字是什么?
    • ①保证此变量对所有线程的可见性
      • ②禁止指令重排序优化
      • 二. volatile两种特性的体现
        • ①保证此变量对所有线程的可见性
          • 结果说明:预期值是500000,结果比预期值小。在count未使用volitail修饰时,count的值,在内存中存在高速缓存区,主存区。当线程更新count值时,由于其他线程不知道count值已修改,因此高速缓存区中的值存在旧数据值的可能,会导致取到的数据不正确。由此示例,可以体现内存模型存在缓存一致性问。
          • 由于volitail是不能保证原子性的,当程序的add()方法的锁给去除时,由于线程不是安全的,存在并发,count++又非原子操作,因此会导致结果不正确。
          • 基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,就可能存在缓存不一致的问题,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。
        • 指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
          • 虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,靠什么呢?处理器会考虑指令之间的数据依赖性
            • 验证volatile的有序性:
              • 当x,y调整为volatile 修饰时,运行结果为:
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档