在CPU与主内存之间存在三级缓存,CPU取数据不是直接从主内存拿取,而是从L1(一级缓存读取到CPU内核),不同的CPU厂商协议不同,java要想实现跨平台,就不得不引入JMM,从而实现对于不同硬件带来的差异进行统一处理,实现跨平台,整体模型如下:
可见性问题是基于CPU位置出现的,由于CPU处理速度太快,相对来说每次去主内存取数据就太慢了,因此CPU提供了L1、L2、L3三级缓存,每次从主内存取完数,将数据存储在三级缓存上,后续CPU使用时直接在三级缓存上拿取,效率得到大幅度提升。
问题:
现在的CPU都是多核的,每个线程的工作内存都是独立的,每个线程只会修改自己的工作内存,如果没有及时同步到主内存,就会导致数据不一致的问题。
示例代码:
// 运行下方代码会发现t1线程并不因为主线程修改flag而结束,此时便出现了可见性问题
// 可见性问题模型如下图所示
private static boolean flag =true;
public static void main(String[] args){
Thread t1 = new Thread(()->{
while(flag){
// .....todo
}
System.out.println("t1线程结束");
})
t1.start();
t1.sleep();
flag = false;
System.out.println("主线程修改flag为false");
}
解决可见性的方式
1 使用volatile关键字修饰成员变量
-
相当于告诉CPU对当前属性的操作不允许使用三级缓存,必须去主内存获取
-
volatile被写:写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
-
volatile被读:读一个volatile变量,JMM会将CPU缓存中的变量设置为无效,必须去主内存重新读取共享变量
加了volatile修饰的属性,会在转为汇编指令后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:
-
将当前处理器缓存行的数据写回到主内存
-
这个写回的数据,在其他CPU内核的缓存中直接无效
示例代码:
// 解决了可见性问题
private volatile static boolean flag =true;
public static void main(String[] args){
Thread t1 = new Thread(()->{
while(flag){
// .....todo
}
System.out.println("t1线程结束");
})
t1.start();
t1.sleep();
flag = false;
System.out.println("主线程修改flag为false");
}
2 synchronized关键字
synchronized的同步代码块或同步方法,必须在获取锁资源后,才会将内部涉及的变量从CPU缓存中移除,并去主内存拿取,而且在释放锁资源后立即将CPU缓存写回到主内存中。
如下正确代码示例:t1线程正确结束
// 解决了可见性问题
private static boolean flag =true;
public static void main(String[] args){
Thread t1 = new Thread(()->{
while(flag){
synchronized(this){
// .....todo
}
}
System.out.println("t1线程结束");
})
t1.start();
t1.sleep();
flag = false;
System.out.println("主线程修改flag为false");
}
错误代码示例如下:t1不会结束,因为synchronized在获取锁的时候从主内存获取flag,此时如果主线程还未修改flag标记,则获取到flag=true,synchronized获取锁读取到的是true,只有在内部代码执行完毕后释放锁了,才会去同步主内存,这就导致无法获取主线程修改后的flag值。
// 错误示例
private static boolean flag =true;
public static void main(String[] args){
Thread t1 = new Thread(()->{
synchronized(this){
while(flag){
// .....todo
}
}
System.out.println("t1线程结束");
})
t1.start();
t1.sleep();
flag = false;
System.out.println("主线程修改flag为false");
}
3 lock锁
基于volitale实现的,Lock锁内部在进行加锁或释放锁时,会对一个由volatile修饰的state属性进行加减操作(可在ReentranLock的lock源码中查看),从而实现可见性
示例代码如下:
private static boolean flag =true;
// 构造一个锁
private static Lock lock = new ReentranLock();
public static void main(String[] args){
Thread t1 = new Thread(()->{
synchronized(this){
while(flag){
lock.lock();
try{
// .....todo
}finally{
lock.unLock();
}
}
}
System.out.println("t1线程结束");
})
t1.start();
t1.sleep();
flag = false;
System.out.println("主线程修改flag为false");
}
4 final
final修饰的属性在运行期间时不允许被修改的,这样就间接的保证了可见性,并不需要每次从主内存中读取,并且与volatile不能同时使用,因为final与volatile从内存语言上是互斥的,volatile的性能要差一些,因为每次都要去主内存读写。