学习笔记

Java并发编程实战-15高级主题_Java内存模型
Publish: 2018/7/23   

什么是内存模型,为什么需要它

平台的内存模型

为了使Java开发人员无须关系不同架构上内存模型之间的差异,Java还提供了自己的内存模型,并且JVM通过在适当的位置上插入内存栅栏来屏蔽JMM与底层平台内存模型之间的差异。

重排序

如果没有同步,那么推断处执行顺序将是非常困难的,而要确保在程序中正确地使用同步却是非常容易的。同步将限制编译器、运行时和硬件对内存操作重排序的方式,从而在实施重排序时不会破坏JVM提供的可见性保证。

Java内存模型简介

Java内存模型通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。要想保证执行操作B的线程看的操作A的结果(无论A和B是否在一个线程中执行),那么在A和B之间必须满足Happens-Before关系。如果两个操作之间缺乏Happens-Before关系,那么JVM可以对它们任意地重排序。

Happens-Before的规则包括:

借助同步

基于BlockingQueue实现的安全发布就是一种”借助”。如果一个线程将对象置入队列并且另一个线程随后获取这个对象,那么这就是一种安全发布,因为在BlockingQueue的实现中包含有足够的内部同步来确保入列操作在出列操作之前执行。

在类库中提供的其他Happpens-Before排序包括:

发布

不安全发布

除了不可变对象以外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行。

安全的发布

Happens-Before安全发布提供了更强可见性与顺序保证。如果将X从A安全的发布到B,那么这种安全发布可以保证X状态的可见性,但无法保证A访问的其他变量的状态可见性。然而,如果将X置入队列的操作在线程B从队列中获取X的操作之前执行,那么B不仅能看到A留下的X状态(假设线程A或其他线程都没有对X再进行修改),而且还能看到A再移交X之前所做的任何操作(JMM确保B至少可以看到A写入的最新值,而对于随后写入的值,B可能看到也可能看不到)。

安全初始化模式

    @ThreadSafe
    public class SafeLazyInitialization{
        private static Resource resource;

        //可以修復UnsafeLazyInitalization中的问题
        public synchronized static Resource getInstance(){
            if(resource == null)
                resource=new Resource();
            return resource;
        }
    }

在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显示的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。

    //提前初始化
    @ThreadSafe
    public class EagerInitialization{
        private static Resource resource = new Resource();

        public static Resource getResource(){return resource;}
    }

    //延迟初始化占位类模式 
    @ThreadSafe
    public class ResourceFactory{
        private static class ResourceHolder{
            public static Resource resource = new Resource();
        }

        public static Resource getResource(){
            return ResourceHolder.resource();
        }
    }

“延迟初始化占位类模式”中使用了一个专门的类来初始化Resource。JVM将推迟ResourceHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Resource,因此不需要额外的同步。

双重检测锁DCL

当没有同步的情况下读取一个共享对象时,可能发生的最糟糕事情只是看到一个失效值(在这种情况下是一个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而,实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态值确是失效的,这意味着线程可以看到对象处于无效或错误的状态。

在JVM5.0及以后的版本种,如果把静态成员声明为volatile类型,那么就能启用DCL,并且这种方式对性能的影响很小。

初始化过程种的安全性

初始化安全性将确保,对于正确构造的对象,所有线程都能看到由构造函数为对象给各个final域设置的正确值,而不管采用何种方式来发布对象。而且,对于可以通过被正确构造对象中某个final域到达的任意数量(例如某个final数组中的元素,或者由一个final域引用的HashMap的内容)将同样对于其他线程是可见的。

    //不可变对象的初始化安全性
    @ThreadSafe
    public class SafeStates{
        private final Map states;

        public SafeStates(){
            states=new HashMap();
            states.put("alaska","ak");
            states.put("alabama","al");
            ...
        }

        public String getAbbreviation(String s){
            return states.get(s);
        }
    }

初始化安全性只能保证通过final域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性。



← Java并发编程实战-6结构化并发应用程序_执行任务 Java并发编程实战-14高级主题_原子变量与非阻塞同步机制 →

Powered by Hexo, Theme designs by @hpcslag.
Style-Framework Tocas-UI designs by @yamioldmel