美文网首页
Java内存模型

Java内存模型

作者: Shmily鱼 | 来源:发表于2018-01-24 20:19 被阅读0次
Java内存的可见性

Java内存模型(Java Memory Model)描述线程之间如何通过内存(memory)来进行交互。


image.png

Java内存模型:所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)

内存可见性:一个线程对共享变量值的修改,能够及时地被其他线程观察到。

JVM关于synchronized的两条规定:
线程加锁时,将清空工作内存中相关共享变量的值,从而使用共享变量时需要从主存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)
线程解锁前,必须把共享变量的最新值刷新到主内存中,So线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

实现了可见性,才能保证线程对共享的资源改变能被其他线程观察到。

线程执行互斥代码的过程如下:
1.获得互斥锁
2.清空工作内存中相关的共享变量(获取哪个对象锁,清空的是对应的共享变量 eg:获取了对象A的锁,那么对象A的数据,都是被清空了)
3.从主内存拷贝变量的最新副本到工作内存
4.执行代码
5.将更改后的共享变量的值刷新到主内存中
6.释放互斥锁

若没有实现可见性,即使一个线程已经把变量修改刷回主存,但是另一个线程的值还是本地线程的,值还是会不同步。以此可以看出双重检查上锁的单例模式中,重复判断的必要性。

public static CommonDialog getInstance() {
      if (null == instance) {
          //这个类的所有对象调用此方法,都会受到锁的影响
          synchronized (CommonDialog。class) {
          //其他线程可能获取过锁,并且实例化了instance 而当前线程一直被阻塞到此处           
                    if(null == instance) {
                           instance = new CommonDialog();
                }
          }
      }
    return instance;
}

原子性

从主存读取、修改、写回主存, 这三个操作合成不可分割的原子操作,在这三个操作过程中,其他线程不能读写。

虽然synchronized实现了可见性与原子性,但是以目前的双重检查上锁的单例模式,还是存在问题的:并不能保证它在单处理器或多处理器上能够正常运行,此原因归咎于java的内存模型的重排序特性。

重排序

代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化

实现了以下规则

as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致。
单线程遵守这个原则,多线程不遵守。

重排序不会给单线程带来内存可见性问题, 但多线程中程序交错执行时,重排序可能会造成一些问题
主要在于singleton = new Singleton()这类语句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1.给singleton 分配内存
2.调用 singleton 的构造函数来初始化成员变量,形成实例
3.将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化,也就是说第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-2-3也可能是1-3-2
如果是后者,则在 3 执行完毕、2 未执行之前,线程①释放了锁,线程②抢占了锁,而 instance此时 已经是非 null 了(但却没有初始化),所以线程②会直接返回 instance,然后使用,然后顺理成章地报错。

解决方案是:volatile

volatile这个关键字修饰的变量,就是可以保证内存可见性的。
一旦某个共享变量(类的成员变量,类的静态成员变量)被volatile修饰,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作的可见性,即一个线程修改了某个变量,这个新的值就会被其他线程立即可见。

2)禁止进行指令重排序。
对于语义1)使用volatile关键字会强制将修改的值立即写入主存
对于语义2)原理:通过加入内存屏障和禁止重排序优化来实现的。
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
对volatile变量执行读操作时,会在读操作前加入一条load屏障指令

so 单例代码再次修改:

 private static volatile Singleton Instance=null;
     public Singleton() {
     }

     public static Singleton getInstance(){
       if(Instance==null){
           synchronized (Singleton.class) {
               if (Instance == null) {
                   Instance = new Singleton();
               }
           }
       }
        return Instance;
    }

终于没有问题了,这却不是目前最好的单例方式
目前最推崇的单例模式如下:

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton () { }
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本,完美!

最后,我们在回顾下本篇提到的java内存模型的一些特性:
1.可见性:一个线程对共享变量值的修改,能够及时地被其他线程观察到。(synchronized 和 volatile 实现了可见性)

2.原子性:从主存读取、修改、写回主存, 这三个操作合成不可分割的原子操作,在这三个操作过程中,其他线程不能读写。(synchronized实现了原子性)

3.重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化。(volatile禁止重排序)
所以二者的结合能解决的问题是:可见性,原子性,重排序。eg:双重检查锁的单例模式。
本篇是浅显的了解下Java内存模型以及其特性,其实更像是对synchronized的扩展知识。关于Java内存模型还有很多地方等着大家去探索。
例如参考文献:Double-checked locking and the Singleton pattern
喜欢学习,乐于分享,不麻烦的话,给个❤鼓励下吧!

相关文章

网友评论

      本文标题:Java内存模型

      本文链接:https://www.haomeiwen.com/subject/vpdeaxtx.html