设计模式之---单例模式(以及DCL懒汉式并发问题解决)

最简单的设计模式之一

单例模式主要用途:保证一个类只能有一个实例,并且在全局提供一个访问点。

常用实现方式:懒汉式、饿汉式、静态内部类式、枚举式

为什么需要单例模式?

假如此时你想读取一个XML配置文件,且该配置会被多次引用,那在每次引用时,就需要实例化一个Java Bean,但实际上我们全程只需要实例化一次,多处调用即可,当配置文件越来越多时,实例的创建占用系统资源是不可忽视的。那么此时就需要用到单例模式。

单例模式常用实现方式

要想保证一个类只能一个实例,那就不能让外面的代码随便访问构造方法创建实例,把创建实例的权限回收,让类自身负责自己的实例创建工作,并向外部提供一个访问这个实例的接口,因此构造方法必须私有化。 


最初的最常见的两个实现方式:懒汉式和饿汉式

相同点

  • 构造方法私有
  • 都拥有提供外部获取实例的public方法getInstance()
  • 因为该类不能多次实例化,所以获取实例的方法必须为静态方法,静态方法中的类必须为静态类,因此存储创建的类实例也必须是静态

不同点: 

  • 一个实例初始化了,一个没初始化
  • 懒汉式存在线程安全问题
  • 存储创建类实例的变量uniqueInstance设置为static一个主动一个被动

懒汉式:见文知意,比较懒,一上来没有实例化对象,而是在需要用到的时候才去实例化,懒加载,时间换空间,是由于getInstance方法是static,内部变量必须是static,uniqueInstance才是static,是被动的。但由于在getInstance时,可能A、B两个线程几乎同时进入,在A实例化未完成的情况下,B判断实例仍然为null,因此继续实例化,线程安全问题违背了单例的初衷。此时的解决办法是加锁synchronized,但加上之后会使效率急剧降低,因此有另一个方式:双重检查加锁。

饿汉式:饿急眼了来了就招呼,上来就把对象给实例化了,以后不用再实例化了,getInstance方法也必须是static,但期内部变量uniqueInstance的static属性是主动的,static变量在类装载时就初始化,且仅初始化一次,多个实例都会共享同一片内存空间,所以在一开始new的时候,static变量的特性保证的单例。因此不存在线程安全问题空间换时间


懒汉式

以时间换空间,在需要用到实例时再实例化,如果一直用不到就永远不实例化,节约空间;但牺牲了需要判断是否实例化的时间。 

懒汉式本身存在线程安全问题,当两个线程同时判断uniqueInstance == null,则会同时进行实例化。可以通过synchronized方法的问题来解决,但这样太重了。

/**
 * 懒汉式
 */
public class Singleton1 {
    
    // 定义一个存储创建的类实例
    private static Singleton1 uniqueInstance = null;

    /**
     * 私有构造方法
     */
    private Singleton1() {

    }

    // 外部获取实例方法
    public static synchronized Singleton1 getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton1();
        }
        return uniqueInstance;
    }
}

 在内层加上synchronized同步代码块,但存在一个新问题,A线程拿到锁开始初始化,B线程已经判断了uniqueInstance == null,只需要等待A执行完,B继续去初始化,仍然线程不安全,因此在synchronized内部再加一层uniqueInstance == null的判断即可。

不在外层直接加synchronized的原因同不在方法上加一样,都太重了。每次进来不管三七二十一先竞争锁,没必要,有实例了直接返回就好了,要锁干嘛。

synchronized不能锁this,因为getInstance是静态方法,不能使用this。

但此时还有问题,最里面的new Singleton1()并非原子操作,其有三步:

  1. 分配内存空间
  2. 执行构造方法 初始化对象
  3. 把对象指向这个空间

正常执行顺序应该是1->2->3,但可能指令会被重排序为1->3->2;

synchronized只能保证有序性,但无法禁止指令重排,Java中只有volatile关键字可以禁止指令重排。

假设此时刚好A线程执行完3还没执行2,空间已经占了,但是还没初始化,当B线程进来判断17行:uniqueInstance == null,空间已经有了就不是null了,于是直接走到29行返回了对象,但此时对象还未初始化。因此 为了防止指令重排,必须给第4行uniqueInstance对象加上volatile关键字。

双重检测锁模式,也叫DCL懒汉式(DCL:Double Check Lock)

public class Singleton1 {

    // 定义一个存储创建的类实例
    private volatile static Singleton1 uniqueInstance = null;

    /**
     * 私有构造方法
     */
    private Singleton1() {

    }

