一.定义
保证一个类仅有一个实例,并提供一个全局访问点
二.类型
创建型
三.适用场景
想确保任何情况下绝对只有一个实例,例如数据库连接池或者工厂等比较重的且线程安全的对象
四.优点
1.在内存里只有一个实例,减少内存开销
2.避免对资源的多重占用
3.设置全局访问点,严格控制访问
五.缺点
没有接口,扩展困难
六.重点
1.私有构造器
2.线程安全
3.延迟加载
4.序列化和反序列化安全
5.防御反射攻击
七.giao,单例模式的N种创建方式
1.懒汉式
package com.sunyard.factory.singleton;
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton(){ } //构造函数私有化
//加锁
public static synchronized LazySingleton getInstance(){
if(lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
注意:这种方式可以保证线程安全,但是锁太重,每次调用getInstance方法都会去获取synchronized锁,严重影响获取单例对象的效率,不推荐。
2.双重检测式
package com.sunyard.factory.singleton;
public class LazyDoubleCheckSingleton {
private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){ }
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
// 1.分配内存对象
// 2.初始化对象
// 3.设置LazyDoubleCheckSingleton指向刚分配的内存地址
//2.3顺序不固定
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
双重检测式创建单例模式,一定要记得用volatile关键字修饰单例对象的引用。当我们new一个对象时,表面上我们只调用的new 关键字,其实底层是分三步的:
(1)堆内存开辟一块空间
(2)初始化这个对象
(3)将对象的引用指向开辟的这块内存空间
其中(2),(3)两步是没有顺序性的,由于指令重排序的原因,很有可能是先执行步骤(3),再执行步骤(2)。那么在高并发的情况下,A线程先执行步骤(3)还没有来得及执行步骤(2),B线程一看对象的引用已经有了,认为对象已经创建完成了,直接返回了这个对象,就会导致不可预估的问题。
那么为什么volatile就可以解决这个问题呢?首先跟大家介绍一下volatile关键字的作用:
(1)内存可见
(2)防止指令重排
volatile产生内存屏障,禁止创建对象中的步骤(2)和(3)的重排序,使他们严格按照顺序执行,从而解决上述问题。
注意:volatile不管是实际运用中还是面试中都是高频知识点,后面我也会出一期关于volatile关键字的文章,这里就不多做阐述了,大家先记住结论就好。
3.静态内部类
package com.sunyard.factory.singleton;
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){};
private static class InnerClassHolder{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClassHolder.staticInnerClassSingleton;
}
}
利用JVM类加载的特性保证单例对象的创建,不需要加锁。这里提一个小知识点。
JVM在什么情况初始化一个类:
1)遇到字节码指令 new putstatic getstatic invokestatic
2)执行主类会被初始化(main方法那个类)
3)当初始化一个类得时候,父类未初始化,会优先触发父类的初始化
4)使用java.lang.reflect包的方法对类进行反射调用的时候,若类没有进行过初始化,则需要先触发初始化
我们这里就是利用了第一条,执行了getstatic字节码指令,开始初始化InnerClassHolder类,从而获取了单例对象。
4.饿汉式
package com.sunyard.factory.singleton;
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
饿汉式代码相对简单,也是利用了JVM类加载的特性。
5.枚举式
package com.sunyard.factory.singleton;
public enum SingletonEnum {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
枚举为什么可以保证对象是单例的呢?
这里我们可以采用反编译工具来把枚举类进行反编译,从而研究其天然单例性的原理。小伙伴们可以点击jad反编译工具进行下载,然后通过jad -sjava xxx.class命令将class文件反编译成java文件。
现在我们打开反编译后的java文件
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SingletonEnum.java
package com.sunyard.factory.singleton;
public final class SingletonEnum extends Enum
{
public static SingletonEnum[] values()
{
return (SingletonEnum[])$VALUES.clone();
}
public static SingletonEnum valueOf(String name)
{
return (SingletonEnum)Enum.valueOf(com/sunyard/factory/singleton/SingletonEnum, name);
}
private SingletonEnum(String s, int i)
{
super(s, i);
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public static final SingletonEnum INSTANCE;
private String name;
private static final SingletonEnum $VALUES[];
static
{
INSTANCE = new SingletonEnum("INSTANCE", 0);
$VALUES = (new SingletonEnum[] {
INSTANCE
});
}
}
通过反编译后的代码我们就可以看到枚举类型是static final修饰的(意味着不可变),在静态代码块中创建对象,所以枚举可以认为是饿汉式的一种变式。
八.序列化+反序列化破坏单例的解决方案
1.单例模式真的单例吗?
我们都知道当我们需要任何情况下绝对只有一个对象时会采用单例模式进行创建对象,但是单例真的单例吗?请看下面的例子:
1.实现了Serializable的饿汉式单例
package com.sunyard.factory.singleton;
import java.io.Serializable;
public class HungrySingleton implements Serializable {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
2.通过序列化和反序列化测试
package com.sunyard.factory.singleton;
import java.io.*;
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
oos.writeObject(hungrySingleton);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
HungrySingleton newSingleton = (HungrySingleton) objectInputStream.readObject();
System.out.println(newSingleton == hungrySingleton);
// false
}
}
通过上面序列化和反序列化测试,发现我们创建了一个新的对象。
2.为什么通过序列化和反序列化可以破坏单例
查看ObjectInputStream中readObject()源码
ObjectInputStream的readObject()方法是调用反射创建对象的自然就会破坏单例结构
注意:枚举式单例不会被序列化和反序列化破坏,有兴趣的小伙伴可以看下readObject()方法对枚举类型的处理
2.如何解决
通过源码可以看到readObject()会判断是否有ReadResolveMethod,如果有就会执行该方法。
那么这个ReadResolveMethod到底是什么方法呢?
相信现在小伙伴们一定恍然大悟,我们只要在单例模式里新增下面的方法,就可以保证序列化和反序列化后还是同一个对象。但是这里我们要注意虽然该方法返回了原来的对象,但是也通过反射生成了新的对象,只是新对象没有被使用)
package com.sunyard.factory.singleton;
import java.io.Serializable;
public class HungrySingleton implements Serializable {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
//新增 readResolve方法
public Object readResolve(){
return hungrySingleton;
}
}
测试
package com.sunyard.factory.singleton;
import java.io.*;
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
oos.writeObject(hungrySingleton);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
HungrySingleton newSingleton = (HungrySingleton) objectInputStream.readObject();
System.out.println(newSingleton == hungrySingleton);
// true
}
}
九.反射攻击破坏单例模式解决方案
1.针对利用类加载时机创建单例模式的方案(饿汉式,静态内部类方式),可以在构造方法中判断对象是否已经创建,若创建就抛出异常,告诉调用者不能通过反射进行创建对象。
2.针对懒汉式 -> 无解
3.枚举类型 ->天然防御攻击(最佳方案)
十.克隆(原型模式)破坏单例模式的解决方案
1.不实现cloneable接口
2.重写Object类中的Clone方法返回getInstance();
小知识点:clone是浅拷贝,在使用clone构造对象的时候,一定要对深拷贝浅拷贝有一个深入的认识,不然可能就会出现难以预估的问题。后面有机会再写一篇文章来和大家一起探讨下深拷贝和浅拷贝相关的话题。
十一.JDK源码单例模式的应用
1.Runtime.getRuntime()
相信聪明的你一定可以看出来这是哪种单例模式了 ^ ^