一、单例模式简介
单例模式是指一个类在任何情况下都保证只有一个实例,并提供一个全局访问点。特点是构造方法私有化、实例的引用编号私有化以及实例访问方法的公有化。J2EE中的ServletContext、Spring中的ApplicationContext以及数据库连接池等都是单例模式。
二、饿汉式单例模式
饿汉式单例模式在类加载时即初始化并创建单例对象。
例:
public class HungrySingletonPattern {
//饿汉式单例模式类加载时初始化并创建对象,优点是不加任何锁,执行效率较高,但可能造成内存浪费
private static final HungrySingletonPattern hungrySingletonPatternInstance = new HungrySingletonPattern();
//将构造方法私有化,不允许调用者自行构建对象。
private HungrySingletonPattern() {
if(null != hungrySingletonPatternInstance) {
throw new RuntimeException("不允许创建多个实例");
}
};
public static HungrySingletonPattern getHungrySingletonPatternInstance() {
return hungrySingletonPatternInstance;
}
}
我们可以看到,该模式下,在加载单例类时就new了单例类实例,并用private关键词修饰了构造方法,同时对外提供了一个获取该实例的方法。饿汉式单例是绝对线程安全的,不需加任何锁,但是由于不论用不用,都会创建实例,因此有可能造成内存浪费。
三、懒汉式单例模式
与饿汉式单例在类加载时创建实例不同,懒汉式单例模式只在外部类调用时才去创建实例。
例1:双重检查锁实现方式
public class LazySingletonPattern {
private static LazySingletonPattern singleton = null;
private LazySingletonPattern() {
if(null != singleton) {
throw new RuntimeException("不允许创建多个实例!");
}
};
public static LazySingletonPattern getLazySingletonPatternInstance() {
//双重检查锁,防止多线程调用时,第一个线程未生成实例,第二个线程刚好走到null判断处,从而通过判断,生成多个实例的情况。
if(null == singleton) {
synchronized (LazySingletonPattern.class) {
if(null == singleton) {
singleton = new LazySingletonPattern();
}
}
}
return singleton;
}
}
实际上就是将饿汉式单例模式中创建实例的动作从声明实例变量处延迟到了对外部提供单例实例的静态方法中。该例中应用了双重检查锁,防止A线程正在创建实例,B线程刚好走至null判断处,从而造成多次创建实例的情况。此方法使用了锁,因此线程多时,会造成性能问题。
为了进一步解决性能问题,还有一种更牛的实现方式,如下
例:静态内部类的实现方式
public class StaticInnerLazySingletonPattern {
private StaticInnerLazySingletonPattern() {
if(null != LazyHolder.LAZY) {
throw new RuntimeException("不允许创建多个实例!");
}
}
public static final StaticInnerLazySingletonPattern getLazySingleton() {
return LazyHolder.LAZY;
}
//在返回结果前调用此内部类,实例化对象
private static class LazyHolder {
private static final StaticInnerLazySingletonPattern LAZY = new StaticInnerLazySingletonPattern();
}
}
如上实例,又将双重锁实现方式中创建对象的动作,应用静态内部类的特性(默认不加载),推迟到一个私有化的静态内部类LazyHolder中,加上final修饰,从而保证线程安全,兼顾了饿汉式内存资源的浪费问题以及synchronized造成的性能问题。
四、枚举式单例模式
jdk1.5出现了枚举类,由于枚举类的特性(枚举对象天生单例),Effective Java中称枚举式单例是单例模式的最优解。但我们在应用单例模式时,也无需硬套成枚举单例模式,因为应用背景不同,单例的各种实现方式的难易程度不同。
例:
public enum EnumSingletonPattern {
INSTANCE("01","helloworld!");
EnumSingletonPattern(String code, String content){
}
}
天生的单例,不用太多修饰,几行代码实现单例,还防反射破坏、序列化破坏。
我们看下反编译后的代码:
public final class EnumSingletonPattern extends Enum
{
public static final EnumSingletonPattern INSTANCE;
private static final EnumSingletonPattern $VALUES[];
public static EnumSingletonPattern[] values()
{
return (EnumSingletonPattern[])$VALUES.clone();
}
public static EnumSingletonPattern valueOf(String s)
{
return (EnumSingletonPattern)Enum.valueOf(design/method/EnumSingletonPattern/EnumSingletonPattern, s);
}
private EnumSingletonPattern(String s, int i, String s1, String s2)
{
super(s, i);
}
static
{
INSTANCE = new EnumSingletonPattern("INSTANCE", 0, "01", "helloworld!");
$VALUES = (new EnumSingletonPattern[] {
INSTANCE
});
}
}
从以上代码,可以看出,enum继承了Enum类,构造方法天生是私有化的,不需额外声明;其中就有一个名为ENUM$VALUES的常量,是个private static final的数组。在枚举类初始化的时候(静态代码块中赋值),会实例化所有的枚举对象然后按顺序放在这个数组中。
因此,枚举单例是天生的饿汉式单例。
那为何枚举单例还能防反射、序列化破坏呢?这个需要看其源码实现,用以下代码实验反射创建实例:
Class clazz = EnumSingletonPattern.class;
Constructor e =clazz.getDeclaredConstructor(String.class,int.class,String.class,String.class);
e.setAccessible(true);
Object c = e.newInstance("1",666,"1","1,w,3");
抛异常:
Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at design.method.hungrySingleton.SingletonTest.main(SingletonTest.java:32)
从字面上就可以看出jdk不允许用反射创建枚举类实例,从Constructor源码看,有这么一个判断:
if ((this.clazz.getModifiers() & 16384) != 0) {
throw new IllegalArgumentException("Cannot reflectively create enum objects");
} else {
这个16384就是ENUM类型,private static final int ENUM = 16384;当修饰符是ENUM抛异常。
为什么能防反序列化破坏单例呢?跟一下readObject源码,可以看大readenum的时候,是从enumConstantDirectory 中取值的,enumConstantDirectory 的声明private transient volatile Map<String, T> enumConstantDirectory = null;可以看到是一个Map,Enum中定义的Enum成员值都被缓存在了这个Map中,Key是成员名称(比如“INSTANCE”),Value就是Enum的成员对象。这样的机制天然保证了取到的Enum对象是唯一的,也就防止了反射生成多实例的情况。
五、容器式单例
容器式单例用Map实现,一般以类名,实例名为键值对存放进私有Map中;容器式单例适用于管理的实例非常多的情况。非线程安全。
例:
public class ContainerSingletonPattern {
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getBean(String className) {
synchronized (ioc) {
if(!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
}catch(Exception e) {
e.printStackTrace();
}
return obj;
}
else {
return ioc.get(className);
}
}
}
}
基于ThreadLocal的线程单例实现
ThreadLocal不能保证其创建的对象全局唯一,但能够保证在单个线程中是唯一的。
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> threadLocalInstance=
new ThreadLocal<ThreadLocalInstance>(){
@Override
protected ThreadLocalInstance initialValue() {
return new ThreadLocalInstance();
}
};
private ThreadLocalInstance(){}
public static ThreadLocalInstance getInstance(){
return threadLocalInstance.get();
}
}