资讯专栏INFORMATION COLUMN

单例模式

Backache / 980人阅读

摘要:构造函数被调用或者,我们利用初始化块,在初始化的时候就完成实例化构造器被调用双重检查锁定避免懒汉模式造成性能低下的另一个思路就是双重检查锁定。

1. 什么是单例

保证一个类仅有一个实例,并提供一个访问它的全局访问点。适用于:

当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。

当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。

在单例模式中,有下列参与者:

Singleton:

定义一个Instance操作,允许客户访问它的唯一实例。Instance是一个类操作。

可能负责创建它自己的唯一实例。

2. 不考虑多线程的情况下的单例

下面就是一个单例的实现:Singleton0当然,这个示例在多线程下有问题

// 单例程序: Singleton0 

class Printer{
    private static Printer printer;
    
    private Printer(){
        
    }
    
    public static Printer getInstance(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }
    
}

public class Singleton {

    public static void main(String[] args) {
        Printer p1 = Printer.getInstance();
        Printer p2 = Printer.getInstance();
        System.out.println(p1);
        System.out.println(p2);
    }

}

/* 运行结果:
 * Printer@659e0bfd
 * Printer@659e0bfd
 * 完全一样,表示只创建了Printer对象的一个实例
 */
3. 多线程环境下的单例

很可惜,上面的单例程序在多线程环境下,会华丽丽的出错!

3.1. 上述单例在多线程环境下的问题

我们修改一下Singleton0,成为如下形式:Singleton1

public class Singleton1 {
    
    private static Singleton1 s1;
    
    private Singleton1(){
        System.out.println("构造函数被调用!");
    }
    
    public static Singleton1 getInstance(){
        if(s1 == null){
            s1 = new Singleton1();
        }
        return s1;
    }
}

// 测试程序,采用JUnit 4.x来测试
import org.junit.Test;

public class Singleton1Test implements Runnable{
    
    @Override
    public void run() {
        Singleton1.getInstance();
    }
    
    @Test
    public void test() {
        for (int i = 0; i < 100000; i++) {
            Thread t = new Thread(new Singleton1Test(), "AnyThreadName");
            t.start();
        }
    }
}

/* 运行结果:(可以发现,构造函数被多次调用!说明无法保证单例)
 * 构造函数被调用!
 * 构造函数被调用!
 * 构造函数被调用!
 * 构造函数被调用!
 * 构造函数被调用!
 */

原因很简单,在多线程的情况下,调用 Singleton1.getInstance() 的时候,可能会多个线程同时调用到,这个时候构造函数 Singleton1() 还没有把 s1 实例化出来。这个时候判断 s1 == null 是对的,所以多个线程都会去执行:

if(s1 == null){
    s1 = new Singleton1();
}

所以,这个构造函数就会被执行多次!

知道了这个原因,我们就可以很方便的找到解决方法,那就是:懒汉模式。

3.2. 懒汉模式

既然是问题出在 getInstance() 上,那么我们就把这个方法设置成synchronized,这样就可以保证同步了,于是我们修改成 Singleton2

/**
 *  这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading。
 *  但是,遗憾的是,效率很低,99%情况下不需要同步。
 * 
 * @author martin.wang
 *
 */
public class Singleton2 {
    private static Singleton2 s2;
    
    private Singleton2(){
        System.out.println("构造函数被调用!");
    }
    
    public static synchronized Singleton2 getInstance(){
        if(s2 == null){
            s2 = new Singleton2();
        }
        return s2;
    }
}

这个方式的最大问题就是:效率太低了。我们知道 synchronized 关键字很消耗资源,而且99%以上的可能性是不用 synchronized 的。每次都要 synchronized 有必要吗?那么我们就有了2种解决方案:

饿汉模式。干脆一开始就给你初始化算了。

双重检查锁定。只在必要的时候用 synchronized

3.3. 饿汉模式

饿汉模式避免了在 getInstance 的时候的判断,所以效率高一点。不过也不是无懈可击,如果这个构造的过程很消费时间,那么每次classloader的时间会非常长,没有起到 lazyload 的效果。

public class Singleton3{
    private static Singleton3 singleton3 = new Singleton3();

    private Singleton3() {
        System.out.println("构造函数被调用!");
    }

    public static Singleton3 getInstance() {
        return singleton3;
    }
}

或者,我们利用初始化块,在初始化的时候就完成实例化

public class Singleton4 {
    private static Singleton4 s4 = null;
    
    static {
        s4 = new Singleton4();
    }
    
    private Singleton4(){
        System.out.println("构造器被调用");
    }
    
    public static Singleton4 getInstance(){
        return s4;
    }
}
3.4. 双重检查锁定

避免懒汉模式造成性能低下的另一个思路就是:双重检查锁定。原理就是:

Singleton1 示例中造成问题的原因是 getInstance() 不同步

Singleton2 示例中造成性能低下的原因是不管三七二十一全同步

那么,我们就只检查可能存在 同步问题 的代码,让代码只在 可能存在问题的时候 再去做同步。

3.4.1. 双重检查锁定的“实现”(有问题的!)
public class Singleton7 {
    private static Singleton7 s7;
    
    private Singleton7() {
        System.out.println("构造函数被调用");
    }
    
