一、什么是可见性?
可见性问题是基于CPU位置出现的,cpu处里速度非常快,相对CPU来说去主内存
获取数据这个事情太慢了,CPU就提供了 L1,L2,L3的三季缓存,每次去主内存拿完
数据后,数据就先存储到三级缓存,然后cpu再去三级缓存取数据,效率肯定会提升;
三级缓存就是是每个线程的工作内存,是相互独立的。
这就带来了一个问题:现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都
是独立的,会告知每个线程做修改时,只修改自己的工作内存,数据没有及时同步到主内存
,从而导致数据不一致的问题
线程运行时数据处里过程如下:
使用下边代码来验证数据可见性的问题,代码如下:
二、解决可见性问题的方式
1、volatile
volatile是一个关键字,用于修饰成员变量
如果属性被volatile修饰,相当于告诉cpu,对于当前属性的操作,不允许使用CPU
缓存(即线程私有内存),必须去操作主内存。
volatile的内存语义:
(1)volatile 属性被写:当写一个volatile变量,JMM会将当前线程的CPU缓存的
数据及时刷新到主内存中
(2)volatile 属性被读:当读一个volatile变量,JMM会将当前线程对应的CPU缓存
设置为无效,必须从主内存读取数据。
其实变量加了volatile就是告诉cpu,对当前变量的读写操作,不允许使用CPU缓存;加
了volatile 的变量会在编译成汇编之后追加一个lock前缀,CPU执行这个指令时,如果
带有lock前缀会做2件事:
(1)将当处理器缓存行的数据写回到主内存
(2)这个写会的数据,在其他的CPU内核的缓存中,直接无效
总结:volatile 就是让CPU每次操作这个数据时,必须立即同步到主内存,以及从主内
存读取数据。
在代码中若先对volatile属性进行操作,则其他属性也是可见性的。
针对上边的代码,采用volatile解决可见性问题实现如下:
/*******************************************************
* 验证线程的可见性问题
* 每个线程都有自己的私有内存,相互独立,线程运行时处里的是自己私有内存的数据
*
* 使用volatile解决内存可见性
*******************************************************/
public class Test02 {
private volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag){
}
System.out.println("子线程 t1 中 flag改变");
});
t1.start();
//预期:flag修改成false后,不影响线程t1的运行
Thread.sleep(100);
//修改flag 的值
flag = false;
System.out.println(" main 线程中修改 flag = false");
}
}
//方案二:在代码中若先对volatile属性进行操作,则其他属性也是可见性的
public class Test02 {
private volatile static int i= 0;
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag){
//todo: 在代码中若先对volatile属性进行操作,则其他属性也是可见性的
i++;
}
System.out.println("子线程 t1 中 flag改变");
});
t1.start();
//预期:flag修改成false后,不影响线程t1的运行
Thread.sleep(100);
//修改flag 的值
flag = false;
System.out.println(" main 线程中修改 flag = false");
}
}
2、synchronized
synchronized也是可以解决可见性问题。
synchronized内存语义:
如果涉及到了synchronized 的同步代码块或者同步方法,获取资源之后,将内部
涉及到的变量从CPU缓存(线程私有内存)中移除,必须重新去主内存中取数据;
而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存中。
使用 synchronized 解决内存可见性 示例代码如下:
/*******************************************************
* 验证线程的可见性问题
* 每个线程都有自己的私有内存,相互独立,线程运行时处里的是自己私有内存的数据
*
* 使用synchronized解决内存可见性
*******************************************************/
public class Test03 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag){
/*
* 问题:为什么这里synchronized 放在while循环里边,不放在外边?
* 因为线程 在获取到 synchronized 锁之后才会从主内存拿数据,若把synchronized 放在while外边
* 则只会从主内存拿一次数据,后边不能监听到变量 flag 变化
*/
synchronized (Test03.class){
//todo
}
}
System.out.println("子线程 t1 中 flag改变");
});
t1.start();
//预期:flag修改成false后,不影响线程t1的运行
Thread.sleep(100);
//修改flag 的值
flag = false;
System.out.println(" main 线程中修改 flag = false");
}
}
3、Lock
Lock锁保证可见性的方式和synchronized 完全不同,synchronized是基于内存语义在获取
锁和释放锁对CPU缓存做一个同步到主内存的操作。
lock 锁是基于volatile实现的,lock 锁内部进行加锁和释放锁时,会对一个volatile修饰的属
state做加减操作。
如果对volatile属性进行写操作,CPU会执行带有lock前缀的指令,会将CPU缓存的数据立
即同步到主内存,同时也会将其他非volatile属性页一起同步到主内存。还会将其他CPU缓
存行 中这个volatile数据设置为无效,必须从主内存重新拉取。
lock解决可见性示例代码如下:
/*******************************************************
* 验证线程的可见性问题
* 每个线程都有自己的私有内存,相互独立,线程运行时处里的是自己私有内存的数据
*
* 使用lock解决内存可见性
*******************************************************/
public class Test04 {
private static boolean flag = true;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag){
lock.lock();
try {
//。。。。。
}finally {
lock.unlock();
}
}
System.out.println("子线程 t1 中 flag改变");
});
t1.start();
//预期:flag修改成false后,不影响线程t1的运行
Thread.sleep(100);
//修改flag 的值
flag = false;
System.out.println(" main 线程中修改 flag = false");
}
}
4、final
final本质上说并不能像synchronized和volatile 那种形式保证可见性,final修饰的属性在
运行期间是不允许修改的,这样一来就间接保证了可见性。
final与volatile不允许同时修饰一个属性,final修饰的属性不允许被修改,而volatile保证
每次从主内存读取数据,并且volatile会影响一定性能,就不需要同时修饰。
final与volatile同时修饰属性会报错,如下图所示: