单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种创建型模式,是一种简单实用又复杂的设计模式。
懒汉式和饿汉式都是一种比较形象的称谓。
Lazy Loa
d)技术”。 private LazySingleton(){
...}
private
后,虽然外部不能再使用new来创建对象,但是在LazySingleton
的内部还是可以创建对象的,同时为了让外界访问到这个唯一实例,可以提供一个方法来返回类的实例,同时该方法要加上static,直接通过类来调用对象。因为这个方法是static
的,那么属性也要被迫变成static
的(这里并没有用到static的特性)。 private static LazySingleton lazySingleton = null;
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
在单线程情况下,上面的代码是没有问题的,但在多线程的环境下,是不安全的。我们假设thread0
和thread1
同时来到了if(lazySingleton == null)
,因为此时lazySingleton
是null,两个线程都通过了条件判断,开始new操作,这样一来,lazySingleton
就被实例化了两次。
我们写两个类通过多线程debug的方式进行测试。
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+lazySingleton);
}
}
public class Test {
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("program end");
}
}
由上图可知,lazySingleton实例化了两次。我们只需要在方法中加上synchronized
即可。这样当thread0
进入getInstance()
时,thread1
就处于阻塞状态,解决了线程安全问题。
public synchronized static LazySingleton getInstance(){
...}
上面的代码虽然解决了线程安全的问题,但是每次调用getInstance()
时都需要进行线程锁定判断,在多线程高并发环境中,将会导致系统性能大大降低。事实上,上述代码无须对整个getInstance()
方法进行锁定,只需要锁定代码lazySingleton = new LazySingleton();
即可。同时要注意,因为这个方法是静态方法,存在方法区并且整个JVM
只有一份,所以要加类锁,即synchronized (LazyDoubleCheckSingleton.class){...}
//LazyDoubleCheckSingleton 与上面LazySingleton作用一样
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
//类锁
synchronized (LazyDoubleCheckSingleton.class){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
return lazyDoubleCheckSingleton;
}
上面代码看似解决了问题,但事实并非如此,使用上面代码来创建单例对象,还是会存在单例对象不唯一的情况。
假设某一瞬间,thread0
和thread1
同时来到了if(lazySingleton == null)
,因为此时lazySingleton
是null,两个线程都通过了条件判断,由于实现了synchronized
加锁机制,thread0
进入synchronized
锁定的代码中执行操作,thread1处于阻塞状态,必须等到线程A执行完毕后才能进入synchronized
锁定的代码。但是thread0
执行完,thread1
并不知道实例已经创建完成,将会继续创建实例,产生了多个单例对象,需要进一步改进,在synchronized
锁定的代码里再进行一次if(lazySingleton == null)
,这种方式就称为双重检查锁定。同时由于JVM编译器的指令重排机制,同样会出现问题。这并不是百分百发生的,但既然存在安全隐患,我们就需要解决它。
当进行lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
时,JVM会进行下面的1,2,3操作(instance
即lazyDoubleCheckSingleton
),其中2,3的操作顺序可能颠倒。
假设线程0要按照1,3,2的顺序进行初始化对象,恰好在1,3步完成synchronized锁
释放后,线程1进入synchronized
代码块中,此时instance
不为null,线程1比线程0首先访问了未初始化好的对象。我们只需要在instance
对象前面增加一个修饰符volatile
,就可以始终保持1,2,3的初始化顺序。这样在线程1看来,instance
对象的引用要么指向null
,要么指向一个初始化完成的instance
,从而保证了安全。完整代码如下:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
//1.分配内存给这个对象
//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
//2.初始化对象
//intra-thread semantics
---------------//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
}
}
}
return lazyDoubleCheckSingleton;
}
}
在Java语言中,不仅仅可以通过new
关键字直接创建对象,还可以通过反射机制创建对象。比如我们可以通过反射获取类中的属性,方法,构造器。即使这些的访问权限是private,我们也可以通过setAccessible()
来启动和禁用访问安全检查的开关,参数值为true则指示反射的对象在使用时应该取消Java语言访问检查,那么我们就可以对这个对象为所欲为了。
比如:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Constructor<LazySingleton> c = LazySingleton.class.getDeclaredConstructor();
//参数值为true则指示反射的对象在使用时应该取消Java语言访问检查
c.setAccessible(true);
LazySingleton instance1 = c.newInstance();
LazySingleton instance2 = LazySingleton.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}
打印结果如下,很明显这两个对象是不一样的。
我们可以在私有构造器里面进行判断,如果lazySingleton对象
已经实例化了,就抛出异常。
private LazySingleton(){
if (lazySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
其实对于饿汉式这样处理完全没有问题,但是懒汉式是有问题的,比如我们先通过反射创建一个对象,创建后的lazySingleton
还是null
,这个时候还是可以通过getInstance()
获取lazySingleton对象
的,这样就两个了。
其实我们还可以通过设置标志位的方式来解决这个问题。
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private static boolean flag =true;
private LazySingleton(){
if (flag){
flag = false;
}else {
throw new RuntimeException("单例构造器禁止反射调用");
}
if (lazySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public synchronized static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
public static void main(String[] args) throws Exception{
/**
* 序列化测试
*/
LazySingleton instance = LazySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
LazySingleton newInstance = (LazySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
当我们进行上面的序列化和反序列化操作时,每次反序列化一个序列化的实例时,都会创建一个新的实例。
所以当我们想将单例类变成可序列化的,仅仅在声明上加上implements Serializable
是不够的, 为了维护并保证单例,必须声明所有实例域都是瞬时( transient
)的,并提供一个 readResolve
方法。
[effective Java(第三版)]
readResolve 特性允许你用 readObject 创建的实例代替另一个实例[Serialization,3.7 ]对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve 方法,并且具备正确的声明,那么在反序列化之后,新建对象上的 readResolve 方法就会被调用,然后,该方法返回的对象引用将被返回,取代新建的对象 ,在这个特性的绝大多数用法中,指向新建对象的引用不需要再被保留,因此立即成为垃圾回收的对象。
序列化形式并不需要包含任何实际的数据;所有的实例域都应该被声明为瞬时的 。事实上,如果依赖 readResolve 进行实例控制,带有对引用类型的所有实例域 必须 transient , 否则,那种破釜沉舟式的攻击者,就有可能在readResolve 方法被运行之前,保护指向反序列化对象的引用
private static transient LazySingleton lazySingleton = null;
private Object readResolve(){
return lazySingleton;
}
我们只需要将代码修改为上面的部分,就可以得到预期的结果。
通过对源码的简单分析,其实现的原理为先通过反射创建一个反序列化后的对象,如果单例对象中定义了readResolve()
方法,则对前面生成的对象进行覆盖,来保证单例。
这个方案装载对象的时候就去创建对象实例。在Java中,static
有两个特性:
static
变量在类装载的时候进行初始化static
变量会共享同一块内存区域 private final static HungrySingleton hungrySingleton = new HungrySingleton();
因为饿汉式在类加载的时候就将自己实例化,无须考虑多线程访问的问题,可以确保实例的唯一性。
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
同样因为类加载的时候就将自己初始化好了,所以当反射攻击的时候,只需要进行if (hungrySingleton != null)
的操作即可。
private HungrySingleton(){
if (hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
饿汉式单例类不不能实现延迟加载,不管将来用不用,它始终占据内存;而懒汉式单例类安全控制繁琐麻烦,而且性能也会受到影响。即将要介绍的这种方式,能够将二者的缺点克服而兼顾优点,这种解决方案被称为Lazy initialization holder class (IoDH)模式
.
首先介绍一下静态内部类的属性
static
修饰的成员式内部类,它的对象与外部类对象不发生依赖关系,其相当于其外部类的成员。public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
当第一次调用getInstance()
时,它第一次读取InnerClass.staticInnerClassSingleton
,导致InnerClass内部类
得到初始化,而这个类在装载并被初始化的时候,会初始化它的静态域。从而创建了StaticInnerClassSingleton
的实例,由于是静态的属性,因此只会在虚拟机装载类的时候初始化一次,并由JVM
来保证它的线程安全性。
与饿汉式处理方式基本一致
private StaticInnerClassSingleton(){
if (InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
在 《effective Java》第三版 中提到: 单元素枚举类型经常成为实现 Singleton的最佳方法。 由于在平时的开发过程中,枚举类用的并不多,所以提前总结一下枚举类的一些重要用法。
enum
定义的枚举类默认继承了 java.lang.Enum
类,因此不能再继承其他类values()
:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。valueOf(String str)
:可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException
。toString()
:返回当前枚举类对象常量的名称public enum EnumInstance {
INSTANCE;
private Object data;
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
/**
* 验证 枚举类如何防止序列化创建对象
*/
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumInstance newInstance = (EnumInstance) ois.readObject();
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
结果:
通过结果可以看出,枚举类单例模式可以很好的解决序列化问题。
由上面的源码分析及查询资料可知,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum
的valueOf方法
来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject
、readObject
等方法。
public static void main(String[] args) throws Exception{
Constructor<EnumInstance> c = EnumInstance.class.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumInstance instance1 = c.newInstance("mazouri", 1);
}
查看源码发现,如果发现枚举类型通过反射构造对象,是会抛出异常的。
// Decompiled Using: FrontEnd Plus v2.03 and the JAD Engine
// Available From: http://www.reflections.ath.cx
// Decompiler options: packimports(3)
// Source File Name: EnumInstance.java
package com.mazouri.design.pattern.creational.singleton.lazy5;
public final class EnumInstance extends Enum
{
public static EnumInstance[] values()
{
return (EnumInstance[])$VALUES.clone();
}
public static EnumInstance valueOf(String name)
{
return (EnumInstance)Enum.valueOf(com/mazouri/design/pattern/creational/singleton/lazy5/EnumInstance, name);
}
private EnumInstance(String s, int i)
{
super(s, i);
}
public void setData(Object data)
{
this.data = data;
}
public Object getData()
{
return data;
}
public static EnumInstance getInstance()
{
return INSTANCE;
}
public static final EnumInstance INSTANCE;
private Object data;
private static final EnumInstance $VALUES[];
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
反编译结果可知:自己定义的枚举属性INSTANCE
会在前面自动加上 public static final
,同时会在静态代码块中进行初始化,而静态代码块在类加载的时候就会被初始化,所以是线程安全的。
关于枚举是不是懒加载,我也不清楚,
StaciOverflow
上面说是。
总的来说,使用枚举来实现单例模式会非常简洁,而且提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化,并且天然线程安全,是更简洁,高效,安全的方式。