单例设计模式

单例设计模式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式的实现

单例设计模式分类两种:

​ 饿汉式:类加载就会导致该单实例对象被创建

​ 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

饿汉式——静态成员变量

public class HungrySingleton {
    /**
     * 私有化构造方法
     */
    private HungrySingleton () {

    }
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

说明:

​ 该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

饿汉式——静态代码块

public class HungrySingleton {
    /**
     * 私有化构造
     */
    private HungrySingleton () {

    }

    /**
     * 实例成员变量
     */
    private static HungrySingleton hungrySingleton;

    /**
     * 通过静态代码块进行赋值
     */
    static {
        hungrySingleton = new HungrySingleton();
    }

    /**
     * 对外暴露的方法
     * @return :HungrySingleton
     */
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

说明:

​ 该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。

饿汉式——枚举

public enum HungrySingleton {
    INSTANCE;
}

说明:

枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

懒汉式——线程不安全

public class LazySingleton {
    private LazySingleton() {

    }
    private static LazySingleton lazySingleton;

    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

说明:

很明显,这里有一个读并写的操作,在多线程环境下是不安全的。

懒汉式——线程安全

所以,我们对该方法加了一个同步锁。

public class LazySingleton {
    private LazySingleton() {

    }

    private static LazySingleton lazySingleton;

    public static synchronized LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

懒汉式——双重检查锁

但是,可以想一下,真正lazySingleton = null的这个判断语句,发现线程不安全的几率高吗?也就是说,实际的情况都是读多写少,我们需要对每次回去实例对象的时候都去竞争同步锁吗?

这里我们引出了一个双重检查锁的方案,去降低锁的粒度。只有当为null,有可能需要发生写操作的时候,才去获取同步锁,另外需要对该对象添加volatile的关键字,保证指令的有序性,避免空指针问题。双重检查锁空指针问题

public class LazySingleton {
    private LazySingleton() {

    }

    /**
     * 注意需要添加volatile的关键字,保证指令有序性,防止指令优化带来的空指针问题
     */
    private static volatile LazySingleton lazySingleton;

    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            synchronized (LazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
}

懒汉式——静态内部类

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

public class LazySingleton {
    private LazySingleton() {

    }

    /**
     * 静态内部类,只有在被访问时才会加载进内存
     */
    private static class SingletonHolder {
        private static final LazySingleton INSTANCE = new LazySingleton();
    }

    /**
     * 直接 通过静态调用获取
     * @return :LazySingleton
     */
    public static LazySingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

单例模式真的能够实现实例的唯一性吗?

序列化

答案是否定的,大多数人都知道,可以通过反射去破坏单例的唯一性,其实还有另外一种——序列化,也可以去破坏单例的唯一性,其实底层的原理还是通过反射去破坏。为什么序列化会破坏单例?

但注意!枚举实现方式的单例模式无法被破坏!因为他是Java底层实现的。

这里我们来尝试破坏一下单例,为了序列化,我们必须首先让类实现序列化接口。这里我们选择双重检查锁的实现来测试,同时验证枚举是否真的无法被破坏单例。

首先我们使用双重检查锁,并输出对象的哈希值。

public class Main {
    public static void main(String[] args) throws Exception {
        write();
        read();
        read();
    }

    public static void read() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\a.txt"));
        LazySingleton singleton = (LazySingleton) ois.readObject();
        System.out.println(singleton.hashCode());
        ois.close();
    }
    public static void write() throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\a.txt"));
        LazySingleton instance = LazySingleton.getInstance();
        //HungrySingleton instance = HungrySingleton.INSTANCE;
        System.out.println(instance.hashCode());
        oos.writeObject(instance);
        oos.close();
    }
}

发现最后的结果确实不一样。

image-20230624110739252

然后我们再尝试一下枚举的实现。

public class Main {
    public static void main(String[] args) throws Exception {
        write();
        read();
        read();
    }

    public static void read() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\a.txt"));
        HungrySingleton singleton = (HungrySingleton) ois.readObject();
        System.out.println(singleton.hashCode());
        ois.close();
    }
    public static void write() throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\a.txt"));
        HungrySingleton instance = HungrySingleton.INSTANCE;
        System.out.println(instance.hashCode());
        oos.writeObject(instance);
        oos.close();
    }
}

输出结果如下:

image-20230624110930740

反射

然后这里再介绍一下最基本的反射破坏单例唯一性。

public class Main {
    public static void main(String[] args) throws Exception {
        //1. 获取字节码对象
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        //2. 获取无参构造方法,因为是private修饰,所以要用Declared
        Constructor<HungrySingleton> cons = hungrySingletonClass.getDeclaredConstructor();
        //3. 取消访问权限限制
        cons.setAccessible(true);
        //4. 创建对象
        HungrySingleton ins = cons.newInstance();
        HungrySingleton ins1 = cons.newInstance();
        //5. 判断内存地址是否相等
        System.out.println(ins == ins1);
    }
}

image-20230624112257041

怎么解决?

那如果已经采用了会被破坏唯一性的单例模式实现方法了,那我要如何防止被破坏呢?对于序列化破坏的话,这里我们参考一下上述博客中的介绍的方法,重写一个readResolve的方法,避免让反序列化过程中,通过反射去获取实例对象,而是我们直接手动指定返回即可。

public class LazySingleton implements Serializable {
    private LazySingleton() {

    }

    /**
     * 注意需要添加volatile的关键字,保证指令有序性,防止指令优化带来的空指针问题
     */
    private static volatile LazySingleton lazySingleton;

    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            synchronized (LazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }

    /**
     * 直接指定序列化返回对象
     * @return :该单例对象
     */
    private Object readResolve() {
        return lazySingleton;
    }
}

然后我们再重新执行一开始的代码,观察一下输出结果,发现已经成功解决了单例唯一性被破坏的问题。

image-20230624111336530

而对于反射破坏单例的,其实没有很好的解决方案,只能通过直接代码异常的情况终止程序,避免该问题的发生。具体做法就是在私有构造器处,判断是否是第一次访问该构造器,如果不是,直接抛出异常,需要注意多线程环境下的安全问题。

public class HungrySingleton {
    /**
     * 私有化构造
     */
    private HungrySingleton () {
        synchronized (HungrySingleton.class) {
            if (hungrySingleton != null) {
                throw new RuntimeException("单例构造器禁止反射调用!");
            }
        }
    }

    /**
     * 实例成员变量
     */
    private static HungrySingleton hungrySingleton;

    /**
     * 通过静态代码块进行赋值
     */
    static {
        hungrySingleton = new HungrySingleton();
    }

    /**
     * 对外暴露的方法
     * @return :HungrySingleton
     */
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

image-20230624113439980

Java中单例模式的运用

Runtime类就是使用的饿汉式的单例设计模式。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    ...
}

使用Runtime类

public class RuntimeDemo {
    public static void main(String[] args) throws IOException {
        //获取Runtime类对象
        Runtime runtime = Runtime.getRuntime();

        //返回 Java 虚拟机中的内存总量。
        System.out.println(runtime.totalMemory());
        //返回 Java 虚拟机试图使用的最大内存量。
        System.out.println(runtime.maxMemory());

        //创建一个新的进程执行指定的字符串命令,返回进程对象
        Process process = runtime.exec("ipconfig");
        //获取命令执行后的结果,通过输入流获取
        InputStream inputStream = process.getInputStream();
        byte[] arr = new byte[1024 * 1024* 100];
        int b = inputStream.read(arr);
        System.out.println(new String(arr,0,b,"gbk"));
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