设计模式——单例模式

单例模式

单例模式:代码中多次获取一个类的对象,这多次获取的结果都是同一个对象,这个对象就是一个单例对象。

常见的单例模式

既然单例不允许有多个重复的对象,而一般情况下对象都是通过构造函数创建的,na那么单例模式必定不能让用户能够调用到构造函数。

饿汉式单例

之所以叫做饿汉式,是因为这个唯一的实例在一开始的时候就会被创建好了,显得很急切,就像一个饿汉一样哪里忍得了把饭放着不吃。

// 看红色字体知道必须私有化构造方法
public class HungerySingleton {
    // 私有化构造方法
    private HungerySingleton(){}
    
    private static final HungerySingleton instance = new HungerySingleton();
    
    public static HungerySingleton getInstance(){
        return instance;
    }
}

其中static借用了Java的类加载时,会加载静态属性的特点完美解决该实例只会被加载一次。getInstance()提供给外接一个获取实例的接口。final关键字起到的作用是限制后续再来修改该属性。
简单测试下:

public class HungryTest {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                HungerySingleton instance = HungerySingleton.getInstance();
                System.out.println(instance);
            }).start();
        }
    }
}

输出结果:

hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2

二十个线程输出的结果一致,因此这二十个对象均属同一个对象。

懒汉式单例

小时候偷懒被老人发现,老人就给我讲了一个故事。一个人因为太懒,不想动手端碗吃饭,于是把烙饼内圈掏空,挂在自己的脖子上,饿了就嘴咬,但是后来嘴边的饼咬完了,懒得把饼转一下,最后饿死了。
懒汉式就是就像小时候放暑假,一定要等到快开心了才去补作业。在类加载的时候不创建实例,等到用的时候才去进行初始化。

public class SimpleLazy {
    // 私有化构造函数,让用户无法通过构造函数创建实例
    private SimpleLazy(){}
    // 不进行初始化
    private static volatile SimpleLazy instance = null;
    // 提供给外界获取实例的接口
    public static SimpleLazy getInstance(){
        if(instance == null){
            instance = new SimpleLazy();
        }
        return instance;
    }
}

这是一个不完美的单例模式,单线程的情况下能保证单例,但是在高并发的情况下,当两个线程同时运行到if(instance == null),则两个线程的判断结果都为true,因此会出现两个实例,且第二个创建的实例会覆盖掉第一个。此时单例无疑是已经被破坏看了。
同样用上方的测试类的代码,做简单的修改:

public class SimpleLazyTest {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                SimpleLazy instance = SimpleLazy.getInstance();
                System.out.println(instance);
            }).start();
        }
    }
}

最后输出结果:
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@edfb547
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
第二个线程获取的对象就开始不老实了,这才是二十个线程就开始出现非单例,所有高并发情况下是不能使用这种单例的。
为了解决并发下的懒汉式单例,可以引入锁。加入在方法上加上排它锁,让访问这个方法的线程进行排队单个进入,那也就没有破坏单例的机会了。

