问题是最好的导师, 细心是最棒的品质
看一个IDEA
自带的提示
Non-atomic operations on volatile fields
在 volatile
字段上进行了非原子类操作,有两个信息
- 字段
count
是volatile
修饰的 -
count++
是非原子操作
这句话都是告诉我们一个事实
-
volatile
不能保证原子性
为什么呢?常见的可见性又是什么?还有其他特性吗?
volatile 的两个重点
- 可见性
- 禁止指令重排
volatile
的可见性一句话描述就是
-
volatile
修饰的共享变量对其他线程具有可见性
而我们熟悉的static
关键字的作用是什么?
-
static
修饰的变量为全局变量, 对所有线程可见, 可用于线程间的通信
是不是感觉特别像?
一、static
的可见性
看一个例子
public class VolatileDemo {
private static int count = 0;
private static void count() {
count++;
}
public static void main(String[] args){
ExecutorService es = Executors.newCachedThreadPool();
long start = System.nanoTime();
for (int i = 0; i < 100; i++) {
es.execute(VolatileDemo::count);
}
es.shutdown();
while (true) {
if (es.isTerminated()) {
System.out.println("end...");
break;
}
try {
TimeUnit.MILLISECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.nanoTime();
System.out.println("count:" + count);
System.out.println("cost:" + (end - start));
}
}
-
VolatileDemo
类中有一个静态字段count
- 启动一个缓存线程池进行对这个字段进行自增
-
100
次自增
看看输出结果
end...
count:96
cost:114914100
多运行几次,会发现结果都不一样,都是100
或100
以下
count = 0
时, t1
线程与 t2
线程读取 count
值, 然后同步修改为 1
, 再写回内存, 写了两遍 1
, 所以此时count
值不是预期中2
而是1
这里我们可以知道
-
static
修饰的变量确实可以在线程间通信, 对各个线程都是可见的 - 这种可见不能保证线程安全
二、volatile
的可见性
- Java虚拟机规范中
先来看下硬件的存储层次
缓存与主存
再看下执行时间
执行时间
- 寄存器的速度可以理解为
CPU
的速度, 它是离CPU
最近的存储容器
网上找了几张经典的图
Java内存模型 并发访问
下面是一些我自己的理解
- 存储大体上分两种,
主存
是堆内存,工作内存
是栈内存, 属于线程私有 - 对字段操作都需要先从主存读取数据加载进工作内存, 工作内存对这个副本数据进行操作
-
volatile
修饰的变量, 等于在堆中这个变量的内存区域上打了个标, 所有操作都必须从主存中读取, 由MESI
<缓存一致性协议
>实现 -
MESI
规定了四种值类型
状态 | 描述 |
---|---|
M <Modified > |
该缓存行只被缓存在该CPU 的缓存中且被修改过, 需要写回主存, 成功写回后变为E , 失败则为I
|
E <Exclusive > |
该缓存行只被缓存在该CPU 的缓存中且未被修改过, 如果被改动了为M , 其他CPU 从主存读取了此缓存行为S
|
S <Shared > |
该缓存行被多个CPU 所读取, 且数据一致. 如果有改动, 改动的那个线程中的值为M , 其他的仍S , 当改动后的数据被刷入主存中时, 其他所有CPU 中的值均为I
|
I <Invalid > |
该缓存行已经被其他CPU 成功修改主存, 需要重新读取 |
我们稍微修改下这个例子
也就是说每次修改volatile
变量都需要重新读取数据, 相比于static
的线程不安全, 那volatile
完全可以保障线程安全啊, 因为你在修改count
值时需要重新读取数据, 就不会发生重复赋值吧.
让我们修改下上面的例子来验证下
public class VolatileDemo {
private volatile static int count = 0;
private static void count() {
count++;
}
public static void main(String[] args){
ExecutorService es = Executors.newCachedThreadPool();
long start = System.nanoTime();
for (int i = 0; i < 100; i++) {
es.execute(VolatileDemo::count);
}
es.shutdown();
while (true) {
if (es.isTerminated()) {
System.out.println("end...");
break;
}
try {
TimeUnit.MILLISECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.nanoTime();
System.out.println("count:" + count);
System.out.println("cost:" + (end - start));
}
}
- 将
count
加了个volatile
修饰
看看执行结果,发现还是有不为100
的情况
end...
count:99
cost:116959800
这就很难受了....
回到开篇提出的问题, IDEA
给出的提示
Non-atomic operations on volatile fields
- volatile 不能保证原子性
什么是原子性? 就是不可分割
-
i = 1
是原子操作 -
i++
不是,i++
可以分为三步- 从主存读取
i
- 在寄存器中进行加一运算, 自增操作, 此时已经修改工作内存中的值
- 将计算后的值赋给
i
, 也就是刷回主存
- 从主存读取
重点就在于计算结果已经有了, 只差一步赋值, 哪怕此时重新读取主存数据也不会再计算一遍
这里的原子性的问题就暴露出来了, 试想一种场景
t1
线程与t2
线程同时从主存中读取了count = 1
,自增, 这时候两个线程的寄存器中存的计算后的值都是 2
, 然后要写回count
的主存, 假设这时 t1
成功了, 那么主存中的count
就是2
, 然后根据MESI
协议, t2
需要重新从主存读取count
值, 得到的是2
, 再将寄存器中的计算结果2
赋值给count
, 刷回主存, 此时主存中的count
值还是2
, 而不是期望中的3
这就是
volatile
的非原子性
拓展: 分析一下上述场景中两个线程的MESI
状态
操作 | t1 |
t2 |
主存中count 值 |
---|---|---|---|
读取count=1
|
S |
S |
1 |
自增 | M |
M |
1 |
t1 成功刷回主存 |
E |
I |
2 |
t2 从主存重新读取 |
S |
M |
2 |
t2 刷回主存 |
S |
S |
2 |
那咋保证原子性呢?
-
synchronized
关键字 -
atomic
原子类
-
atomic
+volatile
private volatile static AtomicInteger count = new AtomicInteger(0);
private static void count() {
count.incrementAndGet();
}
输出结果会发现全都是100
, 线程安全
end...
count:100
cost:120775900
-
synchronized
+static
private static int count = 0;
private synchronized static void count() {
count++;
}
输出结果也是线程安全的.
总结
-
static
修饰的变量是静态变量, 表示不管对象有多少实例, 只有这一个变量, 强调变量
的唯一性 -
volatile
是基于JMM
与MESI
提出的一种内存一致性解决方案, 强调的是对共享变量的修改是可见的,变量值
是唯一的 -
volatile
可以保证对其所修饰的共享变量的原子性操作是线程安全的
网友评论