waitset
- 所有的对象都会有一个waitset,用来存放调用了该对象wait方法之后进入block状态的线程。
- 线程被notify之后,不一定立即得到执行。
- 线程从wait set中被唤醒的顺序不一定是FIFO.
- 线程被唤醒后,必须重新获取锁。
附加:要notify别人的线程也要获取这个锁。
volatile例子
- 例子中并没有使用锁,只对INIT_VALUE使用了volatile
public class VolatileTest {
private static volatile int INIT_VALUE = 0;
private final static int MAX_LIMIT = 9;
public static void main(String[] args) {
new Thread("Reader"){
@Override
public void run() {
int localValue = INIT_VALUE;
while ( localValue < MAX_LIMIT) {
if (localValue != INIT_VALUE) {
System.out.printf("The value updated to [%d]\n",INIT_VALUE);
localValue = INIT_VALUE;
}
}
}
}.start();
new Thread("Updater"){
@Override
public void run() {
int localValue = INIT_VALUE;
while ( localValue < MAX_LIMIT) {
System.out.printf("Update the value to [%d]\n", ++localValue);
INIT_VALUE = localValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
-
效果
Snipaste_2020-10-03_23-42-59.png -
如果去掉volatile,读线程永远无法感知到
Snipaste_2020-10-03_23-44-34.png -
出现这种现象的原因,在去掉volatile的时候,jvm认为reader线程只有读操作,只需要从CPU缓存中读取就行了。而缓存不同步,所以一直没有感知到新值。(纯读操作才会有这个问题)(如果大家都直接从主存读写,当然不会出现这种问题)(结合下图理解)
-
CPU缓存不同步问题
-
CPU高速缓存不同步两种解决方案
- 给数据总线加锁
- 总线( 数据总线、地址总线、控制总线)
- CPU高速缓存一致性协议(MESI-intel发明的)
- 当CPU写入数据的时候,如果发现该变量被共享(也就是说,在其他cpu中也存在该变量的副本),会发出一个信号,通知其他CPU该变量的缓存无效
-
上面的步骤执行完毕后,当其他事CPU访问该变量时,重新从主内存获取。
Snipaste_2020-10-04_00-52-51.png
原子性,可见性,有序性在java中的保证(在多线程中谈才有意义)
- 原子性
- 对基本数据类型的变量读取和赋值都保证了原子性,要么都成功,要么都失败,这些操作不可被中断。
- 例子 i = 10 (CPU cache = 10, memory = 10)
- a = 10;满足原子性
- b = a; 不满足, 1. 读a, 2. 赋值给b
- c++; 不满足,1. 读c ,2. c+1 3赋值给c
- c = c+1 同上
- 可见性
- 使用volatile关键字保证可见性
- 有序性(java的重排序保证最后结果一致就可以了)
- happens-before原则
- 代码的执行顺序,编写在前面的发生在编写在后面的
- unlock必须发生在lock之后
- volatile修饰的变量,对一个变量的写操作先于对该变量的读操作。
- 传递规则,操作A先于B,B先于C,那么A肯定先于C
- 线程的启动规则,start方法肯定先于线程run
- 线程中断规则,interrupt这个动作,必须发生在捕获该动作之前
- 对象销毁规则,初始化必须发生在finalize之前
- 线程终结规则,所有的操作都发生在线程死亡之前
volatile
- 保证重排序的时候不会把后面的指令放到屏障的前面,也不会把前面的放到后面
- 强制对缓存的修改操作立刻写入主存
- 如果是写操作,他会导致其他CPU中的缓存失效
- 比较上边没加volatile的纯读操作线程出现的问题:如果大家都是只操作自己cache,那么下边的代码应该都是各自连续加自己的50(实际效果却是两条数据断断续续一起完成加到50,中间还带重复的)
public class VolatileTest2 {
private static int INIT_VALUE = 0;
private final static int MAX_LIMIT = 50;
public static void main(String[] args) {
new Thread("adder-1"){
@Override
public void run() {
while (INIT_VALUE < MAX_LIMIT) {
System.out.println("T1 ->" + (++INIT_VALUE));
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread("adder-2"){
@Override
public void run() {
while (INIT_VALUE < MAX_LIMIT) {
System.out.println("T2 ->" + (++INIT_VALUE));
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
- 数据断断续续一起完成加到50的原因是写操作是会将CPU缓存刷回主线程,再从主线程到缓存获取,有写就不会被jvm默认优化。
- 这里给INIT_VALUE加volatile并不能保证上面就是线程安全的,偶尔会发生重复(原因:没有保证原子性。先获取INIT_VALUE,++INIT_VALUE)获取INIT_VALUE是有原子性的,但获取完后释放cpu执行权被别的线程一气呵成完成步骤,造成数据重复。解决办法:用锁。
-
使用场景:
Snipaste_2020-10-04_02-41-21.png
第二点就是单例模式doublecheck中实例加volatile的原因,加了后确保构造函数执行完毕。
网友评论