设计模式(一)单例模式Singleton,并没有你想的那么简单

前言

设计模式是开发中的一柄利剑,有助于提高效率,减少冗余代码,使代码更适应将来业务的变化等.
但在实际项目中,我们要尽量做到"心中有剑,手中无剑",不要被这些条条框框所限制,使用设计模式的目的是为了帮助我们开发,不要让它成为我们的限制/禁锢.

单例模式

下面说的"完美"不一定适用于所有场景,
其他不那么"完美"的方式也未必就一点都不能用,还是要具体问题具体分析.

"完美"的两种方式(都算是懒汉)

懒汉:这个类很懒,只有被用到时才去加载资源,否则不加载资源
饿汉:这个类很饿,程序运行-类被加载时,就加载资源.

使用枚举enum

这是effectice JAVA中提出的,可以说是单例模式最完美的一种实现了,不仅可以从正常使用上保证单例,还能防止反序列化,因为枚举类没有构造方法.
但是我从个人使用习惯上,感觉用枚举类实现单例,怪怪的,不习惯.
我还是喜欢饿汉模式,或者使用内部类方式实现的懒汉.

public enum SingletonTest001 {

    INSTANCE("大黄");
    private String name;

    SingletonTest001(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void say() {
        System.out.println("hello~");
    }

    public static void main(String[] args) {
        // 通过输出hashcode,可以判断其是否是多线程安全的单例
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonTest001.INSTANCE.hashCode())).start();
        }
    }
}

使用内部类

这个也是很完美的一种方式,有JVM来帮我保证单例.
众所周知,当一个类被加载的时候,它的内部类是不会被加载的;只有当第一次使用它的内部类时,才会加载内部类.

public class SingletonTest002 {

    private String name = "大黄";

    // 私有化构造方法
    private SingletonTest002() {

    }

	// 内部类,作为一个持有SingletonTest002的容器
    private static class Holder {
        private static final SingletonTest002 INSTANCE = new SingletonTest002();
    }

    public static SingletonTest002 getInstance() {
        return Holder.INSTANCE;
    }

    public String getName() {
        return name;
    }

    public void say() {
        System.out.println("hello~");
    }

    public static void main(String[] args) {
        // 通过输出hashcode,可以判断其是否是多线程安全的单例
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonTest002.getInstance().hashCode()))
                    .start();
        }
    }
}

其他实现方式

饿汉方式

饿汉很简单,直接,干脆,在一些我们知道它一定会被用到的类,我觉得直接用饿汉方式更好.
JVM只会加载一次类,在类加载的时候初始化静态变量.

public class SingletonTest003 {

    public static final SingletonTest003 INSTANCE = new SingletonTest003();

    private String name = "大黄";

    // 私有化构造方法
    private SingletonTest003() {

    }

    public String getName() {
        return name;
    }

    public void say() {
        System.out.println("hello~");
    }

    public static void main(String[] args) {
        // 通过输出hashcode,可以判断其是否是多线程安全的单例
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonTest003.INSTANCE.hashCode()))
                    .start();
        }
    }
}

一个懒汉的错误示范(无锁,线程不安全)

上面那个懒汉方式,有人觉得不优雅,有些类根本用不到还加载资源,浪费内存,然后就想了个办法,getInstance时先判断有没有初始化,没有初始化过就去初始化,然后返回实例.

下面这样写在单线程环境下没问题,如果是多线程并发访问的话,就可能不会是真的单例了;
加入线程t1和t2同时调用getInstance()方法,他俩都判断INSTANCE == null为true,就各自都加载资源去了,造成资源浪费或者严重的不易察觉的错误.

public class SingletonTest004 {

    private static SingletonTest004 INSTANCE;

    public static SingletonTest004 getInstance() {
        if (INSTANCE == null) {
            // 模拟加载资源的过程,初始化比较慢
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new SingletonTest004();
        }
        return INSTANCE;
    }

    private String name = "大黄";

    // 私有化构造方法
    private SingletonTest004() {

    }

    public String getName() {
        return name;
    }

    public void say() {
        System.out.println("hello~");
    }

    public static void main(String[] args) {
        // 通过输出hashcode,可以判断其是否是多线程安全的单例
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonTest004.getInstance().hashCode()))
                    .start();
        }
    }
}

方法上加锁的懒汉

上面那个线程不安全的问题,有人很快想到答案了,加锁就是了嘛!
但是加锁是需要额外消耗资源的,这里如果多线程下getInstance调用比较频繁,还是很消耗资源的.

public class SingletonTest004 {

    private static SingletonTest004 INSTANCE;

    public static synchronized SingletonTest004 getInstance() {
        if (INSTANCE == null) {
            // 模拟加载资源的过程,初始化比较慢
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new SingletonTest004();
        }
        return INSTANCE;
    }

    private String name = "大黄";

    // 私有化构造方法
    private SingletonTest004() {

    }

    public String getName() {
        return name;
    }

    public void say() {
        System.out.println("hello~");
    }

    public static void main(String[] args) {
        // 通过输出hashcode,可以判断其是否是多线程安全的单例
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonTest004.getInstance().hashCode()))
                    .start();
        }
    }
}

一个懒汉的错误示范(锁代码块,单次判断,线程不安全)

既然锁加在方法上,会消耗很多资源,那就想着:先判断一次,只有加载资源时候加锁,以后判断就不加锁了.

下面这个例子有很大问题,假设还未实例化,很多个线程同时访问getInstance,大家都判断INSTANCE == null为true了,然后排着队一个个的去加载资源,实例化,没有解决问题.

public class SingletonTest006 {

    private static SingletonTest006 INSTANCE;

    public static SingletonTest006 getInstance() {
        if (INSTANCE == null) {
            // 模拟加载资源的过程,初始化比较慢
            synchronized (SingletonTest006.class) {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new SingletonTest006();
            }
        }
        return INSTANCE;
    }

    private String name = "大黄";

    // 私有化构造方法
    private SingletonTest006() {

    }

    public String getName() {
        return name;
    }

    public void say() {
        System.out.println("hello~");
    }

    public static void main(String[] args) {
        // 通过输出hashcode,可以判断其是否是多线程安全的单例
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonTest006.getInstance().hashCode()))
                    .start();
        }
    }
}

双重检验锁Double Check Lock

这里要注意,一定要加volatile关键字,防止指令重排序.
这个算是没啥问题的懒汉了,但是感觉略复杂,不那么优雅…

public class SingletonTest007 {

    private static volatile SingletonTest007 INSTANCE;

    public static SingletonTest007 getInstance() {
        if (INSTANCE == null) {
            // 模拟加载资源的过程,初始化比较慢
            synchronized (SingletonTest007.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new SingletonTest007();
                }
            }
        }
        return INSTANCE;
    }

    private String name = "大黄";

    // 私有化构造方法
    private SingletonTest007() {

    }

    public String getName() {
        return name;
    }

    public void say() {
        System.out.println("hello~");
    }

    public static void main(String[] args) {
        // 通过输出hashcode,可以判断其是否是多线程安全的单例
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(SingletonTest007.getInstance().hashCode()))
                    .start();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值