关于volatile关键字

Jul 29, 2016


volatile

Java中关键字,保证所声明数值,对象引用对其他线程的可见性;

我之前一直认为其是保证数据操作原子性(脑子犯糊涂了)

在深入Java虚拟机中,对volatile关键字的解释如下:

  • 1.关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制
  • 2.当一个变量声明为volatile,保证此变量对所有线程的可见性(这里的可见性是指当一个线程修改了变量值,新值对于其他线程是立即得知的)

为什么说变量声明为volatile,新值对于其他线程是立即得知的?

因为线程对变量的操作直接在主内存中进行,而不需要将变量拷贝至工作内存中操作后再写入到主内存;

原子性,可见性程度上分析volatile

举个例子说明:

两个线程A,B对int型变量i进行操作(解释volatile如何工作);

如下图:

1.A,B对i都进行(i=1)操作

volatile

因为i=1操作本身就是原子性操作

而volatile关键字声明的变量i,直接是在主内存中对i进行操作,所以新值对其他线程是立即得知的;

所以这里我们可以知道,volatile关键字声明的变量满足可见性

下面我们以不是原子性操作的(i++)来分析,volatile是否满足原子性;

2.A,B对i都进行(i++)操作

我们先写个程序来测试:

public class VolatileTest {
	//使用volatile关键字声明变量i
	public volatile int i = 0;

	//定义i++操作;
	public void increaseI() {

		i++;			

	}
	//测试主方法
	public static void main(String [] args) {
		
		VolatileTest test = new VolatileTest();
		//利用jdk8中的lambda表达式创建Runable接口
		Runable runable = () -> {
		   
		   
		   while(test.i < 1000) {
			
			//获取当前线程名称
			String currName = 
				Thread.currentThread().getName();
		         
			System.out.println(currName + " : " + test.i);
			
			//执行i++操作
			test.increaseI();
			
			//输出i的值
			System.out.println(currName + " : " + test.i);
			
			//为了输出的效果,线程休眠2秒
			try{
			
			    Thread.sleep(2000);
				
			}catch(InterruptException e) {

				e.printStackTrace();

			}


		   }

		}
		
		//创建两个线程
		Thread threadN = new Thread(thread);
		Thread threadW = new Thread(thread);
		
		//启动线程
		threadN.start();
		threadW.start();

	}
}

执行效果如下图:

volatileThread

分析图如下:

volatilei

产生了一个问题就是:

当线程threadN执行i++操作,假设这时候i值为1,操作之后i变为2,此时threadW执行i++操作,i操作之后变为3;而此时返回给线程的i值却是2;

为什么会产生这个问题?

我们通过javap -verbose命令查看VolatileTest字节码来进行分析(仅仅查看increaseI方法):

increaseI

我们可以知道:

i++操作主要由四条指令完成:

  • getfield
  • iconst_1
  • iadd
  • putfield

当getfield指令将i的值取出来,执行iconst_1,iadd操作(自增操作)的时候,其他线程正好对i进行操作,这时候i的值成为过期的期望值,所以最后执行putfield把过期的数据同步回主内存中,之后所有线程拿到的都是过期数据;

所以现在可以得出结论:

volatile只能保证可见性,不能确保原子性

所以volatile声明的变量不一定是线程安全的

如何使用volatile来完成真正的同步呢?

1.利用锁机制

比如i++操作,我们可以对上例中increaseI方法中i++进行同步操作:

  • 使用synchronized关键词加同步块
  • 创建锁对象–> Lock lock = new ReentrantLock()进行lock和unlock操作

在这顺便提一下synchronized与reentrantlock的区别(在这提一点):

  • synchronized采用悲观锁机制(即独占锁)
  • reentrantlock采用乐观锁机制(利用CAS原理,下文会提到)

具体详情请参考深入研究 Java Synchronize 和 Lock 的区别与用法

由于volatile提供的本身就是轻量级同步机制,如果使用锁的话:

  • 第一,就不必使用volatile,得不偿失;
  • 第二,使用锁的话,性能上会有所损失;

2.CAS

CAS的定义:

  • 有三个操作数:V代表内存值,current代表旧的期望值,update代表更新值;
  • 当V与current相等的情况下,才对V值进行更新(true),否则什么都不做(false)

在java.util.concurrent.atomic.*包下AtomicXXX类都是通过volatile加上CAS机制完成同步操作;

我们拿AtomicInteger类来进行分析,看看是如何处理的:

AtomicInteger类作用就是int包装类:

private volatile int value;

private static final long valueOffset;

static {
	try {
		valueOffset = unsafe.objectFieldOffset
			(AtomicInteger.class.getDeclaredField("value"));
	} catch (Exception ex) {
	 	throw new Error(ex); 
	}
}

利用volatile声明可以保证value的可见性

一直不太理解valueOffSet的意思;

我的理解就是value在内存中的位置,详情参考unsafe.objectFieldOffset的理解

类中getAndIncrement()方法代表value的自增操作:

//代表value++操作
public final int getAndIncrement() {
	//valueOffSet实际为value值的位置
	return unsafe.getAndAddInt(this, valueOffSet, 1);
}

在网上找到UnSafe类,但是一直纳闷怎么没有getAndAddInt方法;

原来jdk8已经对这个类进行了修改详见UnSafe源码

//进行o自增操作(o实际为AtomicInteger,AtomicLong...)
public final int getAndAddInt(Object o, long offset, int delta) {
	int v;
	do {
		//获取o对象中的value值
		v = getIntVolatile(o, offset);
	} while (!compareAndSwapInt(o, offset, v, v + delta));

	return v;
}

//获取o对象中的value值		
public native int getIntVolatile(Object o, long offset);
//CAS算法
public final native boolean compareAndSwapLong(Object o, long offset,
						long expected, long x);

那核心就是compareAndSwapInt方法;

所以AtomicInteger中的getAndIncrement方法可以这么分析理解:

  • 调用unsafe.getAndAddInt(this, valueOffSet, 1)进行自增操作;
  • unsafe中getAndAddInt方法通过valueOffSet位置,this对象获取对象中value值
  • 循环判断compareAndSwapInt(this, valueOffSet, v, v+1)条件是否成立
  • compareAndSwapInt采用的就是CAS算法:
    • 具有3个操作数:内存值 旧的期望值 更新值
    • 当内存值与旧的期望值相等情况下,才对值进行更新(返回true)
    • 否则不进行任何操作,返回false

这样就保证了AtomicInteger中i++的原子操作;

意味着:

  • 当线程A进行i++操作,此时B也进行i++操作,两者操作不会互相影响;
  • A,B对i的状态都是一致的;
  • 并且不需要加锁同步,省去了锁开销

这就是我的简单分析;

详情参考:

源码剖析sun.misc.Unsafe && Compare And Swap(CAS)操作

Unsafe源码

CAS分析

使用UnSafe及反射对内存进行内省

反射性能的优化