资讯专栏INFORMATION COLUMN

Java 双重加锁单例与 java 内存重排序特性

HackerShell / 829人阅读

摘要:关于对于重排序的讲解,强烈推荐阅读程晓明写的深入理解内存模型二重排序。语义语义单线程下,为了优化可以对操作进行重排序。编译器和处理器为单个线程实现了语义,但对于多线程并不实现语义。双重加载的单例模式分析即双重检查加锁。

版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009231182

1. 引言

在开始分析双重加锁单例代码之前,我们需要先理解 java 内存模式的重排序和无序写入特性。

2. Java 内存模型——重排序

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。

同样 Java 为了实现这一目标,在它的编译和处理时会对代码进行重新排序,从而达到更高的并行度提升程序性能。

Java 在进行重排序操作时会遵守数据依赖性,即编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

关于对于重排序的讲解,强烈推荐阅读程晓明写的《深入理解Java内存模型(二)——重排序》。

2.1 as-if-serial 语义

as-if-serial 语义
: 单线程下,为了优化可以对操作进行重排序

Java 编译器和处理器为单个线程实现了 as-if-serial 语义,但对于多线程并不实现 as-if-serial 语义。

2.2 无序写入

若程序定义的变量之间没有依赖关系,那么这两个变量在 JVM 中的加载顺序是不确定的。

3. 单例模式

单例模式带来的好处:

方便共享通用的资源。

避免频繁操作共享资源所带来的性能消耗。

而我们已单例模式有有饿汉式(static 变量,在类加载时就进行初始化一次)懒汉式(在使用到时才初始化一次)两种,考虑到对于始初化单例类的开销较大,往往我们需要创建单例是懒加载的,即在程序使用到单例时才创建,从而可以避免创建单例时拖慢程序的启动速度。

所以对于使用单例模式有两个要求:(1)懒加载。(2)多线程安全

3.1 双重加载的单例模式分析

DCL(double-checked locking) 即双重检查加锁。因为 DCL 模式的单例是懒加载的,所以这往往也是在许多项目中最容易见到的单例模式写法。但是这种方式创建的单例,是多线程安全的吗?

对于双重检查加载的单例代码:

package com.wuxianjiezh.demo.threadpool;

public class Singleton {

    private static Singleton instance;

    // 私有化的构造方法,保证外部的类不能通过构造器来实例化
    private Singleton() {
    }

    // 双重检查加锁来获取对象单例
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

假设有二个线程要获取上面的单例,当其中 线程一 进入同步块执行到 instance = new Singleton(); 时,线程二 来到了 外的第一个 null 判断。注意这里,线程一 在执行 instance = new Singleton(); 这段代码时有以下几个步骤,其中执行是无序的(无序写入),可能出现下面这种情况:

// 1. 为 Singleton 对象分配内存
memory = allocate();
// 2. 注意现在 instance 是非空的,但还没初始化
instance = memory;
// 3. 调用 Singleton 的构造函数,传递 instance
ctorSingleton(instance);

当在执行到 instance = memory; 时,线程二 进入了第一次的 null 判断,此才 线程二 判断 instance 不为 null,返回了 instance,但此时返回的不是单例的实例对象,而是内存对象。

3.2 单例模式推荐写法

使用静态内部类:

package com.wuxianjiezh.demo.threadpool;

public class Singleton {

    // 私有化的构造方法,保证外部的类不能通过构造器来实例化
    private Singleton() {
    }

    // 静态内部类只会被加载一次
    // 内部类 SingletonHolder 只有在 getInstance() 方法第一次调用的时候才会被加载(实现了lazy)
    // 而且其加载过程是线程安全的(多线程安全)
    private static class SingletonHolder {
        // 单例变量

        // 常规写法
        // private static final Singleton instance = new Singleton();
        
        // 假设单例对象构造方法会抛出异常时的写法
        private static final Singleton instance;

        static {
            instance = new Singleton();
        }
    }

    // 获取单例对象实例
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69943.html

相关文章

  • Java单例模式实现

    摘要:所以,在版本前,双重检查锁形式的单例模式是无法保证线程安全的。 单例模式可能是代码最少的模式了,但是少不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对Java中常见的单例模式写法做了一个总结,如有错漏之处,恳请读者指正。 饿汉法 顾名思义,饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。代码如下: public class Singleton...

    jaysun 评论0 收藏0
  • 长文慎入-探索Java并发编程与高并发解决方案

    摘要:所有示例代码请见下载于基本概念并发同时拥有两个或者多个线程,如果程序在单核处理器上运行多个线程将交替地换入或者换出内存这些线程是同时存在的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上此时,程序中的每个线程都 所有示例代码,请见/下载于 https://github.com/Wasabi1234... showImg(https://upload-images.jians...

    SimpleTriangle 评论0 收藏0
  • 单例模式你会几种写法?

    摘要:使用静态类体现的是基于对象,而使用单例设计模式体现的是面向对象。二编写单例模式的代码编写单例模式的代码其实很简单,就分了三步将构造函数私有化在类的内部创建实例提供获取唯一实例的方法饿汉式根据上面的步骤,我们就可以轻松完成创建单例对象了。 前言 只有光头才能变强 回顾前面: 给女朋友讲解什么是代理模式 包装模式就是这么简单啦 本来打算没那么快更新的,这阵子在刷Spring的书籍。在看...

    solocoder 评论0 收藏0
  • 并发编程的艺术

    摘要:假设不发生编译器重排和指令重排,线程修改了的值,但是修改以后,的值可能还没有写回到主存中,那么线程得到就是很自然的事了。同理,线程对于的赋值操作也可能没有及时刷新到主存中。线程的最后操作与线程发现线程已经结束同步。 很久没更新文章了,对隔三差五过来刷更新的读者说声抱歉。 关于 Java 并发也算是写了好几篇文章了,本文将介绍一些比较基础的内容,注意,阅读本文需要一定的并发基础。 本文的...

    curlyCheng 评论0 收藏0

发表评论

0条评论

HackerShell

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<