一文拿下单例模式的七种写法
单例模式简介
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
比如Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象。SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个SessionFactory就够,这是就会使用到单例模式。
这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
在讲单例之前,要做一次基础知识的科普行动。大家都知道Java类加载器加载内容的顺序:
1、从上往下(Java的变量需要先声明才能使用)
2、静态后动态(对象实例化)(静态块和static关键字修饰在实例化以前分配内存空间)
3、先属性后方法(成员变量不能定义在方法中,只能定义在class下)
4. 类被加载器加载时时线程安全的,这个是jvm类加载器保证的
核心
我们在写项目的时候 单例我们其实用的很多甚至说是无处不在,但是我们为啥很多同学并不是很熟悉,一方面是因为太多了平时不怎么不关注,更重要的是被面试官吊打过,对他有心理阴影,感觉这个东西很难,甚至很深,今天看了我的这一篇文章让你解开单例的面纱,再也不怕面试官吊打
不要看这篇文章很长下面的三句话记住绝对很简单:
1. 只有private构造方法,确保外部无法实例化;
2. 通过private static变量持有唯一实例,保证全局唯一性;
3. 通过public static方法返回此唯一实例,使外部调用方能获取到实例。
当你理解了上面的三句话还管他集中写法吗? 爱几种几种,只是不同的变种而已,
public class Singleton {
//1.构造函数私有化
private Singleton(){}
//2. 静态字段引用唯一实例:
private static final Singleton INSTANCE=new Singleton();
}
这是完成了上面的的三步中的前两步,但是我们如何给外部提供对象那,你说使用一个静态方法可以吗?或者吧INSTANCE变量直接privat修改成public暴露出去可以吗完全没问题只要你能提供就可以了
完全没问题吧,但是我们项目中还是更多的是使用静态方法的方式比较多写,就是一个习惯,或者说这个实例并不是很容易创建,所以还是方法比较合适;
public class Singleton {
//1.构造函数私有化
private Singleton(){}
//2. 静态字段引用唯一实例:
private static final Singleton INSTANCE=new Singleton();
//3.通过静态方法来获取
public static Singleton getInstance(){
return INSTANCE;
}
}
class test1{
public static void main(String[] args) {
// System.out.println(Singleton.INSTANCE.hashCode());
// System.out.println(Singleton.INSTANCE.hashCode());
// System.out.println(Singleton.INSTANCE.hashCode());
// System.out.println(Singleton.INSTANCE.hashCode());
System.out.println(Singleton.getInstance().hashCode());
System.out.println(Singleton.getInstance().hashCode());
System.out.println(Singleton.getInstance().hashCode());
System.out.println(Singleton.getInstance().hashCode());
}
}
项目当中就使用这种方式即可,到现在很多同学还不行估计,可能认为你不会就别瞎忽悠了,不会说不会的吧…我现在有一百个不服气不信就写写看看几个方式
饿汉
上面的写法就是平时说的饿汉,不管你是通过静态变量还是静态方法都算一种吧.既然说有那么多种写法我们就得对比这说是优缺点吧
优缺点说明:
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类
装载,这时候初始化instance就没有达到lazy loading的效果 - 结论:这种单例模式可用,可能造成内存浪费
简单说: 线程安全,内存有可能浪费
我们写的代码程序一次都不用的概率不高,不然不白写了,这也是我推荐的写法的理由,简单,安全安全
懒汉四种写法
饿汉就是针对懒汉内存有可能浪费进行的改良,啥意思,就是类初始化的时候不进行创建,在第一次使用的时候进行创建,比较简单:
第一种写法: 错误的写法不能使用
public class Singleton1 {
private Singleton1() {
}
public static Singleton1 singleton1 = null;
public static Singleton1 getInstance(){
singleton1=new Singleton1();
}
return singleton1;
}
}
单线程是完全没问题的
从下图可以看出来多线程不安全,因为不是单例的
那怎么着啊,那就改成线程安全的吧,不能因为这个小问题就把我们难住吧,走起最简单的就是加一个synchronized
第二种写法 在getInstance方法上加同步synchronized,效率低不推荐使用
public class Singleton2 {
// 1. 构造方法私有化
private Singleton2() {
}
//2、然后声明一个静态变量保存单例的引用
public static Singleton2 singleton1 = null;
//3.静态方法获取实例
不安全的
public static synchronized Singleton2 getInstance(){
if (singleton1==null){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton1=new Singleton2();
}
return singleton1;
}
}
多线程也是安全的了吧,但是吧我们都知道synchronized是重量锁,这样吞吐量又不高了,我们怎么解决这个吞吐量的问题
- 解决了线程不安全问题
- 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
- 结论:在实际开发中,不推荐使用这种方式
第三种 双重检查,推荐使用
我们知道可以使用静态块解决上面每次都进行同步的过程,
public class Singleton3 {
private Singleton3() {
}
//2、然后声明一个静态变量保存单例的引用
public static Singleton3 singleton1 = null;
//3.静态方法获取实例
不安全的
public static volatile Singleton3 getInstance(){
if (singleton1==null){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Singleton3.class){
singleton1=new Singleton3();
}
}
return singleton1;
}
}
多线程下我们发现并不是单例模式,
- 这种方式,本意是想对上面实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
- 但是这种同步并不能起到线程同步的作用。假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,
另一个线程也通过了这个判断语句,这时便会产生多个实例 - 结论:在实际开发中,不能使用这种方式
说白了这个就是错误的写法我们改良成下面的方案:
public class Singleton3 {
// 1. 构造方法私有化
private Singleton3() {
}
//2、然后声明一个静态变量保存单例的引用
public static volatile Singleton3 singleton1 = null;
//3.静态方法获取实例
不安全的
public static Singleton3 getInstance(){
if (singleton1==null){
synchronized (Singleton3.class){
if (singleton1==null){
singleton1=new Singleton3();
}
}
}
return singleton1;
}
}
优缺点说明:
- Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两
次if (singleton == null)检查,这样就可以保证线程安全了。 - 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),
直接return实例化对象,也避免的反复进行方法同步. - 线程安全;延迟加载;效率较高
- 结论:在实际开发中,推荐使用这种单例设计模式
第四种 静态内部类应用实例 推荐使用
public class Singleton4 {
private Singleton4() {
}
public static final Singleton4 getInstance(){
return SingletonInstance.INSTANCE;
}
static class SingletonInstance{
public static final Singleton4 INSTANCE=new Singleton4();
}
}
优缺点说明:
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
- 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论:推荐使用.
枚举 (没见过)
实现Singleton的方式是利用Java的enum,因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可:
public enum Singleton5{
// 唯一枚举:
INSTANCE;
private String singleton5;
public String getSingleton5() {
return singleton5;
}
public void setSingleton5(String singleton5) {
this.singleton5 = singleton5;
}
}
使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。
这种方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我从来没这么写过,也没见过这么写的代码
登记式单例(1种写法,不常用)
public class Singleton6 {
private static Map<String,Singleton6> map = new HashMap<String,Singleton6>();
static{
Singleton6 single = new Singleton6();
map.put(single.getClass().getName(), single);
}
//保护的默认构造子
protected Singleton6(){}
//静态工厂方法,返还此类惟一的实例
public static Singleton6 getInstance(String name) {
if(name == null) {
name = Singleton6.class.getName();
}
if(map.get(name) == null) {
try {
map.put(name, (Singleton6) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
}
登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。
这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。
总结
懒汉式单例跟饿汉式单例的根本区别
从名字上来说,饿汉和懒汉,
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,
而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。
另外从以下两点再区分以下这两种方式:
1、线程安全:
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。
2、资源加载和性能:
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
至于1、2、3这三种实现又有些区别,
第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,
第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。
分布式环境下的单例
有两个问题需要注意:
1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
对第一个问题修复的办法是:
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null){
classLoader = Singleton.class.getClassLoader();
}
return (classLoader.loadClass(classname));
}
对第二个问题修复的办法是:
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() { }
private Object readResolve() {
return INSTANCE;
}
}
单例模式在JDK 应用的源码分析
jdk也是直接使用的饿汉,我非常推荐的也是这个简单好用
单例模式注意事项和细节说明
- 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需
要频繁创建销毁的对象,使用单例模式可以提高系统性能 - 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使
用new - 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或
耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数
据库或文件的对象(比如数据源、session工厂等)
我们在web项目中更多的是使用spring,将类依赖spring容器管理,这样获取到的都是单例的
@Component // 表示一个单例组件
从上面看过了,除了不能用的,不常用的,不推荐的之外好像也没多少比较好用的了,平时我在项目中使用的比较多的就是懒汉,和静态内部类,同学们你都明白了,你的项目中使用多哪几种方式,欢迎评论区说下