volatile3个特点:
1.强制刷新主内存
2.不保证原子性
3.防止指令重排序
这3点之前我也学习过,我也认为自己搞懂了,但是在看了JVM后我对其原理产生了质疑,最后终于理清楚了,特此记录。
1 public class RunThread extends Thread { 2 3 private boolean isRunning = true; 4 5 public boolean isRunning() { 6 return isRunning; 7 } 8 9 public void setRunning(boolean isRunning) { 10 this.isRunning = isRunning; 11 } 12 13 @Override 14 public void run() { 15 System.out.println("进入到run方法中了"); 16 while (isRunning == true) { 17 } 18 System.out.println("线程执行完成了"); 19 } 20 } 21 22 public class Run { 23 public static void main(String[] args) { 24 try { 25 RunThread thread = new RunThread(); 26 thread.start(); 27 Thread.sleep(1000); 28 thread.setRunning(false); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 } 33 }
还是这个例子,while的条件中,isRunning每次取的都是拷贝副本
如果你加以volatile关键字,他的查询,就会转移到主内存进行一个查询
所以volatile在这里的作用是:禁止“读取”相关的一系列字节码操作,是从本地内存读的。
也就是说,这一串字节码操作,你都得重新走一遍。原来的是读进来作为拷贝变量以后,还有可能读取这个拷贝变量。用了volatile关键字以后,就禁止了读取拷贝变量。
对于不能防止原子自增:
这是普通变量的一个情况
如果加了volatile关键字,你对于该变量的操作,另外的所有线程都能感知到。也就是说这个变量变成了实时数据。但是问题在于,虽然这很好的做到了,你在读取这个变量的时候,可以确保他是实时的。但是i=i+1这个操作,你读取i,确实是实时的, 但是i+1后不会立刻进行一个赋值,也就是说,你使得i+1了,但是你没有进行一个赋值操作,其他线程又何来感知i的值的变化呢?这就是问题最关键的原因,i+1和赋值之间存在一定的时间差。假设这样一个场景:10个线程同时加了1,虽然是多线程,但是i的赋值总有一个先后,哪怕你赋值成功了,其他线程也无从得知,因为问题已经变成了,10个值同时去更新i。他不在乎你i之前是多少。这样讲有点极端,但是事实就是如此。
AutoInteger是如何做到原子自增的呢?
这里就是用到了CAS,乐观锁,当若干线程更新一个值的时候,同时只有一个线程可以更新,而其他线程就会更新失败。更新失败怎么办呢?他们就会一直等待,重新开始,直到成功为止。
那么CAS代码是如何实现的?
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这个compareAndSwapInt你可以认为他就是CAS了,他是native方法。我们是通过它来实现原子自增的。
最后,指令重排序,设定内存屏障:
这个我了解不深,目前也不想深入,简单举个知乎的例子https://www.zhihu.com/question/40885211?sort=created
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于3是volatile,而且是写,所以编译器在voaltile写操作之前插入一个release barrier(相当于loadstore+storestore)实现。