深度解析volatile关键字(保证够全面)❤❤

Posted 勇敢牛牛不怕困难@帅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度解析volatile关键字(保证够全面)❤❤相关的知识,希望对你有一定的参考价值。

深度解析volatile关键字

volatile名词解释

volatile第一次在c++代码里有接触,当时老师只介绍了其用法,原理这些并没有深入了解,如今再一次在java代码里碰见了。就必须得好好的一探究竟!首先volatile是一个特征修饰符,它的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且每次要求直接读取。volatile修饰变量是说这个变量可能会被意想不到地改变,编译器就不会去假设这个变量的值了。

volatile底层作用@C和C++

简单一点理解就是防止编译器对代码的优化,对于硬件而言,假设下面四条语句表示了不同的操作,就是会产生四种不同的效果,但是编译器会进行优化,会将0x58做为最后的操作。这样一来只执行了一条机器代码。如果对XBYTE[]加上volatile关键字,则会产生四条代码。

XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;

C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier,当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

volatile int i=10;
int a = i;
...
...
int b = i;

volatile指出i是随时可能发生变化的,每次使用它必须从当前i的地址去读取,因而编译器生成汇编代码会重新从i的地址读取值放到b中。对于某些编译器自带优化功能,由于发现两次进行对i进行读数据,编译器就会主动把上次读取的值放在b中,而不是重新去i地址读取i。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

#include <stdio.h>
 
void main()
{
    int i = 10;
    int a = i;
 
    printf("i = %d", a);
 
    // 下面汇编语句的作用就是改变内存中 i 的值
    // 但是又不让编译器知道
    __asm {
        mov dword ptr [ebp-4], 20h
    }
 
    int b = i;
    printf("i = %d", b);
}

在不加volatile关键字的情况下,输出的结果都是10,这是由于编译器的优化问题,在a变量取到i的值,接着b也去获取i的值,此时便会去读取a地址的值也就是10,所以此时b也就为10 。但是如果把i变量加上关键字volatile就不会出现这种情况。因为每次对i的读操作都会去i本身地址处读取该值,也就不会往i引用变量处取值。
其实不只是内嵌汇编操纵栈"这种方式属于编译无法识别的变量改变,另外更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。一般说来,volatile用在如下的几个地方:

  1. 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
  2. 多任务环境下各任务间共享的标志应该加 volatile;
  3. 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;
    小结:
    这个关键字是用来设定某个对象的存储位置在内存中,而不是寄存器中。因为一般的对象编译器可能会将其的拷贝放在寄存器中用以加快指令的执行速度。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,该关键字的作用是防止优化编译器把变量从内存装入 CPU 寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。例如下图:

java中的volatile关键字(主要针对于高并发)

一谈高并发必不可少便是并发编程的3大重要特征:
原子性: 即一个操作或者多个操作,要么全部执行,并且执行过程中不会被任何因素打算,一旦打断就都不执行。原子性通常不允许多线程对其操作,同时对一变量只能有一个线程进行处理。在java里对基本数据类型赋值操作,必须是值赋给变量的情况可以看作是原子性操作。i++这种不属于原子性操作。
可见性: 当多个线程访问同一个变量的时 一个线程修改这个变量的值时 其他线程能够立即看到这会个修改的值。例如:
这是线程1执行的代码 CPU1
Int i = 0;
i = 10;
线程2执行的代码 CPU2
int j = i;
会把初始值加载到CPU1的高速缓存中 然后赋值为0 那么CPU1的高速缓存当中i就变成了10 却没有立即写入到主存中。
这个时候j = i 它会先去主存中读取并且加载到CPU2中 这个时候值还是0。
有序性: 指程序代码会按照先后顺序执行。如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。例如:
int i= 0;
Boolean flag = false;
i = 10; //语句1
flag = true //语句2
如果JVM在执行代码的时候语句2在语句1之前执行,可能会发生指令重排序
指令重排序:
处理器为了提高运行效率,可能会对输入的代码进行优化,它不保证程序中各个语句的执行的先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码的顺序结果一致。

		int a = 10;//语句1
		int r = 2;//语句2
		a = a+3;//语句3
		r = a*a;//语句4

这是一段变量赋值的代码,处理器在执行过程中不会先执行语句3,4.处理器在进行重排序的时候会考虑指令之间的依赖性。指令的重排序 不会影响到单个线程的执行,但是会影响到并发执行的正确性。java的内存模型具备一些先天的“有序性“ 不需要通过任何手段就能够保证有序,通常被称为happens-before原则。
happens-before原则:先行发生规则
程序的次序规则:
一个线程内 按照代码顺序 书写在前面的操作先执行,发生于书写在后面的操作
锁定规则:
一个unLock操作 先行发生于后面对同一个锁的lock操作
Volatile变量规则:
对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:
如果操作A先行发生于操作B 而操作B又线程发生操作C,则可以得出操作A先行发生于操作C
线程启动规则:
Thread对象的start方法先行发生于此线程的每一个动作
线程中断规则:
对线程interrupt()调用先行发生于被中断线程的代码检测到中断时间的发生
线程终结规则:
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread中join方法结束,Thread中isAlive()返回值手段检测到线程已经终止执行
对象终结规则:
一个对象的初始化完成 先行与发生它的finalize()方法的开始。

解析java中的volatile

volatile可以用于修饰变量,一旦共享一个变量(类的成员变量,类的静态成员变量) 被volatile修饰之后具有两层含义:
1.保证了不同线程对这个变量进行操作时可见性
2.禁止进行指令重排序
例如:

// 线程1
 Boolean stop = false;
  Wihle(!stop){
    dosomething();
}
// 线程2
stop = true;

会导致死循环、中断最终无法执行。线程2 更改了stop的值之后, 但是还没来得及把stop的值写到主存中,线程2去执行其他的代码部分,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存中, 线程1不知道线程2修改了stop的值 所以该程序的执行结果 死循环

使用volatile关键字

1.强制将修改的值立即写入主存
2.线程2进行修改时 会导致线程1的工作内存中缓存变量stop的缓存无效
3.由于线程1的工作内存中的缓存变量stop无效 线程1会到主存中去读取

package com.openlab.test;

public class VolatileTest {
	
	
	public volatile int inc = 0;
	
	public void increase(){
		inc++;
	}
	
	public static void main(String[] args) {
		
		VolatileTest vt = new VolatileTest();
		
		for (int i = 0; i < 10; i++) {
			
			new Thread(){
				public void run(){
					
					for (int j = 0; j < 1000; j++) {
						vt.increase();
					}
					
				};
			}.start();
		}
		
		while(Thread.activeCount()>1){
			//保证前面的线程都执行完
			
			Thread.yield();
			
			System.out.println(vt.inc);
		
		}
		
	}

}

Volatile关键字可以保证并发的可见性,没能保证原子性,可见性只能保证每次读取的是最新的值,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性。

采用synchronized保证原子性

package com.openlab.test;

public class SyTest {
	
    public  int inc = 0;
	
	public synchronized void increase(){
		inc++;
	}
	
	public static void main(String[] args) {
		final SyTest vt = new SyTest();
		
		for (int i = 0; i < 10; i++) {
			
			new Thread(){
				public void run(){
					
					for (int j = 0; j < 1000; j++) {
						vt.increase();
					}
					
				};
			}.start();
		}
		
		while(Thread.activeCount()>1){
			//保证前面的线程都执行完
			
			Thread.yield();
			
			System.out.println(vt.inc);
		
		}
		
	}
	

}

采用Lock锁保证原子性

package com.openlab.test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
	
	public  int inc = 0;
	
	Lock lock = new ReentrantLock();
	
	public void increase(){
		
		lock.lock();
		try{
			
			inc++;
		}finally{
			lock.unlock();
		}
	}
	
	public static void main(String[] args) {
		
        final LockTest vt = new LockTest();
		
		for (int i = 0; i < 10; i++) {
			
			new Thread(){
				public void run(){
					
					for (int j = 0; j < 1000; j++) {
						vt.increase();
					}
					
				};
			}.start();
		}
		
		while(Thread.activeCount()>1){
			//保证前面的线程都执行完
			
			Thread.yield();
			
			System.out.println(vt.inc);
		
		}
		
		
	}

}

采用AtomicInteger原子操作类

Java.util.concurrent.atomic 包提供了原子操作类, 对基本数据类型的 自增加1操作,自减
加法操作 减法操作,能够保证操作是原子性 atomic 利用CAS来实现原子性操作的
(Compare And Swap) CAS 是利用处理器提供的CMPXCHG指令来实现。 本身就是一个原子性操作

package com.openlab.test;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {
	
	public AtomicInteger inc = new AtomicInteger();
	
	
	public void increase(){
		
		inc.getAndIncrement();
	}
	
	public static void main(String[] args) {
		
        final AtomicIntegerTest vt = new AtomicIntegerTest();
		
		for (int i = 0; i < 10; i++) {
			
			new Thread(){
				public void run(){
					
					for (int j = 0; j < 1000; j++) {
						vt.increase();
					}
					
				};
			}.start();
		}
		
		while(Thread.activeCount()>1){
			//保证前面的线程都执行完
			
			Thread.yield();
			
			System.out.println(vt.inc);
		
		}
	}
	

}

volatile保证了有序性

volatile关键字能禁止指令重排序,能在一定程序上保证有序性。
禁止指令重排序有两层意思:
1.当程序执行到volatile变量的读操作或写操作时,在其前面的操作的更改肯定要全部执行,且结果已经对后面的操作可见,在其后面的操作还没执行。
2.在进行指令优化时 不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前执行。

线程1
context = loadContext()//语句1
inited = true //语句2


线程2while(!inited){
   sleep();
}
doSomeint(context)

如果使用volatile关键字对inited变量去修饰一下,就能够保证程序有序性,在执行最后一行代码之前能够保证context变量的初始化。

加入volatile关键字时 会多出一个lock前缀指令,实际上相当于一个内存屏障—内存栅栏
内存屏障:
1.它要确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障之后,并且保证在执行内存屏障时 前面操作已经全部完成。
2.它会强制将对缓存的修改操作立即写入main memory 主存
3.如果是写操作 它会导致其他cpu中对应的缓存行无效。

volatile的使用场景

Synchronized 关键字是防止多线程同时执行一段代码,那么就会很影响到程序的执行效率,而volatile关键字在某些情况下性能要优越与Synchronized 但是要注意volatile关键字是无法替代Synchronized。
使用volatile关键字时需要具备两个条件
1.对变量的写操作不依赖于当前值。
2.该变量没有包含在具有其他变量的不变式中,可以被写入volatile变量独立于任何的程序状态,包括变量的当前状态。

package com.openlab.test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//双关锁 double check
public class Singleton {
	
	private volatile static Singleton st = null;

	
	private Singleton() {
		// TODO Auto-generated constructor stub
	}
	
	public static synchronized Singleton getInstance(){
		
		if(st == null){
			
			synchronized (Singleton.class) {
				
				if(st == null){
					st = new Singleton();
					
				}
				
			}
		}
		return st;
		
	}

}

以上是关于深度解析volatile关键字(保证够全面)❤❤的主要内容,如果未能解决你的问题,请参考以下文章

java并发系列-----Java并发:volatile关键字解析

volatile原理解析

volatile关键字解析

volatile关键字解析

volatile关键字解析

解析Java的volatile关键字