每个线程都有自己的一个工作内存,工作内存中存储着主内存变量的副本。当工作内存的变量发生改变后,会重新写回到主内存。
内存间的相互操作
lock 将对象变成线程独占的状态
unlock 将线程独占状态的对象的锁释放出来
read 从主内存读数据
load 将从主内存读取的数据写入工作内存
use 工作内存使用对象
assign 对工作内存中的对象进行赋值
store 将工作内存中的对象传送到主内存当中
write 将对象写入主内存当中,并覆盖旧值
Volatile语义
Volatile的第一个语义就是保证此线程的可见性,一个线程对此变量的更改其他线程是立即可知的。也就是说 assign,store,write这三个操作是原子的,中间不会中断,会马上同步回主存,就好像直接操作主存一样,并通过缓存一致性通知其他缓存中的副本过期。普通变量可能会在assign,store,write之间插入其他操作,导致更改后的数据无法马上同步回主存,其他线程读取的可能是过期的旧数据。
Cpu与内存数据读取
在多核cup时代,不同线程可能在不同cup的核心中执行,由于cup处理速度和内存的读取速度大概相差大约一百倍,为了让cpu性能不浪费,cpu中做了一个高速缓存,cpu在处理的时候会把一批可能用到的数据载入到缓存中,等执行完毕再写回内存。
共享内存的变量与线程栈中的变量副本有可能在主存中,也有可能在cpu缓存中或者cpu寄存器中。
Q:为什么volatile不是线程安全的?
public class Counter {
public volatile static int count = 0;
public static void inc() {
//这里延迟1毫秒,使得结果明显
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
count++;
}
public static void main(String[] args) {
//同时启动1000个线程,去进行i++计算,看看实际结果
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
}
}).start();
}
//这里每次运行的值都有可能不同,可能为1000
System.out.println("运行结果:Counter.count=" + Counter.count);
}
}
运行结果还是没有我们期望的1000,下面我们分析一下原因
count++编译后最终并非一个原子操作,它由几个指令一起组合实现。
count++被分割成5个步骤(当然这个并不是确切的指令执行步骤),这5步不具有原子性,假如在完成过程中,其他线程就去读了主存的count变量,那明显导致了一个脏读现象。
read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容
其中use and assign 可以多次出现
这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的
例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值
在线程1对count进行加1之后,会write到主内存中,主内存中的count变量就会变为6
线程2由于已经进行read,load操作,没有及时更新主内存数据,在进行加1运算之后,也会更新主内存count的变量值为6
导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。
导致这个问题的原因其实是因为volatile不具备锁操作,要解决此问题其实不难,就是将这五步变为原子操作,即保证线程一完成之前不能有其他线程读取count变量,对count变量加一个互斥锁即可达到,线程一在执行第①步前对count加锁,其他线程无法对count进行访问,线程一执行完第⑤步后释放锁,此刻开始才允许其他线程获取此变量。
变量的自增操作 i++,分三个步骤:
①从内存中读取出变量 i 的值
②将 i 的值加1
③将 加1 后的值写回内存
这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行,写回去也可能把别的线程写入主内存的数据覆盖掉。
网友评论