资讯专栏INFORMATION COLUMN

java单例模式

刘福 / 2349人阅读

摘要:单例是一个类,我们希望该类的对象在任意高并发多线程的调用下,只被初始化一次例如用于系统环境和词典的加载等,后续线程均直接调用即可。我们首先来讲解几种常见的单例模式和其优缺点吧。更多单例模式可以看这里。线程进入区域,此时还未启动。

写此文初衷源于昨晚线上代码抛出的一个空指针异常。单例是一个类,我们希望该类的对象在任意高并发多线程的调用下,只被初始化一次(例如用于系统环境和词典的加载等),后续线程均直接调用即可。我们首先来讲解几种常见的单例模式和其优缺点吧。

1.懒汉式
// 懒汉式,线程不安全
class SingletonDemo {
    
    // 定义一个私有的静态全局变量来保存该类的唯一实例
    private static SingletonDemo instance;
    
    private SingletonDemo() {
    
    }
    
    public static SingletonDemo getInstance() {
        // 这里可以保证只实例化一次
        if (instance == null) {  //语句(1)
            instance = new SingletonDemo();
        }
        return instance;
    }
}

以上代码很明显不能满足我们的要求。设想有n个线程同时执行语句(1),此时实例还未被初始化,因此均判断为null,于是这n个线程每一个都新建了该类对象。

2.懒汉式改进
// 懒汉式,线程安全,但不高效,因为任何时候只能有一个线程调用getInstance()方法。
class SingletonDemo2 {
    
    private static SingletonDemo2 instance;
    
    private SingletonDemo2() {}
    
    public static synchronized SingletonDemo2 getInstance() { //语句(1)
        if (instance == null) { //区域(1)
            instance = new SingletonDemo2();
        }
        return instance;
    }
}

我们使用synchronized来强制使每个线程串行执行语句(1),因此永远只有第一个线程新建了该类对象。那么这段代码缺点在哪呢?速度慢。假设现在有1000个线程,均在语句(1)处排队,当第一个线程创建新对象后,剩下999个线程仍然需要排队,进入区域(1)判断不为空并返回。

这里懒汉式的意思是:要用的时候才去new。区别于接下来要讲的:

3.饿汉式
/**
 * 饿汉式,单例的实例被声明成static和final,在第一次加载到内存中时会初始化。
 * 
 * 缺点:
 * 不是一种懒加载(lazy initlalization),在一些场景中无法使用:
 * 譬如Singleton实例的创建时以来参数或者配置文件的,在getInstance()之前必须调用
 */
class SingletonDemo5 {
    // 类加载时就初始化
    private static final SingletonDemo5 instance = new SingletonDemo5();
    
    private SingletonDemo5() {}
    
    public static SingletonDemo5 getInstance() {
        return instance;
    }
}

这段代码利用了jvm对private static final只初始化一次的特性,可以解决多线程问题,但是当我们要在getInstance()前做一些配置工作(例如初始化数据库连接等),那么这种方式就捉襟见肘了。

4.双重检验锁
// 双重检验锁(double checked) 
class SingletonDemo3 {
    
    private static volatile SingletonDemo3 instance;
    
    private SingletonDemo3() {}
    
    public static SingletonDemo3 getInstance() {
        if (instance == null) {    // 区域(1)
            synchronized (SingletonDemo3.class) {  // 区域(2)
                if (instance == null) {
                    instance = new SingletonDemo3(); // 语句(1)
                }
            }
        }
        return instance;
    }
}

双重检验锁(double checked)。注意到它有2次判断,一次在同步块内,一次在同步块外。假设现在有4个线程T1,T2,T3,T4。T1,T2进入了区域(1),T3,T4还没启动。T1能进入区域(2)创建instance成功,之后T2进入区域(2),判断非空并出来。此时T3,T4启动了,不会进入区域(1),且无需等待锁。

instance变量声明成volatile, 它可以禁止指令重排序优化。
volatile的两个作用:

禁止指令重排优化。

所修饰的变量一旦被修改,其他线程立即可见。(但是非原子操作,即其他线程可以感知到变量被修改,但无法使用val += 1这种语句使其原子增加。)因此可用于1读者n写者的场景,例如点击"游戏退出按钮",其他(金币累加/音效)线程将立即感知到。

