译——Singletons (单例模式多种方式分析)

原文:http://stackoverflow.com/documentation/java/130/singletons#t=201701151121320953557

Introduction

一个singleton 是一个只有一个实例的类,典型的存储在一个static final 域中, ==equals(Object) 方法对于单例的比较两者没有什么不同(即使你向上或着向下转型),但是对于性能表现== 还是更被倾向。

Examples

一、Enum Singleton

public enum Singleton {
    INSTANCE;

    public void execute (String arg) {
        // Perform operation here 
    }
}

枚举类型有私有的构造器, 且属性是public 、static、final的,并且提供了合适的序列化机制,非常简介明了且用一种线程安全的方式懒惰加载。
JVM提供了一种保证:枚举值不能被实例化两次,这对枚举型单例模式对于反射攻击提供了强大的防卫。
枚举类型不防范其他的开发者物理的添加其它的元素向代码中。因此,如果你选择这种类型去实现单例,你应该清楚的知道不能在这些枚举中添加新的元素是很有必要的。
这也是被Joshua Bloch in Effective java 所推荐的一种实现单例的方式。

Singleton without use of Enum (eager initialization)

(不用枚举的“饿汉”式加载)

public class Singleton {    

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这个样例被论证能够有效的进行懒加载 章节12.4.1 of the Java Language Specification 声明:

一个类或着接口类型T 在以下任何一个条件发生之前将会立刻被初始化。

  • T是一个类,并且这个类被创建
  • T是一个类, 在T中声明的静态方法被调用
  • T中声明的A 静态域被赋值
  • T中声明的A静态域被使用并且这个域是非常量
  • T是一个顶级类, 并且T中嵌套的断言声明语句被执行

    因此,在类中只要没有其它的静态域或着静态方法,单例将会被初始化直到方法getInstance() 第一次被调用

Thread-safe lazy initialization using holder class | Bill Pugh Singleton implementation

(使用内部类线程安全的实现单例模式)


public class Singleton {
    private static class InstanceHolder {
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return InstanceHolder.INSTANCE;
    }

    private Singleton() {}
}

Singleton.getInstance()在第一次调用时初始化 INSTANCE 变量,利用了java语言的对静态初始化的线程安全的保证,过程中不要求额外的synchronization。

Thread safe Singleton with double checked locking

(线程安全的双检查锁)
这种类型的单例是安全的,防止了单例被初始化之后 非必要的上锁消耗

public class MySingleton {

    // instance of class
    private static volatile MySingleton instance = null;

    // Private constructor
    private MySingleton() {
        // Some code for constructing object
    }

    public static MySingleton getInstance() {
        MySingleton result = instance;

        //If the instance already exists, no locking is necessary
        if(result == null) {
            //The singleton instance doesn't exist, lock and check again
            synchronized(MySingleton.class) {
                result = instance;
                if(result == null) {
                    instance = result = new MySingleton();
                }
            }
        }
        return result;
    }
}

应该被强调 – 在java SE 5版本之前,这种实现是不正确的,并且应该被避免,在java5版本之前实现双重检查锁定是不可能的 (文末会解释为什么)

译者补充:

####**双重检查锁定的由来**
java程序中,有时候可能需要推迟一些高开销的对象初始化工作,并且只有在使用它的时候才进行初始化,此时程序员会采用延迟初始化。下面是非线程安全的延迟初始化对象的代码:
public class UnsafeLazyInitialization {
        private static Instance instance;
        public static Instance getInstance() {
            if (instance == null) // 1:A线程执行
                instance = new Instance(); // 2:B线程执行
            return instance;
        }
    }

在UnsafeLazyInitialization类中,假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化。
对于UnsafeLazyInitialization类,我们可以对getInstance()方法做同步处理来实现线程安全的延迟初始化。示例代码如下:

   public class SafeLazyInitialization {
        private static Instance instance;
        public synchronized static Instance getInstance() {
            if (instance == null)
                instance = new Instance();
            return instance;
        }
    }

由于对getInstance() 方法做了同步处理,synchronized将导致性能开销,如果getInstance()
法被多个线程频繁的调用,将会导致程序执行性能的下降。
在早期的JVM中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销。因此,人们想出了一个“聪明”的技巧:双重检查锁定(Double-Checked Locking)。人们想通过双重检查锁定来降低同步的开销。下面是使用双重检查锁定来实现延迟初始化的示例代码:

    public class DoubleCheckedLocking { // 1
        private static Instance instance; // 2
        public static Instance getInstance() { // 3
            if (instance == null) { // 4:第一次检查
                synchronized (DoubleCheckedLocking.class) { // 5:加锁
                    if (instance == null) // 6:第二次检查
                        instance = new Instance(); // 7:问题的根源出在这里
                } // 8
            } // 9
            return instance; // 10
        } // 11
    }

如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始
化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全
其美。
多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
·在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。
双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第4行,代码读
取到instance不为null时,instance引用的对象有可能还没有完成初始化

问题的根源

前面的双重检查锁定示例代码的第7行(instance=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。

memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址

如果2、3步骤进行了重排序:

memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象

这个重排序在没有改变单线程程序执行结果的前提下,可以
提高程序的执行性能,所以被允许了。
那么A、B两个线程下时序如下:
这里写图片描述

Java内存模型的intra-thread semantics将确保A2一定会排在
A4前面执行。因此,线程A的intra-thread semantics没有改变,但A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。

在知晓了问题发生的根源之后,我们可以想出两个办法来实现线程安全的延迟初始化。
1)不允许2和3重排序。
2)允许2和3重排序,但不允许其他线程“看到”这个重排序。

第一种解决方案是利用volatile的顺序一致性模型的增强语义来禁止2、3步骤重排序。instance 由volatile来修饰:

   public class SafeDoubleCheckedLocking {
        private volatile static Instance instance;
        public static Instance getInstance() {
            if (instance == null) {
                synchronized (SafeDoubleCheckedLocking.class) {
                    if (instance == null)
                        instance = new Instance(); // instance为volatile,现在没问题了
                }
            }r
            eturn instance;
        }
    }

这个解决方案需要JDK 5或更高版本(因为从JDK 5开始使用新的JSR-133内存模
型规范,这个规范增强了volatile的语义)

第二种解决方案就是上面翻译的那个静态内部类实现单例,利用了JVM的特性。
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>