前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >内存可见性

内存可见性

作者头像
SuperHeroes
发布2018-05-31 13:41:14
7910
发布2018-05-31 13:41:14
举报
文章被收录于专栏:云霄雨霁云霄雨霁云霄雨霁

可见性:当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。如果没有同步,这种情况就无法实现。

下面的代码说明了当多个线程在没有同步的情况下共享数据时出现的错误。在代码中,主线程和读线程都会访问共享变量ready和number。很显然代码看起来会输出42,但事实上肯可能输出0(重排序导致),甚至根本无法终止。这是因为代码中没有足够的同步机制,因此无法保证主线程写入ready和number对读线程是可见的。

public class NoVisibility{
    private static bolean ready;
    private static int number;  

    private static class ReaderThread extends Thread{
        public void run(){
            while(!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args){
        new ReadThread().start();
        number = 42;
        ready = true;
    }
}
失效数据:

当读线程查看ready变量时,可能会得到一个失效的值。除非每次访问变量时都使用同步,否则很可能获得该变量的一个失效值。

下面的代码是很容易出现失效值问题的:如果一个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的值,也可能看不到。

public class MutableInteger{
    private int value;
    public int get(){ return value; }
    public void set(int alue) { this.value = value; }
}

将上面的代码改为线程安全的类,仅对set方法进行同步是不够的,调用get的线程仍然会看到失效值。

public class MutableInteger{
    @GuardedBy("this") private int value;

    public synchronized int get(){ return value; }
    public synchronized void set(int alue) { this.value = value; }
}
非原子的64位操作:

最低安全性:当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是某个线程设置的值,而不是一个随机值。这种安全性被称为最低安全性。

最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(long和double)。Java内存模型要求变量的读取和写入必须是原子操作,但对于非volatile类型的64位数值变量,JVM允许将64位的操作分解为 两个32位的操作。那么如果在多线程中对该变量的读取和写入在不同线程中进行,很可能读取到某个值的高32位和另一个值的低32位。即使不考虑实习失效数据问题,多线程中共享64位类型数据也是不安全的,除非用volatile声明或者加锁。

加锁和可见性:

当线程B执行由锁保护起来的代码时,可以看到线程A之前在同一个同步代码块中所有的操作结果。如果没有同步,那么就无法实现上述保证。

现在可以理解为什么访问某个共享的且可变的变量时要求所有线程在同一个锁上同步,就是为了保证某个线程对变量的修改对其他线程来讲都是可见的。否则一个线程在未持有正确的锁的情况下读取某个变量,那么读到的可能是一个失效值。

Volatile变量:

Volatile变量是一种稍弱的同步机制,用来确认变量的更新操作通知到其他线程。当把变量声明为volatile时,这个变量就是共享的,编译器不会将该变量上的操作与其他内存操作一起重排序。Volatile变量也不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。Volatile变量的正确使用方式包括:

  • 确保它们自身状态的可见性;
  • 确保它们所引用对象的状态的可见性;
  • 标记一些重要的程序生命周期事件的发生。

下面的代码是volatile最经典的用法:检查某个状态标记以判断是否执行某个操作(单例模式):

public class Singleton{
    private volatile static Singleton uniqueInstance;
    private Singleton(){}//构造器定义为私有 
    
    public static Singleton getInstance(){
        if(uniqueInstance==null){ //检查实例,不存在则进入同步块 
            synchronized (Singleton.class){ 
                if(uniqueInstance==null){ //进入同步块后再次检查 
                    uniqueInstance = new Singleton(); 
                } 
            } 
        } 
        return uniqueInstance;
    } 
}

加锁机制既可以保证可见性又可以保证原子性,而volatile变量只能保证可见性。

许多人认为:“volatile变量对所有线程是立即可见的,对volatile变量的修改能立即反映到其他线程中,所以基于volatile变量的运算是线程安全的”。这句话论据部分没错,但其论据并不能得出它的结论。Volatile变量在各个线程中不存在不一致问题(各个线程的工作内存中volatile可以不一致,但由于每次使用都会先刷新,执行引擎看不到不一致的情况),但Java中的运算是非原子性的,导致volatile变量在并发情况下一样是不安全的。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景下,仍然需要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能保证只有单一线程修改变量的值;
  • 变量不需要与其他状态变量共同参与不变约束。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.05.03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 可见性:当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。如果没有同步,这种情况就无法实现。
    • 失效数据:
      • 非原子的64位操作:
        • 加锁和可见性:
          • Volatile变量:
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档