教你如何写出正确的单例模式

概念:

单例模式是23种设计模式之一,它是java中一种常见的设计模式,单例模式的写法有好几种,这里主要介绍八种:懒汉线程不安全、懒汉线程安全、饿汉、饿汉变种、静态内部类、枚举、双重校验锁、登记式。

懒汉:懒汉就是我们所说的延迟加载,懒加载。

饿汉:不延迟加载,比如说:随着类的加载而加载。


单例模式描述实现方法:

  1. 私有化成员变量。
  2. 私有化构造方法。
  3. 提供一个公共的方法供外部访问,从而实现单例。


单例模式特点:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

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


第一种 (懒汉,线程不安全)

public class Singleton {

	private static Singleton singleton;
	
	private Singleton() {
		// TODO Auto-generated constructor stub
	}
	
	public static Singleton getInstance(){
		if(singleton==null){
			singleton=new Singleton();
		}
		return singleton;
	}
}

Singleton通过将构造方法限定于private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。
但是以上实现没有考虑线程安全问题。所谓线程安全是指:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行的结果和单线程运行的结果一样的,而且其他的变量的值也和预期的值是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步问题。显示以上实例并不满足线程安全的要求,如果多个线程同时运行这段代码,就会创建多个实例,因此不符合单例模式,所以不适合多线程。


第二种 (懒汉,线程安全)

public class Singleton {

	private static Singleton singleton;
	
	private Singleton() {
		// TODO Auto-generated constructor stub
	}
	
	public static synchronized Singleton getInstance(){
		if(singleton==null){
			singleton=new Singleton();
		}
		return singleton;
	}
}

以上实例添加了关键字synchronized 来修饰方法,synchronized 是线程锁,也就是说让线程同步,能够保证在同一时刻最多只有一个线程执行该代码,另一个线程必须等待当前线程执行完这段代码以后才能执行,虽然保证了线程安全,实现了在多线程中的单例模式,但是遗憾的是这样效率很低,99%情况下是不需要同步的。


第三种(饿汉)

public class Singleton {

	private static Singleton singleton=new Singleton();
	
	private Singleton() {
		// TODO Auto-generated constructor stub
	}
	
	public static Singleton getInstance(){
		return singleton;
	}
}

这种方式基于classloder机制避免了多线程的同步问题,不过,singleton在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance()方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化singleton显然没有达到延迟加载的效果。还有就是不适合传参实例化。


第四种(饿汉,变种)

public class Singleton {

	private static Singleton singleton=null;
	
	static{
		singleton=new Singleton();
	}
	
	private Singleton() {
		// TODO Auto-generated constructor stub
	}
	
	public static Singleton getInstance(){
		return singleton;
	}
}

表面上看起来差别很大,其实跟第三种方式差不多,都是在类初始化即实例化singleton。


第五种(静态内部类)

public class Singleton {
	
	private static class SingletonHolder{
		private static final Singleton singleton=new Singleton();
	}

	private Singleton() {
		// TODO Auto-generated constructor stub
	}
	
	public static final Singleton getInstance(){
		return SingletonHolder.singleton;
	}
}

这种方式同样利用了classloder的机制类保证初始化singleton时只有一个线程,它跟第三种和第四种方式不同的是:第三种和第四种方式是只要Singleton类被装载了,那么singleton就会被实例化,不过没有达到延迟加载的效果,而这种方式是Singleton类被装载了,singleton不一定被初始化。因为SingletonHolder类没有被主动使用,只是显示通过调用getInstance()方法时,才会显示装载SingletonHolder类,从而实例化singleton。想象一下,如果实例化singleton很消耗资源,我想让它延迟加载,另外一方面,我不希望在Singleton加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被调用从而被加载,那么这个时候实例化singleton显然不合适。这个时候,这中方式相比第三种和第四种显然是合理的。


第六种(枚举)

public enum enumElvis {

	INSTANCE;

	public void leaveTheBuilding(){

	System.out.println("Whoa baby");

	}

	public static void main(String[] args){

	enumElvis elvis = enumElvis.INSTANCE;

	elvis.leaveTheBuilding();

	}


	
}

单例模式从java1.5发型起,实现单例模式的方法只需编写一个包含单个元素的枚举类型。

这种方法在功能上与公有域方法相近,但它更加整洁,无偿的提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。单元素的枚举类型已经成为实现Singleton的最佳方法。不过不适合Android单例模式。


第七种(双重校验锁)

public class Singleton {
	
	private volatile static Singleton singleton;

	private Singleton() {
		// TODO Auto-generated constructor stub
	}
	
	public static Singleton getInstance(){
		if(singleton==null){
			synchronized (Singleton.class) {
				if(singleton==null){
					singleton=new Singleton();
				}
			}
		}
		return singleton;
	}
}

volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其他的线程。

volatile包含以下语义:

(1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。

(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。

volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

在JDK1.5之后,双重检查锁定才能够正常达到单例效果。


第八种(登记式)

import java.util.HashMap;
import java.util.Map;

public class Singleton3 {
	
	private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
    static{
        Singleton3 single = new Singleton3();
        map.put(single.getClass().getName(), single);
    }
    //保护的默认构造子
    protected Singleton3(){}
    //静态工厂方法,返还此类惟一的实例
    public static Singleton3 getInstance(String name) {
        if(name == null) {
            name = Singleton3.class.getName();
            System.out.println("name == null"+"--->name="+name);
        }
        if(map.get(name) == null) {
            try {
                map.put(name, (Singleton3) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
    //一个示意性的商业方法
    public String about() {    
        return "Hello, I am RegSingleton.";    
    }    
    public static void main(String[] args) {
        Singleton3 single3 = Singleton3.getInstance(null);
        System.out.println(single3.about());
    }

}


总结

有两个问题需要注意:

1、如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类  装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。

 2、如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。

对于第一个问题修复的办法是:

public class Singleton{

	@SuppressWarnings({ "unused", "rawtypes" })
	private static Class getClass(String classname) throws ClassNotFoundException {
		
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     

		if(classLoader==null){
			classLoader = Singleton.class.getClassLoader();     
		}
		return (classLoader.loadClass(classname));     
	}
	

}

对于第二个问题修复的办法是:

import java.io.Serializable;

@SuppressWarnings("serial")
public class Singleton implements Serializable {

	public static Singleton INSTANCE = new Singleton();

	public Singleton() {
		// TODO Auto-generated constructor stub
	}

	private Object readResolve() {
		return INSTANCE;
	}

}

对我来说,我比较喜欢第三种和第五种方式,简单易懂,而且在JVM层实现了线程安全(如果不是多个类加载器环境),一般的情况下,我会使用第三种方式,只有在要明确实现lazy loading效果时才会使用第五种方式,另外,如果涉及到反序列化创建对象时我会试着使用枚举的方式来实现单例,不过,我一直会保证我的程序是线程安全的,而且我永远不会使用第一种和第二种方式,如果有其他特殊的需求,我可能会使用第七种方式,毕竟,JDK1.5已经没有双重检查锁定的问题了。






  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值