详解单例设计模式

什么是单例模式?

单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。

从概念中体现出了单例的一些特点:

  1. 在任何情况下,单例类永远只有一个实例存在

  2. 单例需要有能力为整个系统提供这一唯一实例

在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

正是由于这个特点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或文件中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。这种方式极大地简化了在复杂环境 下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。

1、饿汉式单例

饿汉式单例是指在方法调用前,实例就已经创建好了。下面是实现代码:

public class MySingleton {
	
	private static MySingleton instance = new MySingleton();
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		return instance;
	}
}

以上是单例的饿汉式实现,我们来看看饿汉式在多线程下的执行情况,给出一段多线程的执行代码:

 
public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(MySingleton.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

以上代码运行结果:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

从运行结果可以看出实例变量额hashCode值一致,这说明对象是同一个,饿汉式单例实现了。

2、懒汉式单例

懒汉式单例是指在方法调用获取实例时才创建实例,因为相对饿汉式显得“不急迫”,所以被叫做“懒汉模式”。下面是实现代码:

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		if(instance == null){//懒汉式
			instance = new MySingleton();
		}
		return instance;
	}
}

这里实现了懒汉式的单例,在多线程并发下这样的实现是无法保证实例实例唯一的,甚至可以说这样的失效是完全错误的,下面我们就来看一下多线程并发下的执行情况,这里为了看到效果,我们对上面的代码做一小点修改:

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		try { 
			if(instance != null){//懒汉式 
				
			}else{
				//创建实例之前可能会有一些准备性的耗时工作 
				Thread.sleep(300);
				instance = new MySingleton();
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}

这里假设在创建实例前有一些准备性的耗时工作要处理,多线程调用:

 
public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(MySingleton.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

执行结果如下:

1210420568
1210420568
1935123450
1718900954
1481297610
1863264879
369539795
1210420568
1210420568
602269801

从这里执行结果可以看出,单例的线程安全性并没有得到保证,那要怎么解决呢?

3、线程安全的懒汉式单例

要保证线程安全,我们就得需要使用同步锁机制,下面就来看看我们如何一步步的解决 存在线程安全问题的懒汉式单例(错误的单例)。

  1. 方法中声明synchronized关键字
    出现非线程安全问题,是由于多个线程可以同时进入getInstance()方法,那么只需要对该方法进行synchronized的锁同步即可:
 
public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public synchronized static MySingleton getInstance() {
		try { 
			if(instance != null){//懒汉式 
				
			}else{
				//创建实例之前可能会有一些准备性的耗时工作 
				Thread.sleep(300);
				instance = new MySingleton();
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}

此时任然使用前面验证多线程下执行情况的MyThread类来进行验证,将其放入到org.mlinge.s03包下运行,执行结果如下:

1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373

从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低。同步方法效率低,那我们考虑使用同步代码块来实现:

  1. 同步代码块实现
 
public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	//public synchronized static MySingleton getInstance() {
	public static MySingleton getInstance() {
		try { 
			synchronized (MySingleton.class) {
				if(instance != null){//懒汉式 
					
				}else{
					//创建实例之前可能会有一些准备性的耗时工作 
					Thread.sleep(300);
					instance = new MySingleton();
				}
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}

这里的实现能够保证多线程并发下的线程安全性,但是这样的实现将全部的代码都被锁上了,同样的效率很低下。
3. 针对某些重要的代码来进行单独的同步(可能非线程安全)
针对某些重要的代码进行单独的同步,而不是全部进行同步,可以极大的提高执行效率,我们来看一下:

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() {
		try {  
			if(instance != null){//懒汉式 
				
			}else{
				//创建实例之前可能会有一些准备性的耗时工作 
				Thread.sleep(300);
				synchronized (MySingleton.class) {
					instance = new MySingleton();
				}
			} 
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}

此时同样使用前面验证多线程下执行情况的MyThread类来进行验证,将其放入到org.mlinge.s04包下运行,执行结果如下:

1481297610
397630378
1863264879
1210420568
1935123450
369539795
590202901
1718900954
1689058373
602269801

从运行结果来看,这样的方法进行代码块同步,代码的运行效率是能够得到提升,但是却没能保住线程的安全性。看来还得进一步考虑如何解决此问题。
4. Double Check Locking 双检查锁机制(推荐)
为了达到线程安全,又能提高代码执行效率,我们这里可以采用DCL的双检查锁机制来完成,代码实现如下:

public class MySingleton {
	
	//使用volatile关键字保其可见性
	volatile private static MySingleton instance = null;
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() {
		try {  
			if(instance != null){//懒汉式 
				
			}else{
				//创建实例之前可能会有一些准备性的耗时工作 
				Thread.sleep(300);
				synchronized (MySingleton.class) {
					if(instance == null){//二次检查
						instance = new MySingleton();
					}
				}
			} 
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}

将前面验证多线程下执行情况的MyThread类放入到org.mlinge.s05包下运行,执行结果如下:

369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795

从运行结果来看,该中方法保证了多线程并发下的线程安全性。
这里在声明变量时使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化。集合其二者,这种实现方式既保证了其高效性,也保证了其线程安全性。

4、使用静态内置类实现单例模式

DCL解决了多线程并发下的线程安全问题,其实使用其他方式也可以达到同样的效果,代码实现如下:

public class MySingleton {
	
	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
}

以上代码就是使用静态内置类实现了单例模式,这里将前面验证多线程下执行情况的MyThread类放入到org.mlinge.s06包下运行,执行结果如下:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

从运行结果来看,静态内部类实现的单例在多线程并发下单个实例得到了保证。

5、序列化与反序列化的单例模式实现

静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

 
import java.io.Serializable;
 
public class MySingleton implements Serializable {
	 
	private static final long serialVersionUID = 1L;
 
	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
}

序列化与反序列化测试代码:

public class SaveAndReadForSingleton {
	
	public static void main(String[] args) {
		MySingleton singleton = MySingleton.getInstance();
		
		File file = new File("MySingleton.txt");
		
		try {
			FileOutputStream fos = new FileOutputStream(file);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(singleton);
			fos.close();
			oos.close();
			System.out.println(singleton.hashCode());
		} catch (FileNotFoundException e) { 
			e.printStackTrace();
		} catch (IOException e) { 
			e.printStackTrace();
		}
		
		try {
			FileInputStream fis = new FileInputStream(file);
			ObjectInputStream ois = new ObjectInputStream(fis);
			MySingleton rSingleton = (MySingleton) ois.readObject();
			fis.close();
			ois.close();
			System.out.println(rSingleton.hashCode());
		} catch (FileNotFoundException e) { 
			e.printStackTrace();
		} catch (IOException e) { 
			e.printStackTrace();
		} catch (ClassNotFoundException e) { 
			e.printStackTrace();
		}
		
	}
}

运行以上代码,得到的结果如下:

865113938
1442407170

从结果中我们发现,序列号对象的hashCode和反序列化后得到的对象的hashCode值不一样,说明反序列化后返回的对象是重新实例化的,单例被破坏了。那怎么来解决这一问题呢?
解决办法就是在反序列化的过程中使用readResolve()方法,单例实现的代码如下:

public class MySingleton implements Serializable {
	 
	private static final long serialVersionUID = 1L;
 
	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
	
	//该方法在反序列化时会被调用,该方法不是接口定义的方法,有点儿约定俗成的感觉
	protected Object readResolve() throws ObjectStreamException {
		System.out.println("调用了readResolve方法!");
		return MySingletonHandler.instance; 
	}
}

再次运行上面的测试代码,得到的结果如下:

865113938
调用了readResolve方法!
865113938

从运行结果可知,添加readResolve方法后反序列化后得到的实例和序列化前的是同一个实例,单个实例得到了保证。

6、使用static代码块实现单例

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性的实现单例设计模式。

public class MySingleton{
	 
	private static MySingleton instance = null;
	 
	private MySingleton(){}
 
	static{
		instance = new MySingleton();
	}
	
	public static MySingleton getInstance() { 
		return instance;
	} 
}

测试代码如下:

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		for (int i = 0; i < 5; i++) {
			System.out.println(MySingleton.getInstance().hashCode());
		}
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[3];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

运行结果如下:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

从运行结果看,单例的线程安全性得到了保证。

7、使用枚举数据类型实现单例模式

枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例:

public enum EnumFactory{ 

    singletonFactory;

    private MySingleton instance;

    private EnumFactory(){//枚举类的构造方法在类加载是被实例化
        instance = new MySingleton();
    }

    public MySingleton getInstance(){
        return instance;
    }
}
 
class MySingleton{//需要获实现单例的类,比如数据库连接Connection
    public MySingleton(){} 
}
 
public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

执行后得到的结果:

1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610

运行结果表明单例得到了保证,但是这样写枚举类被完全暴露了,据说违反了“职责单一原则”,那我们来看看怎么进行改造呢。

8、完善使用enum枚举实现单例模式

不暴露枚举类实现细节的封装代码如下:

public class ClassFactory{ 
	
	private enum MyEnumSingleton{
		singletonFactory;
		
		private MySingleton instance;
		
		private MyEnumSingleton(){//枚举类的构造方法在类加载是被实例化
			instance = new MySingleton();
		}
 
		public MySingleton getInstance(){
			return instance;
		}
	} 
 
	public static MySingleton getInstance(){
		return MyEnumSingleton.singletonFactory.getInstance();
	}
}
 
class MySingleton{//需要获实现单例的类,比如数据库连接Connection
	public MySingleton(){} 
}

验证单例实现的代码如下:

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(ClassFactory.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

验证结果:

1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450

验证结果表明,完善后的单例实现更为合理。


单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

  1. 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
  2. 省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
  3. 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
    首先我们写一个简单的单例类:
public class Singleton {  
  
    /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */  
    private static Singleton instance = null;  
  
    /* 私有构造方法,防止被实例化 */  
    private Singleton() {  
    }  
  
    /* 静态工程方法,创建实例 */  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
  
    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    public Object readResolve() {  
        return instance;  
    }  
}  

这个类可以满足基本要求,但是像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对getInstance方法加synchronized关键字,如下:

public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
}  

但是,synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个:

public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized (instance) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  

似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例:

  1. A、B线程同时进入了第一个if判断
  2. A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
  3. 由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
    d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
  4. 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。
    所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化:
    private static class SingletonFactory{           
        private static Singleton instance = new Singleton();           
    }           
    public static Singleton getInstance(){           
        return SingletonFactory.instance;           
    }   

实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:

public class Singleton {  
  
    /* 私有构造方法,防止被实例化 */  
    private Singleton() {  
    }  
  
    /* 此处使用一个内部类来维护单例 */  
    private static class SingletonFactory {  
        private static Singleton instance = new Singleton();  
    }  
  
    /* 获取实例 */  
    public static Singleton getInstance() {  
        return SingletonFactory.instance;  
    }  
  
    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    public Object readResolve() {  
        return getInstance();  
    }  
}  

其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的:

public class SingletonTest {  
  
    private static SingletonTest instance = null;  
  
    private SingletonTest() {  
    }  
  
    private static synchronized void syncInit() {  
        if (instance == null) {  
            instance = new SingletonTest();  
        }  
    }  
  
    public static SingletonTest getInstance() {  
        if (instance == null) {  
            syncInit();  
        }  
        return instance;  
    }  
}  

考虑性能的话,整个程序只需创建一次实例,所以性能也不会有什么影响。
补充:采用"影子实例"的办法为单例对象的属性同步更新

public class SingletonTest {  
  
    private static SingletonTest instance = null;  
    private Vector properties = null;  
  
    public Vector getProperties() {  
        return properties;  
    }  
  
    private SingletonTest() {  
    }  
  
    private static synchronized void syncInit() {  
        if (instance == null) {  
            instance = new SingletonTest();  
        }  
    }  
  
    public static SingletonTest getInstance() {  
        if (instance == null) {  
            syncInit();  
        }  
        return instance;  
    }  
  
    public void updateProperties() {  
        SingletonTest shadow = new SingletonTest();  
        properties = shadow.getProperties();  
    }  
}  
  1. 单例模式理解起来简单,但是具体实现起来还是有一定的难度。
  2. synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。

突然想到另一个问题,就是采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?
首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)
其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。
再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。
最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面最后实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!

//双重校验锁
public class Singleton {  
   private volatile static Singleton singleton;  
   private Singleton (){}  
   public static Singleton getSingleton() {  
   if (singleton == null) {  
       synchronized (Singleton.class) {  
       if (singleton == null) {  
           singleton = new Singleton();  
       }  
       }  
   }  
   return singleton;  
   }  
}

//枚举
public enum Singleton {  
   INSTANCE;  
   public void whateverMethod() {  
   }  
}

猜你喜欢

转载自blog.csdn.net/shkstart/article/details/107764450