前言
确保一个类只有一个实例,并且自行实例化向整个系统提供这个实例
单例模式优缺点
优点
- 节省内存开支
- 减少系统性能开销
- 可以避免对资源的多重占用。比如写文件操作,由于只有一个实例在内存中,避免了对同一个资源文件的同时写操作
- 优化和共享资源访问。可以设置一个单例类作为全局访问点
缺点
- 单例模式一般没有接口,扩展很困难
- 对测试是不利的,在并行开发中,如果单例模式没有完成,是无法进行测试的
- 与单一职能原则有冲突。一个类应该只实现一个逻辑,而不关系它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中
实现方式
提起单例模式,应该都不陌生,最先想到的应该就是经典的懒汉式和饿汉式,下面对单例模式的几种实现方式进行介绍:
- 饿汉式
在第一次引用该类的时候,不管实际是否需要创建,都会创建对象实例
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return instance;
}
}
- 懒汉式
延迟加载,在需要域的值时才会创建。
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) instance = new LazySingleton();
return instance;
}
}
延迟初始化的问题:降低了初始化类的或者创建实例的开销,却增加了访问被延迟初始化域的开销。如果域只在类的实例部分被访问,并且初始化这个域的开销很高,就值得延迟初始化。
上面是经典的两种写法,但是不是线程安全的,下面考虑线程安全的写法:
- 线程安全的写法
public class SyncSingleton {
private static volatile SyncSingleton instance = null;
private SyncSingleton() {}
public static SyncSingleton getInstance() {
synchronized (SyncSingleton.class) {
if (instance == null) {
instance = new SyncSingleton();
}
}
return instance;
}
}
这种写法考虑到了线程安全,使用Synchronized加锁,同时使用了volatile关键字进行了限制,虽然保证了对所有线程的可见性,但是这种写法效率比较低,无法实际应用。
- 双重检查锁模式(DDL)
public class DclSingleton {
private static volatile DclSingleton instance = null;
private DclSingleton() {}
public static DclSingleton getInstance() {
if (instance == null) {//first check(no locking)
synchronized (DclSingleton.class) {
if (instance == null) {//second check(with locking)
instance = new DclSingleton();
}
}
}
return instance;
}
}
此种模式较上一种模式虽然多了一层null检查,因为在单例中new的情况比较少,绝大多数都是读操作,因此在加锁前多一次检查,可以减少绝大多数的加锁操作,从而提高效率。
- 静态内部类的写法
public class StaticSingleton {
private StaticSingleton() {}
private static class SingletonHolder {
private final static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
}
到这里有必要说一下静态内部类,静态内部类的加载不依赖于外部类,在使用的时候才会被加载。
第一次检查时没有锁定,看看这个域是否被初始化,第二次检查时有锁定,只有当第二次检查时表明这个域没有被初始化才会对这个域进行初始化。
下面验证一下静态内部类的延迟加载:
public class StaticSingleton {
private StaticSingleton() {}
private static class SingletonHolder {
private final static StaticSingleton instance = new StaticSingleton();
static {
System.out.println("内部类被加载");
}
}
static {
System.out.println("外部类被加载");
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
public static void main(String[] args) {
StaticSingleton singleton = null;
try {
Class<StaticSingleton> singletonClass = (Class<StaticSingleton>) Class.forName("com.lisp.controller.StaticSingleton");
System.out.println("测试外部类" + singletonClass);
//System.out.println(StaticSingleton.getInstance());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
上面的两个静态代码块是为了验证是否执行内部类和外部类
当执行第一个输出语句时,控制台输出如下:
外部类被加载
测试外部类class com.lisp.controller.StaticSingleton
执行第二个输出语句时,输出如下:
外部类被加载
内部类被加载
可以得到结论:
静态内部类的加载不依赖于外部类,在使用的时候才会被加载,但加载静态内部类的时候,外部类也会被加载
以上的方法都有共同缺点: * 需要额外的工作支持序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。 * 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
- 枚举方式的写法
public enum EnumSingleton {
INSTANCE;
// 这里隐藏了一个空的私有构造方法
private Singleton () {}
}
对于一个标准的枚举单例模式,最好的写法还是实现接口的形式:
public interface MySingleton {
void doSomething();
}
enum Singleton implements MySingleton {
INSTANCE {
@Override
public void doSomething() {
System.out.println("complete singleton");
}
};
public static MySingleton getInstance() {
return Singleton.INSTANCE;
}
}
使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
补充
以下是摘自Effective Java中的代码
- 实例域双重检查锁
public class FieldType {
private volatile FieldType instance;
private FieldType() {}
public FieldType getInstance() {
FieldType result = instance;
if (result == null) {
synchronized (this) {
result = instance;
if (result == null) {
instance = result = new FieldType();
}
}
}
return result;
}
}
result的作用是确保field只在已经被初始化的情况下读取一次,可以提升性能
- 实例域单重检查锁
public class FieldType {
private volatile FieldType instance;
private FieldType() {}
public FieldType getInstance() {
FieldType result = instance;
if (result == null) {
instance = result = new FieldType();
}
return result;
}
}
对于实例域可以使用双重检查模式,对于静态域可以使用holder模式,对于可以接受的重复初始化的实例域可以考虑单重检查模式。
应用场景
- 生成唯一的序列号
- 在整个项目中需要一个共享访问点或共享数据。比如web页面上的计数器等
- 创建一个对象消耗的资源过多
- 需要定义大量的静态常量和静态方法,如工具类,当然也可以直接声明为static的形式