并发编程的三大特性--可见性

在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的性能要差一些,因为每次都要去主内存读写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值