更多单例模式可以看这里。

5.异常问题回放

昨晚报出异常的代码片段如下:

class WrongSample {
    
    private volatile static WrongSample instance;
    
    private WrongSample() {

    }
    
    public static WrongSample getInstance() {
        if (instance == null) {    // 区域(1)
            synchronized (WrongSample.class) {  // 区域(2)
                if (instance == null) {
                    instance = new WrongSample(); // 语句(1)
                    instance.init(); //语句(2)
                }
            }
        }
        return instance;
    }

    private void init() {
    
    }
}

该代码使用双重检验锁构建了一个单例,且对单例进行初始化。那么空指针异常抛出对原因在哪呢?设想现在有线程T1,T2。线程T1进入区域(2),T2此时还未启动。T1执行了语句(1),但并未执行语句2,此时instance已经不是null,所以T2启动时在区域(1)判断非null将直接返回instance,但T2并未被初始化,由是产生异常。

解决方案:初始化操作放进构造函数,执行语句(1)时里暗含里执行构造函数。代码如下:

class WrongSample {
    
    private volatile static WrongSample instance;
    
    private WrongSample() {
        init();
    }
    
    public static WrongSample getInstance() {
        if (instance == null) {    // 区域(1)
            synchronized (WrongSample.class) {  // 区域(2)
                if (instance == null) {
                    instance = new WrongSample(); // 语句(1)
                }
            }
        }
        return instance;
    }

    private void init() {
    
    }
}

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

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

相关文章

  • 单例模式你会几种写法?

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

    solocoder 评论0 收藏0
  • Java 双重加锁单例java 内存重排序特性

    摘要:关于对于重排序的讲解,强烈推荐阅读程晓明写的深入理解内存模型二重排序。语义语义单线程下,为了优化可以对操作进行重排序。编译器和处理器为单个线程实现了语义,但对于多线程并不实现语义。双重加载的单例模式分析即双重检查加锁。 版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009231182 1. 引言 在开始分析双重加锁单...

    HackerShell 评论0 收藏0
  • 设计模式单例模式

    摘要:单例模式关注的重点私有构造器线程安全延迟加载序列化和反序列化安全反射攻击安全相关设计模式单例模式和工厂模式工厂类可以设计成单例模式。 0x01.定义与类型 定义:保证一个类仅有一个实例,并提供一个全局访问点 类型:创建型 UML showImg(https://segmentfault.com/img/bVbtDJ2?w=402&h=268); 单例模式的基本要素 私有的构造方...

    陆斌 评论0 收藏0
  • Java设计模式-单例模式(Singleton Pattern)

    摘要:如果需要防范这种攻击,请修改构造函数,使其在被要求创建第二个实例时抛出异常。单例模式与单一职责原则有冲突。源码地址参考文献设计模式之禅 定义 单例模式是一个比较简单的模式,其定义如下: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 或者 Ensure a class has only one instance, and provide a global point of ac...

    k00baa 评论0 收藏0
  • Java基础学习——多线程之单例设计模式(转)

    摘要:总之,选择单例模式就是为了避免不一致状态,避免政出多头。二饿汉式单例饿汉式单例类在类初始化时,已经自行实例化静态工厂方法饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。 概念:  Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍两种:懒汉式单例、饿汉式单例。  单例模式有以下特点:  1、单例类只能有一个实例。 ...

    dendoink 评论0 收藏0
  • 从未这么明白的设计模式(一):单例模式

    摘要:一般来说,这种单例实现有两种思路,私有构造器,枚举。而这种方式又分了饱汉式,饿汉式。通过关键字防止指令重排序。什么是单例?为什么要用单例? 一个类被设计出来,就代表它表示具有某种行为(方法),属性(成员变量),而一般情况下,当我们想使用这个类时,会使用new关键字,这时候jvm会帮我们构造一个该类的实例。而我们知道,对于new这个关键字以及该实例,相对而言是比较耗费资源的。所以如果我们能够想...

    NikoManiac 评论0 收藏0

发表评论

0条评论

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