public class SimpleLazy {
    // 私有化构造函数,让用户无法通过构造函数创建实例
    private SimpleLazy(){
    public class SimpleLazy {
    // 私有化构造函数,让用户无法通过构造函数创建实例
    private SimpleLazy() {
    }

    // 不进行初始化
    private static volatile SimpleLazy instance = null;

    // 提供给外界获取实例的接口
    public static SimpleLazy getInstance(){
    // public static synchronized SimpleLazy getInstance() {
        if (instance == null) {
            synchronized (SimpleLazy.class) {
                if(instance == null) {
                    instance = new SimpleLazy();
                }
            }
        }
        return instance;
    }
    // 提供给外界获取实例的接口
    // public static SimpleLazy getInstance(){
    public static synchronized SimpleLazy  getInstance(){
        if(instance == null){
            instance = new SimpleLazy();
        }
        return instance;
    }
}

测试代码不变,结果显然是能保证并发的单例的,此处就不贴出测试结果了。

双重检查锁懒汉式代理

并发的目的是充分利用CPU的性能,加锁反而限制了线程并行,这与初心背道而驰了,但是性能和安全向来都是权衡利弊的关系,如果把锁的粒度再进一步细化,似乎就能接受了。

public class SimpleLazy {
    // 私有化构造函数,让用户无法通过构造函数创建实例
    private SimpleLazy() {
    public class SimpleLazy {
    // 私有化构造函数,让用户无法通过构造函数创建实例
    private SimpleLazy() {
    }

    // 不进行初始化
    private static volatile SimpleLazy instance = null;

    // 提供给外界获取实例的接口
    public static SimpleLazy getInstance(){
    // public static synchronized SimpleLazy getInstance() {
        if (instance == null) {
            synchronized (SimpleLazy.class) {
                if(instance == null) {
                    instance = new SimpleLazy();
                }
            }
        }
        return instance;
    }
    // 提供给外界获取实例的接口
    public static SimpleLazy getInstance(){
        if (instance == null) {
            synchronized (SimpleLazy.class) {
                if(instance == null) {
                    instance = new SimpleLazy();
                }
            }
        }
        return instance;
    }
}

这段可能存在争议的就是将最外层的if (instance == null)去掉是否合适,实际上去掉在外层的话,性能随着并发量的增大而降低。从一方面讲,去掉外层的判断后,代码和上边一个方法锁性能并无差异。另一方面,最外层的判断是阻断了第二次访问开始的请求,会直接返回实例,而不用再来获取锁之后再确定要不要创建对象。而内层的if (instance == null)是解决高并发的关键。外层的判断是存在并发的,因此可能出现多个线程在排队获取锁,因此如果没有内层的判断,当第一个线程释放锁之后,进入了第一层的线程会直接重新创建一个对象。
在这里插入图片描述

反射破坏饿汉式单例

熟悉反射的话就会知道反射中有这么一些方法Class.getDeclare***,这个Declare就是能够拿到private修饰的方法(字段,函数,构造函数等),接着还有一个setAccessible方法,者是一个很暴力的方法,可以将private修饰的方法、字段设置强行修改。因此通过反射获取到构造函数,然后设置强行修改,这样就能创建出一个新实例出来

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 正常获取单例实例
        HungerySingleton instance = HungerySingleton.getInstance();
        // 通过反射获取构造方法(包含private修饰的)
        Constructor<HungerySingleton> cons = HungerySingleton.class.getDeclaredConstructor();
        // 设置强行访问和修改
        cons.setAccessible(true);
        // 通过构造函数创建一个新的实例
        HungerySingleton consInstance = cons.newInstance();
        System.out.println(instance);
        System.out.println(consInstance);
        System.out.println(instance == consInstance);
    }

输出结果:

hungry.HungerySingleton@4554617c
hungry.HungerySingleton@74a14482
false

这样看来,饿汉式单例是不是就不安全了呢。Of courself not!既然反射会想到从构造函数进攻,那么我们就从构造函数进行防御。

public class HungerySingleton {
    // 私有化构造方法
    private HungerySingleton(){
        throw new RuntimeException("禁止通过构造函数创建单例Bean,请自重!");
    }

    private volatile static HungerySingleton instance = new HungerySingleton();

    public static HungerySingleton getInstance(){
        return instance;
    }
}

通过阻断构造函数的方式阻止用户通过反射强行创建实例。

序列化破坏单例
public class HungerySingleton implements Serializable {
    private HungerySingleton(){
   		throw new RuntimeException("禁止通过构造函数创建单例Bean,请自重!");
    }
    private static final HungerySingleton instance = new HungerySingleton();
    public static HungerySingleton getInstance(){
        return instance;
    }
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
    // 将单例的实例进行序列化到文件
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
    HungerySingleton dc = HungerySingleton.getInstance();
    objectOutputStream.writeObject(dc);
    // 从文件中反序列化到实例
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("tempFile"));
    HungerySingleton o = (HungerySingleton)objectInputStream.readObject();
    System.out.println(o==dc);
}

输出结果:

false

要解决这个问题似乎就很难了,但是我们不难想出与反序列化有关,因此这里不得不去看源码了。
如果还不熟悉看源码的方式,可以通过Debug来看,如果是老司机,那就凭着感觉去跟。切忌因小失大,不必拘泥于细节。限于篇幅,此处直接贴出调用栈直达关键代码处。

其中黑色背景下的高亮行表示此处将调用其他方法。
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
到了这里,将目光瞪到

Object obj;
try {// 判断构造方法是否为空(一定不会为空的),因此一定会new一个对象用obj接收
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(
        desc.forClass().getName(),
        "unable to create instance").initCause(ex);
}

其中三元表达式obj = desc.isInstantiable() ? desc.newInstance() : null;的意思是当desc.isInstantiable()返回true,则进行desc.newInstance(),否则返回null。于是将目光转移到isInstantiabl()方法。

boolean isInstantiable() {
    requireInitialized();
    return (cons != null);
}

主要判断cons(cons是构造函数private Constructor<?> cons;)是否为空。这是一个类,构造函数当然不会为null,所以必定会执行desc.newInstance()方法(该方法的核心内容是return cons.newInstance();,即通过构造方法创建的)。因此反序列化后,相当于创建了一个新的对象了。
而且无法通过限制构造函数的方式来阻止,因为构造函数不执行成功,反序列化就失败了。

再看这段代码:

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{// 调用readResolve方法获取到一个对象
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}

return obj;

虽然最后返回的还是obj(通过构造函数创建的实例),但是可以看到有这么个函数handles.setObject(passHandle, obj = rep);,从函数的命名看似乎是设置某个值得意思,而第二个参数竟然是将rep赋值给了obj。
如果这个方法能够保证一定执行,name就只会存在一个实例了。因此再看上方进入这部分代码的条件。

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{

handles.lookupException(passHandle) == null可以不用管,直接看 desc.hasReadResolveMethod())

/**如果当前类可以(被序列化 或者 externalizable(也是实现序列化的接口)),而且定义了readResolve方法则返回true,否则返回false
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method.  Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
   requireInitialized();
   return (readResolveMethod != null);
}

readResolveMethodprivate Method readResolveMethod;意思就是说如果被序列化和反序列化的类中,如果有这个方法,就会就如我们期望的代码块。

但是这个方法具体需要做什么呢?此外就算是最后只返回一个实例,怎么就能保证序列化之前和序列化之后返回的实例是同一个呢?

这是我们抛出的问题,既然问题抛出来了,我们的目标也更加明确了,第一:要定义这个方法;第二:保证序列化前和反序列化后的实例是同一个。
于是大胆地这样写:

public Object readResolve(){
    return instance;
}

用原来的测试方法一测,居然没翻车。
因此对于单例模式,如果想要阻止反射破坏单例,则在构造函数中抛异常。想要阻止序列化反序列化破坏单例,就写readResolve方法。当然成年人都是All In。这两个保障都加上去。

匿名内部类单例

只有外部方法调用时,内部类的逻辑才会执行,因此利用JVM底层执行逻辑,完美避免了线程安全问题

public class InternalSingleton {
    private InternalSingleton(){
        if(InnerClass.instance != null){
            throw new RuntimeException("请通过getInstance方法获取实例!");
        }
    }

    public static InternalSingleton getInstance(){
        return InnerClass.instance;
    }

    private static class InnerClass{
        private static final InternalSingleton instance = new InternalSingleton();
    }
}

需要注意的是该单例实例仅仅能通过内部类调用外部类的构造方法来实例化,由于实例放在了内部列中,被final修饰,因此只会被实例化一次。外部类的构造函数要保证第一次正常调用,第二次不允许调用,因此加上了判断。
测试

public class InternalSingletonTest {
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                InternalSingleton instance = InternalSingleton.getInstance();
                System.out.println(instance);
            }).start();
        }
    }
}

测试结果就不贴出来了,多线程下依然单例。

容器式单例

通过能存储不可重复的数据,并且能支持并发的集合来将单例的实例注册起来,从而实现单例。

public class ContainnerSingleton {
    // 支持并发的容器
    private static ConcurrentHashMap<String,ContainnerSingleton> container = new ConcurrentHashMap<>();
    // 私有化构造方法
    private ContainnerSingleton(){
        if(container.size() > 0){
            throw new RuntimeException("禁止通过构造方法创建实例!");
        }
    }

    public static ContainnerSingleton getInstance(String url) {
        // 不允许用""和null作为键
        if(null == url ||"".equals(url)){
            return null;
        }else if(!container.containsKey(url)){
            // 当对象不存在的时候旧创建
            // 使用并发容器的putIfAbsent方法防止重复添加
            container.putIfAbsent(url,new ContainnerSingleton());
        }
        return container.get(url);
    }
}

因为putIfAbsent方法的的存在,容器中的相同key的对象无法被覆盖。构造函数添加验证仅仅是防止反射直接创建实例,同时也可以加上readResolve方法防止序列化破坏单例。

写给自己的

打心底讲,目前对于枚举,我本身很不熟悉,因为用得很少。枚举实际上也可以实现单例,而且枚举实现的单例用序列化无法破坏,这部分先给自己立一个flag,后续一定会更新枚举实现单例,并完整解析为什么序列化反序列化无法破坏枚举单例。

时间:2021年9月20日23:35:30

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值