Java设计模式创建型模式:单例模式

Posted hans774882968

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java设计模式创建型模式:单例模式相关的知识,希望对你有一定的参考价值。

枚举类单例(推荐)

Effective Java书推荐使用这种单例模式。因为它足够简单,线程安全,且天然可以防止多实例化,即使是在面对反序列化和反射攻击时。

package singleton;

import java.lang.reflect.Method;

enum Singleton 
    instance;

    Singleton() 
        System.out.println("正在实例化,对象地址:" + hashCode());
    

    private String name;

    void setName(String name) 
        this.name = name;
    

    String getName() 
        return name;
    


public class EnumSingleton 
    public static void main(String[] args) throws ClassNotFoundException 
        Singleton.instance.setName("hans");
        System.out.println(Singleton.instance.getName());
        Singleton.instance.setName("acmer");
        System.out.println(Singleton.instance.getName());
        // 枚举类实现单例模式天然能对抗反射,反射拿不到构造方法
        Method[] methods = Class.forName("singleton.Singleton").getDeclaredMethods();
        for (Method method : methods) 
            System.out.println(method.getName());
        
    

静态内部类

静态内部类的写法是线程安全,也是延迟加载的。静态内部类和非静态内部类一样,都是在被调用时才会被加载。下面的代码验证了这一点。

package singleton;

class StaticInnerSingleton 
    private static class Holder 
        private static final StaticInnerSingleton instance = new StaticInnerSingleton();
    

    private StaticInnerSingleton() 
        System.out.println("正在实例化,对象地址:" + hashCode());
    

    static StaticInnerSingleton getInstance() 
        System.out.println("即将实例化");
        return Holder.instance;
    


public class StaticInnerClassSingleton 
    public static void main(String[] args) 
        int ans = 0;
        for (int i = 0; i < 10; ++i) ans += i * i;
        System.out.println(ans + "实例化前");
        StaticInnerSingleton u1 = StaticInnerSingleton.getInstance();
        System.out.println("实例化完成");
        StaticInnerSingleton u2 = StaticInnerSingleton.getInstance();
        System.out.println(u1.hashCode() + " , " + u2.hashCode());
    

懒汉式

这种写法需要多做分析。网上最常看到的标准做法是double check+synchronized+volatile。

只有外层check行吗?

不行。线程1释放锁后,线程2拿到锁,把instance覆盖掉了。这肯定很荒谬啊!

只有内层check行吗?

可以,但这样的效果相当于synchronized修饰方法,每个线程都要进来阻塞一遍,效率低。

如何解决线程安全问题?
  1. instance要加volatile,防止指令重排序(先把指针指向对应堆空间,再初始化对象),导致另一个线程拿到空对象。值得注意的是,比较旧版的JDK,volatile不能满足“有序性”,于是下面的代码在旧版的JDK不是线程安全的。
  2. 需要一个对象锁来防止被多个线程实例化。
为什么需要volatile?

对象创建过程:1.分配对象内存空间。 2.初始化对象。 3.设置对象指向内存空间。

2和3是没有依赖关系的,所以JVM指令重排序可能会把指令排成132。如果线程1完成了13,线程2在外层check(没有进入阻塞等锁的阶段)时就判定instance != null,于是拿到了没有初始化的对象。

值得注意的是,如果直接锁静态方法,那么即使可能有指令重排,也不需要volatile,因为其他线程进不去外层check。但这样效率就低了。

如何处理反射攻击?

参考链接说“在构造函数加一个判定instance是否null”的判定即可,其实不完全对。如果第一次生成是用的静态方法,那么没问题;如果所有实例都用反射的方式生成,那么以上简单的做法就没法防范。

我自己乱搞了一个4次check的做法,是为了解决反射攻击的问题,运行看上去没啥问题,要是这种乱搞做法有问题可以评论区告诉我QAQ。

package singleton;

import java.lang.reflect.Constructor;

public class LazySingleton 
    private static volatile LazySingleton instance = null;
    private String name;

    private LazySingleton(String name) 
        if (instance != null) throw new IllegalStateException("禁止多次初始化!");
        synchronized (LazySingleton.class) 
            if (instance != null) throw new IllegalStateException("禁止多次初始化!");
            System.out.println("正在实例化,对象地址:" + hashCode());
            this.name = name;
            instance = this;
        
    

    // 1-instance要加volatile,防止指令重排序(先把指针指向对应堆空间,再初始化对象),
    // 导致另一个线程拿到空对象。
    // 2-需要一个对象锁来防止被多个线程实例化。
    public static LazySingleton getInstance(String name) 
        if (instance != null) return instance;
        synchronized (LazySingleton.class) 
            if (instance != null) return instance;
            instance = new LazySingleton(name);
        
        return instance;
    

    void setName(String name) 
        this.name = name;
    

    String getName() 
        return name;
    

    public static void main(String[] args) throws Exception 
        Thread t1 = new Thread(() -> 
            LazySingleton user = LazySingleton.getInstance("hans");
            System.out.println(user.getName() + user.hashCode());
        ), t2 = new Thread(() -> 
            LazySingleton user = LazySingleton.getInstance("xyz");
            System.out.println(user.getName() + user.hashCode());
        );
        t1.start();
        t2.start();
//        Class<LazySingleton> clazz = (Class<LazySingleton>) Class.forName("singleton.LazySingleton");
//        Constructor<LazySingleton> cons = clazz.getDeclaredConstructor(String.class);
//        LazySingleton u1 = cons.newInstance("hans"), u2 = cons.newInstance("hans");
//        System.out.println("hashCode:u1 = " + u1.hashCode() + ", u2 = " + u2.hashCode());
    

饿汉式

这种写法很简单,我就直接拿别人的例子了。

因为对象创建时机足够早,所以自然不存在线程安全问题。

缺点:即使单例没有被使用,对象也会被创建,占用资源。

package com.junmoyu.singleton;

/**
 * 饿汉式单例模式 - 线程安全
 * 该类在程序加载时就已经初始化完成了
 */
public class EagerlySingleton 
    /**
     * 初始化静态实例
     */
    private static EagerlySingleton INSTANCE = new EagerlySingleton();

    /**
     * 私有构造函数,保证无法从外部进行实例化
     */
    private EagerlySingleton() 
        System.out.println(getClass().getCanonicalName() + " 被实例化,hashCode:" + hashCode());
    

    /**
     * 可被用户调用以获取类的实例
     */
    public static EagerlySingleton getInstance() 
        return INSTANCE;
    

    public static void main(String[] args) throws Exception 
        // 延迟加载测试
        System.out.println("测试代码启动");
        Thread.sleep(1000);

        // 多线程测试
        for (int i = 0; i < 5; i++) 
            new Thread(() -> System.out.println("多线程测试:hashCode:" + "@" + EagerlySingleton.getInstance().hashCode())).start();
        
    

参考

https://juejin.cn/post/6958740833482997790

静态内部类何时初始化:https://www.cnblogs.com/maohuidong/p/7843807.html

以上是关于Java设计模式创建型模式:单例模式的主要内容,如果未能解决你的问题,请参考以下文章

Java设计模式——单例模式(创建型模式)

单例模式---创建型模式

Java设计模式创建型模式:单例模式

Java设计模式——创建型模式之单例模式

Java设计模式之创建型:单例模式

java 单例模式这个要怎么理解?