一、定义及其优缺点
单例模式:整个程序有且仅有一个实例;该类负责创建自己的对象,同时确保只有一个对象被创建。
一般实现单例模式需要以下几点:
- 类构造器私有(一般为无参构造)
- 持有自己类型的私有属性(private static Singleton instance;)
- 对外提供获取实例的静态方法(public static Singleton getInstance())
优点:
- 单例模式在内存中只有一个实例,减少内存开支。(特别是一个对象需要频繁地创建销毁时)
- 单例模式只生成一个实例,减少系统的性能开销。(当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决)
- 单例模式可以避免对资源的多重占用。(例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作)
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问。(例如,可以设计一个单例类,负责所有数据表的映射处理)
缺点:
- 单例模式没有抽象层,扩展很困难;若要扩展,除了修改代码基本上没有第二种途径可以实现。
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 滥用单例将带来一些负面问题:如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
二、单例模式的几种实现及其优缺点
1.饿汉模式:单例对象非延迟加载,线程安全,比较常用,但容易产生垃圾,因为一开始就初始化
public class HungryManSingleton {
//1、私有化构造器
private HungryManSingleton() {
}
//2、持有自己类型的私有属性(类加载时,天然的线程安全,类初始化时,立即加载这个对象(唯一一个),缺乏延时加载的优势)
private static HungryManSingleton instance = new HungryManSingleton();
//3、暴露创建对象的方法(方法没有加同步,因此调用效率高)
public static HungryManSingleton getInstance() {
return instance;
}
}
2.懒汉模式(线程安全模式):实现延迟加载(懒加载)、线程安全,但效率低,每一次生成实例都要同步
public class LazyManSingleton {
//1、私有化构造器
private LazyManSingleton() {
}
//2、持有自己类型的私有属性,类初始化时,不初始化这个对象(延迟加载,真正用时再创建)
private static LazyManSingleton instance;
//3、暴露创建对象的方法(方法同步,因此调用效率低)
public static synchronized LazyManSingleton getInstance() {
if(null == instance) {
instance = new LazyManSingleton();
}
return instance;
}
}
3.双重检查锁模式:线程安全,延迟初始化,效率高
public class DoubleCheckSingleton {
// 私有化构造器
private DoubleCheckSingleton() {}
// volatile关键字禁止指令重排
private volatile static DoubleCheckSingleton instance;
// 双重检查锁
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
双重检查锁模式在创建实例的时候进行了双重判断,第一层判断是为了避免已经存在实例时,再次进入同步,由于避免了除第一次建立实例的同步操作,提升了效率;第二层判断是为了保证多线程下的同步操作,防止创建多个实例。
由于JVM内部的优化机制,有可能导致 instance = new DoubleCheckSingleton() 对象在创建时发生指令重排(对象创建过程很复杂,JVM会对其创建过程进行指令重排,导致提前释放锁),因此多线程下该模式存在风险;volatile关键字禁止指令重排,解决该问题。
4.静态内部类实现单例模式:线程安全、调用效率高、延迟加载(好于懒汉模式,可以优先选择)
public class StaticInnerClassSingleton {
//1、私有化构造器
private StaticInnerClassSingleton() {}
//2、静态内部类(延迟加载)
private static class Singleton{
private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
//3、暴露创建对象的方法,直接返回内部类的对象(内部类加载,天然线程安全)
public static StaticInnerClassSingleton getInstance() {
return Singleton.instance;
}
}
5.枚举实现单例模式:线程安全、调用效率高、避免了反射与反序列化的漏洞(非延迟加载,好于饿汉式)
public enum EnumSingleton {
//枚举元素,本身就是单例对象
INSTANCE;
//操作枚举元素即可
public void singletonOperation() {}
}
三、防止反射和反序列化破解单例模式(枚举天然防护此漏洞)
1.反射可以通过constructor.setAccessible(true);的方式,跳过private的防护,调用私有构造器,因此可以利用反射生成新的实例。为防止其破解单例模式,可以在构造器那里加以限制,代码如下:
public class LazyManSingletonCrack {
//1、私有化构造器
private LazyManSingletonCrack() {
//防止反射破解单例模式
if (instance != null) {
//若已经创建实例,主动抛出异常
throw new RuntimeException();
}
}
//2、持有自己类型的私有属性,类初始化时,不初始化这个对象(延迟加载,真正用时再创建)
private static LazyManSingletonCrack instance;
//3、暴露创建对象的方法(方法同步,因此调用效率低)
public static synchronized LazyManSingletonCrack getInstance() {
if(null == instance) {
instance = new LazyManSingletonCrack();
}
return instance;
}
}
2.反序列化时,会创建一个新的实例接收原来序列化后写到硬盘中的实例,从此破解单例模式;为防止其进行破解,可以在单例模式中定义readResolve()方法,让实例保持唯一,代码如下:
public class LazyManSingletonCrack implements Serializable{
//1、私有化构造器
private LazyManSingletonCrack() {}
//2、持有自己类型的私有属性
private static LazyManSingletonCrack instance;
//3、暴露创建对象的方法
public static synchronized LazyManSingletonCrack getInstance() {
if(null == instance) {
instance = new LazyManSingletonCrack();
}
return instance;
}
//反序列化时,如果定义了readResolve,可以将指定对象返回,不会返回反序列化后的新对象
private Object readResolve() throws ObjectStreamException{
return instance;
}
}
四、测试各种单例模式的性能
1.多线程环境下测试不同单例模式的效率,在本机运行环境下得到测试结果,测试代码如下
public class TestEfficiency {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
int threadNum = 10;//线程数量
//为了保证线程都执行完,再计算时间,使用线程计数器CountDownLatch,内部类不能调用局部变量,因此声明为final常量
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
//new threadNum 个线程
for (int i = 0; i < threadNum; i++) {
//内部类方式实现多线程
new Thread(new Runnable() {
@Override
public void run() {
//循环创建100000次
for (int i = 0; i < 1000000; i++) {
Object obj = LazyManSingleton.getInstance();
}
//线程结束后计数器减一
countDownLatch.countDown();
}
}).start();
}
//main线程进行等待(阻塞),直到所有线程结束
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
2.多线程环境下测试不同单例模式的效率,测试结果如下
饿汉式 | 25ms |
懒汉式 | 139ms |
双重检查锁 | 46ms |
静态内部类 | 31ms |
枚举 | 39ms |