    // 外部获取实例方法
    public static Singleton1 getInstance() {
        // 外层判断
        if (uniqueInstance == null) {
            synchronized (Singleton1.class) {
                // 内层判断
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton1();// 非原子性操作
                    /**
                     * 1、分配内存空间
                     * 2、执行构造方法 初始化对象
                     * 3、把对象指向这个空间
                     */
                }
            }
        }
        return uniqueInstance;
    }
}

饿汉式 

以空间换时间,类加载时就创建实例,如果一直不用就浪费空间;但需要使用时无需判断,已经存在,节省判断时间。 

/**
 * 饿汉式
 */
public class Singleton2 {

    // 定义一个存储创建的类实例,并创建实例
    private static Singleton2 uniqueInstance = new Singleton2();

    /**
     * 私有构造方法
     */
    private Singleton2() {

    }

    // 外部获取实例方法
    public static synchronized Singleton2 getInstance() {
        return uniqueInstance;
    }
}

静态内部类式

懒汉式的懒加载方式节约空间,但存在线程安全问题,即使存在解决办法也不是非常完美;

饿汉式虽然没有问题,但浪费空间;

静态内部类方式,既可以实现懒加载,又不浪费空间,代码如下:

/**
 * 静态内部类式
 */
public class Singleton4 {

    /**
     * 私有静态内部类
     */
    private static class SingletonHolder {

        // 定义一个存储创建的类实例,并创建实例
        private static Singleton4 uniqueInstance = new Singleton4();
    }

    /**
     * 私有构造方法
     */
    private Singleton4() {

    }

    // 外部获取实例方法
    public static Singleton4 getInstance() {
        return SingletonHolder.uniqueInstance;
    }
}

本例采用了一个私有静态内部类的方式,private static class;该方式的特点是:静态内部类相当于外部类的成员,其属于外部类本身,而不属于外部类的对象,因此外部类实例化时静态内部类仍不会被加载。只有在静态内部类第一次真正被使用时才会被加载。

因此,内部类SingletonHolder中定义静态类实例,但在SingletonHolder未被调用时不会调用实例化;而当调用到getInstance方法时,才调用到SingletonHolder,于是uniqueInstance开始初始化;即实现了线程安全,又实现了懒加载。

 枚举式

【此处非常奇怪,idea一直报错,最后重启了idea就好了。。。】

/**
 * 枚举
 */
public enum Singleton3 {

    UNIQUE_INSTANCE;
}

现在普遍来说,枚举是实现单例的最好方式,其由JVM从根本上保证了单例。前两种若使用反射或者序列化方式,仍可生成多个实例化对象。

例如:反射通过动态获取其类,然后调用newInstance方法实例化对象;序列化也同样,在其过程中会自动调用反射的newInstance方法实例化对象。若对枚举类使用反射并创建构造方法,jdk会抛出“Cannot reflectively create enum objects”的异常信息,无需我们做额外的特殊处理。


下面开始测试:

/**
 * 测试类
 */
public class Test {
    public static void main(String[] args) {

        // 懒汉式
        Singleton1 singleton1A = Singleton1.getInstance();
        Singleton1 singleton1B = Singleton1.getInstance();
        System.out.println("======= 懒汉式 =======");
        System.out.println("singleton1A:"+singleton1A.hashCode());
        System.out.println("singleton1B:"+singleton1B.hashCode());

        // 饿汉式
        Singleton2 singleton2A = Singleton2.getInstance();
        Singleton2 singleton2B = Singleton2.getInstance();
        System.out.println("======= 饿汉式 =======");
        System.out.println("singleton2A:"+singleton2A.hashCode());
        System.out.println("singleton2B:"+singleton2B.hashCode());

        // 枚举式
        Singleton3 singleton3A = Singleton3.UNIQUE_INSTANCE;
        Singleton3 singleton3B = Singleton3.UNIQUE_INSTANCE;
        System.out.println("======= 枚举式 =======");
        System.out.println("singleton3A:"+singleton3A.hashCode());
        System.out.println("singleton3B:"+singleton3B.hashCode());

        // 静态内部类式
        Singleton4 singleton4A = Singleton4.getInstance();
        Singleton4 singleton4B = Singleton4.getInstance();
        System.out.println("======= 静态内部类式 =======");
        System.out.println("singleton4A:"+singleton4A.hashCode());
        System.out.println("singleton4B:"+singleton4B.hashCode());
    }
}

输出结果:

可以看到每种方式不同的实例间使用相同的hashCode 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值