一、公共堆栈与线程的私有堆栈
在启动线程时,变量的值是存在于公共堆栈及线程的私有堆栈中。在JVM被设置为-server 模式时为了线程运行的效率,线程一直在私有堆栈中取变量的值,即使有其他线程将变量的值进行了修改,更新的却是公共堆栈中的变量值,私有堆栈中的值不会变,这就导致线程运行存在问题。内存关系如图:
····························
volatile作用
使用volatile 关键字,可以强制的从公共内存中读取值。使用volatile关键字增加了实例变量在多个线程之间的可见性。但是volatile关键字的缺点是不支持原子性。
volatile与synchronized 比较
1)volatile 是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,但是volatile只能修饰变量,而synchronized可以修饰方法,代码块。
2)多线程访问volatile 不会发生阻塞,而synchronized 会发生阻塞。
3)volatile 能保证数据的可见性,但不能保证原子性,而 synchronized可以保证原子性,也可以保证可见性,因为它会将私有内存和公共内存中的数据做同步。
volatile的非原子特性
-
程序的原子性概念:一个操作的过程是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一旦一个原子性的操作开始后,就不会受其他线程的干扰。
-
验证
public class MyThread extends Thread{
// static修饰的变量只被初始化一次,下一次依据上一次结果值
volatile public static int count;
private static void addCount(){
for (int i = 0; i < 100; i++){
count++;
}
System.out.println("count="+count);
}
@Override
public void run(){
addCount();
}
}
public class VolatileRun {
public static void main(String[] args) {
MyThread[] mythreadArr = new MyThread[100];
// 创建 100个实例
for(int i = 0; i < 100; i++){
mythreadArr[i] = new MyThread();
}
for(int i = 0; i < 100; i++){
mythreadArr[i].start();
}
}
}
运行结果:不是10000
··························
关键字volatile 提示线程每次从公共堆栈读取值,而不是从私有内存中读取,这样就保证了数据的同步数据可见性。但是:如果修改实例变量中的数据,比如 i++,也就是i = i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++ 的操作步骤分解如下:
1)从内存中取出 i 的值;
2)计算 i 的值;
3)将 i 的值写到内存中
假如在第2步计算值的时候,另一个线程也修改了 i 的值,那么这时候就会出现脏数据。解决办法就是使用synchronized 关键字。所以说 volatile 本身并不处理数据的原子性,只是强制数据的读写及时影响到主内存中。
- 用图来演示下volatile出现非线程安全的原因。变量在内存中工作的过程。
1)read 和 load 阶段:读取和加载
2)use 和 assign 阶段: 操作和赋值
3)store 和 write 阶段:储存和 写
在多线程环境,use和assign 是多次出现的,但这一操作不是原子性的,在read和load之后,如果主内存的count变量的值修改之后,线程工作内存中的值 由于已经加载,不会产生对应变化,也就是私有内存和公共内存中的变量不同步了,所以计算出的结果和预期结果不一样,也就出现了非线程安全的问题。