volatile
Java中关键字,保证所声明数值,对象引用对其他线程的可见性;
我之前一直认为其是保证数据操作原子性(脑子犯糊涂了)
在深入Java虚拟机中,对volatile关键字的解释如下:
- 1.关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制
- 2.当一个变量声明为volatile,保证此变量对所有线程的可见性(这里的可见性是指当一个线程修改了变量值,新值对于其他线程是立即得知的)
为什么说变量声明为volatile,新值对于其他线程是立即得知的?
因为线程对变量的操作直接在主内存中进行,而不需要将变量拷贝至工作内存中操作后再写入到主内存;
原子性,可见性程度上分析volatile
举个例子说明:
两个线程A,B对int型变量i进行操作(解释volatile如何工作);
如下图:
1.A,B对i都进行(i=1)操作
因为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();
}
}
执行效果如下图:
分析图如下:
产生了一个问题就是:
当线程threadN执行i++操作,假设这时候i值为1,操作之后i变为2,此时threadW执行i++操作,i操作之后变为3;而此时返回给线程的i值却是2;
为什么会产生这个问题?
我们通过javap -verbose命令查看VolatileTest字节码来进行分析(仅仅查看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的状态都是一致的;
- 并且不需要加锁同步,省去了锁开销
这就是我的简单分析;
详情参考: