概述
单例模式(Singleton pattern),从字名意思来看就知道只会产生一个实例对象,属于创建类型的一种常用设计模式
Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
一般这种专业的定义都写的晦涩难懂,我们来理解一下,一个类有且仅有一个实例,说的是这个类只会产生一个对象,任何其他对象对它的依赖都是一样的,访问的是同一个对象。自行实例化向整个系统提供,说的是单例模式不需要你自己去new一个对象,你去访问的这时候对象已经自行实例化好了,直接拿来主义即可。简单理解就是:
- 只会生产一个对象
- 拿来主义,不需要主动去new一个对象,直接拿来用就好
优缺点
优点
- 单例模式只在内存中产生了一个实例对象,减少了内存开支
- 减少系统性能的开销,比如创建一个对象需要消耗很多系统资源,如读取配置文件等,则可以直接使用单例来创建一个对象,常驻内存
- 全局访问对象。当系统中有一些共享资源需要全局唯一,就可以使用单例模式来实现
缺点
- 单例模式抽象,不容易扩展
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,比如,为了节省资源将数据库连接对象设置为单例,可能会导致共享连接池对象的程序过多而出现连接池溢出
使用场景
单例模式要求只能产生一个实例对象,那么产生多个对象会有问题的场景,都是单例的使用场景:
- 要求生成唯一序号的场景
- 系统中共享的数据
- 创建对象消耗的开销过大,也适合用单例模式
实现方式
/**
* 单例模式-饿汉式
*/
public class SingletonDemo1 {
private static SingletonDemo1 instance = new SingletonDemo1();
//构造函数私有化,禁止new对象
private SingletonDemo1(){
}
public static SingletonDemo1 getInstance(){
return instance;
}
}
饿汉式是最简单的单例模式实现,私有化构造函数,getInstance()方法直接返回一个内部静态实例对象,缺点很明显:单例对象一开始就创建好了,无论是否使用,对象都会进行初始化,假如对象一直没有被使用将造成一种资源浪费。
/**
* 单例模式-懒汉式
*/
public class SingletonDemo2 {
private static SingletonDemo2 instance = null;
//构造函数私有化,禁止new对象
private SingletonDemo2(){
}
public static synchronized SingletonDemo2 getInstance(){
if(instance == null){
instance = new SingletonDemo2();
}
return instance;
}
}
懒汉式解决了延迟初化化的问题,对象在使用时进行创建对象,但是在getInstance()方法上加了锁,同时只有一个线程可进行getInstance()的调用,线程安全但性能不高
/**
* 单例模式-Double CheckLock实现
*/
public class SingletonDemo5 {
private volatile static SingletonDemo5 instance;
private SingletonDemo5(){
}
public static SingletonDemo5 getInstance(){
if(instance == null){
synchronized (SingletonDemo5.class){
if(instance == null){
instance = new SingletonDemo5();
}
}
}
return instance;
}
}
Double CheckLock实现,由于Java编译器允许处理器指令重排序,故Double CheckLock实现存在安全隐患,不推荐使用
/**
* 单例模式-静态内部类方式
*/
public class SingletonDemo3 {
private static class SingletonClass{
private static final SingletonDemo3 instance = new SingletonDemo3();
}
//构造函数私有化,禁止new对象
private SingletonDemo3(){
}
public static SingletonDemo3 getInstance(){
return SingletonClass.instance;
}
}
静态内部类方式,可以实现延迟加载,线程安全,调用的效率高。
以上是单例模式最常见的三种实现方式,看似静态内部类是最好的实现方式,其实上面几种模式看似实现了单例模式,但都存在共同的缺陷。尽管构造函数被设置成私有的,但仍然可以通过反射来创建对象,这样就违背了单例的原则。
还有一种枚举的实现方式,可以解决以上几种实现方式的问题:
/**
* 单例模式-枚举方式
*/
public enum SingletonDemo4 {
INSTANCE;
public void method(){
System.out.println("doSomething");
}
}
public class SingletonTest {
public static void main(String[] args){
//直接访问method()方法
SingletonDemo4.INSTANCE.method();
}
}
枚举方式线程安全、调用效率高,可以天然防止反射、反序列化的调用,但它不能延迟加载
如何选择
我们看到单例模式看似简单,但实现起来并没有想像中的那么简单,需要考虑各种特殊的情况。那么,我们在实际使用过程中该如何选择使用哪种方式来实现呢?
- 当单例对象占用资源少,不考虑延迟加载时,枚举优于饿汉
- 当单例对象占用资源多,需要延时加载时,静态内部类优于懒汉
总结一下
- 单例模式只会产生一个对象,所有依赖访问的都是相同的对象
- 单例模式优点
- 减少了内存开支
- 减少系统性能的开销
- 资源全局统一访问
- 单例模式缺点
- 单例模式抽象,不容易扩展
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题
- 单例模式的使用场景
- 要求生成唯一序号的场景
- 系统中共享的数据
- 创建对象消耗的开销过大,也适合用单例模式
- 实现方式
- 饿汉式(线程安全,调用效率高,不能延时加载,可反射破坏单例)
- 懒汉式(线程安全,调用效率不高,但是能延时加载,可反射破坏单例)
- Double CheckLock实现方式
- 静态内部类方式(线程安全,调用效率高,可以延时加载,可反射破坏单例)
- 枚举方式(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用)
- 如何选择
- 当单例对象占用资源少,不考虑延迟加载时,枚举优于饿汉
- 当单例对象占用资源多,需要延时加载时,静态内部类优于懒汉