    public static Singleton7 getInstance() {
        if(s7 == null) {
            synchronized (Singleton7.class) {   // A
                if(s7 == null) {                // B
                    s7 = new Singleton7();      // C
                }
            }
        }
        return s7;
    }
}

思路分析:

如果 s7 == null ,那么这个时候要同步了。

在注释 A 的里面,设置一个同步锁

如果线程 T1 访问同步块 A 中的代码的时候,线程 T2A 附近等待释放锁。

T1 线程完成,这个时候 T2 线程开始运行 A 中的代码。这个时候 s7 已经被 T1 线程初始化了,执行 B 的时候会返回 false,不会去执行构造函数。

不过,这个办法还是不能保证完美无缺,还存在至少是理论上的缺陷。

3.4.2. 原因分析

双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。

双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”。

也就是说,不能保证 A, B, C 是按顺序运行的,这个可以Google一下 指令重排,这里不展开了。

说实话,我自己测试了100+次,并没有出现构造函数出现2次或以上的情况。出现这情况的概率很小很小。Java的内存模型很复杂,牵涉到具体的JVM实现。
3.4.3. 修改后的加强版
public class Singleton7 {
    private static volatile Singleton7 s7;
    
    private Singleton7() {
        System.out.println("构造函数被调用");
    }
    
    public static Singleton7 getInstance() {
        if(s7 == null) {
            synchronized (Singleton7.class) {   // A
                if(s7 == null) {                // B
                    s7 = new Singleton7();      // C
                }
            }
        }
        return s7;
    }
}
不过,据说这个也不是特别靠谱,我不去深究了。

看到这里,你是否有种想骂人的冲动?什么鬼,做个单例模式就这么难啊。有没有更方便的办法?有,还不止一种:

3.5. 静态内部类法
public class Singleton5 {

    private static class SingletonHolder{
        private static final Singleton5 INSTANCE = new Singleton5();
    }
    
    private Singleton5() {
        System.out.println("构造函数被调用");
    }
    
    public static final Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

// 测试方法:
import org.junit.Test;

public class Singleton5Test implements Runnable{

    @Override
    public void run() {
        Singleton5.getInstance();
    }
    
    @Test
    public void test() {
        for (int i = 0; i < 10000; i++) {
            Thread t = new Thread(new Singleton4Test(), "T5");
            t.start();
        }
    }
    
}
3.6. 枚举类法

枚举类发是《Effective Java》的作者 Josh Bloch 推荐的一种实现方式,除了具有上述方法的优点的话,还能防止反序列化重新创建新的对象、防止被反射攻击。超级牛叉!

TODO:现在还没有去写利用枚举类法防止反序列化,反射攻击的测试用例。希望以后来填坑。
public enum Singleton6 {
    INSTANCE;
    
    private Singleton6() {
        System.out.println("构造函数被调用");
    }
    
    protected void doSomething() {
        
    }
}

// 测试:
import org.junit.Test;

public class Singleton6Test implements Runnable{
    
    
    @Override
    public void run() {
        Singleton6.INSTANCE.doSomething();
    }

    @Test
    public void test() {
        for (int i = 0; i < 1000; i++) {
            Thread t = new Thread(new Singleton6Test());
            t.start();
        }
    }
}
4. 总结

如果没有什么特别需要,我个人认为还是用饿汉方式算了,简单有效。如果有懒加载要求,就用静态内部类法,也不错。有反序列化要求的,就用枚举类法。

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

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

相关文章

  • Android中的设计模式单例模式

    摘要:总结单例是运用频率很高的模式,因为客户端没有高并发的情况,选择哪种方式并不会有太大的影响,出于效率考虑,推荐使用和静态内部类实现单例模式。 单例模式介绍 单例模式是应用最广的模式之一,也可能是很多人唯一会使用的设计模式。在应用单例模式时,单例对象的类必须保证只用一个实例存在。许多时候整个系统只需要一个全局对象,这样有利于我么能协调整个系统整体的行为。 单例模式的使用场景 确保某个类有且...

    yzd 评论0 收藏0
  • JavaScript设计模式----单例模式

    摘要:不符合设计模式中的单一职责的概念。引入代理实现单例模式引入代理实现单例模式的特点我们负责管理单例的逻辑移到了代理类中。的单例模式对比在以上的代码中实现的单例模式都混入了传统面向对象语言的特点。 声明:这个系列为阅读《JavaScript设计模式与开发实践》 ----曾探@著一书的读书笔记 1.单例模式的特点和定义 保证一个类仅有一个实例,并且提供一个访问它的全局访问点。 2.传统面向对...

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

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

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

    摘要:在设计模式一书中,将单例模式称作单件模式。通过关键字,来保证不会同时有两个线程进入该方法的实例对象改善多线程问题为了符合大多数程序,很明显地,我们需要确保单例模式能在多线程的情况下正常工作。 在《Head First 设计模式》一书中,将单例模式称作单件模式。这里为了适应大环境,把它称之为大家更熟悉的单例模式。 一、了解单例模式 1.1 什么是单例模式 单例模式确保一个类只有一个实例,...

    everfight 评论0 收藏0

发表评论

0条评论

Backache

|高级讲师

TA的文章

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