Java 设计模式 — 单例模式(3)

发布于:2022-12-02 ⋅ 阅读:(290) ⋅ 点赞:(0)

目录

一、单例模式

二、测试代码

三、单例模式的实现方式

<1> 懒汉模式

1) 线程不安全

2) 线程安全

<2> 饿汉模式

<3> 双检锁/双重校验锁(DCL: double-checked locking)

<4> 登记式/静态内部类

<5> 枚举

总结


一、单例模式

单例模式通常会涉及到单一的一个类,这个类负责创建自己的对象,并同时确定这个类仅仅只有一个对象被创建。显然,单例模式也属于创建型模式,同样也提供了创建对象的最佳方式。单例模式是 Java 最简单的设计模式其中之一,这种模式下的类,会提供一种访问这个唯一对象的方式,可以直接访问,并不需要再次实例化这个类。

我们必须保证在使用单例模式时,一个类有且仅有一个实例,并提供一个全局访问点来访问这个类。

二、测试代码

代码架构图

SingletonObj类

public class SingletonObj {
    /**
     * SingletonObj类
     * 1. 在该类内部直接创建对象变量
     * 2. 将构造方法私有化(关键代码),防止外部再次实例化这个类实例化,导致这个类产生多个实例
     * 3. getSingletonObj() 方法提供了唯一的方式来获取 SingletonObj 类的唯一的对象
     */
    private static final SingletonObj SINGLETON_OBJ = new SingletonObj();

    private SingletonObj() {}

    public static SingletonObj getSingletonObj() {
        System.out.println("SingletonObj类中的SINGLETON_OBJ: " + SINGLETON_OBJ.hashCode());
        return SINGLETON_OBJ;
    }

    public void sayHello() {
        System.out.println("这里是SingletonObj的单例模式~");
    }
}

Test类

public class Test {
    public static void main(String[] args) {
        SingletonObj singletonObj = SingletonObj.getSingletonObj();
        System.out.println("Test类中的singletonObj: " + singletonObj.hashCode());
        singletonObj.sayHello();
    }
}

测试结果

SingletonObj类中的SINGLETON_OBJ: 1163157884
Test类中的singletonObj: 1163157884
这里是SingletonObj的单例模式~

从结果中,我们可以看到,单例模式下类的对象变量的 hasCode 的值是相同的,也就是说明,这两个对象变量指向的是同一个对象,也就是说只存在唯一一个对象。

我们了解完单例模式的的基本思想后,接下来,我们来看看单例模式有哪些实现方式?

三、单例模式的实现方式

<1> 懒汉模式

1) 线程不安全

① 是否实现 Lazy loading:是  ② 多线程是否安全:否

这种创建单例的方式不支持多线程,因为没有加锁 synchronized,所以严格来说不算是单例模式。

测试代码

SingletonObj类

public class SingletonObj {
    private static SingletonObj SINGLETON_OBJ;

    private SingletonObj(){}

    public static SingletonObj getSingletonObj(){
        if(SINGLETON_OBJ == null){
            SINGLETON_OBJ = new SingletonObj();
        }
        return SINGLETON_OBJ;
    }
}

Test类

public class Test {
    public static void main(String[] args) {
        Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

测试结果 

这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 1037172763
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 224049094

我们可以看到,在不同的线程下,SingletonObj 类的对象并不是同一个,在两个线程中分别创建了两个对象吗,所以这种单例模式的实现,在多线程的情况下是不安全的。

2) 线程安全

① 是否实现 Lazy loading:是  ② 多线程是否安全:是

这种方式必须加锁,才可以在多线程中使用,但是效率低下,因为加锁 synchronized 会影响效率,这种方式只有在调用时才会被实例化,避免了内存的浪费。

测试代码

SingletonObj类

public class SingletonObj {
    private static SingletonObj SINGLETON_OBJ;

    private SingletonObj() {}

    public static synchronized SingletonObj getSingletonObj() {
        if(SINGLETON_OBJ == null) {
            SINGLETON_OBJ = new SingletonObj();
        }
        return SINGLETON_OBJ;
    }
}

 Test类

public class Test {
    public static void main(String[] args) {
        Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

测试结果 

这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 1037172763
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 1037172763

我们可以看到,在进行加锁后,不同线程中的 SingletonObj 的对象是中唯一,并不会发生改变,所以这种方式在多线程下是安全的。

<2> 饿汉模式

① 是否实现 Lazy loading:否  ② 多线程是否安全:是

这种方式来实现单例比较常用,但是很容产生垃圾对象,基于 classloader 机制避免了多线程的同步问题,相对于懒汉模式的线程安全相比,没有了枷锁,所以在效率上会提高。这种创建方式,只要 SingletonObj 类被装载,那么就会被实例化,没有 Lazy loading,会浪费内存。

测试代码

SingletonObj类

public class SingletonObj {
    private static final SingletonObj SINGLETON_OBJ = new SingletonObj();

    private SingletonObj() {}

    public static SingletonObj getSingletonObj() {
        return SINGLETON_OBJ;
    }
}

Test类 

public class Test {
    public static void main(String[] args) {
        Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

测试结果 

这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 1037172763
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 1037172763

<3> 双检锁/双重校验锁(DCL: double-checked locking)

① 是否实现 Lazy loading:是  ② 多线程是否安全:是

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

测试代码

SingletonObj类

public class SingletonObj {
    private volatile static SingletonObj SINGLETON_OBJ;

    private SingletonObj() {}

    public static SingletonObj getSingletonObj() {
        if(SINGLETON_OBJ == null) {
            synchronized (SingletonObj.class) {
                if(SINGLETON_OBJ == null) {
                    SINGLETON_OBJ = new SingletonObj();
                }
            }
        }
        return SINGLETON_OBJ;
    }
}

 Test类 

public class Test {
    public static void main(String[] args) throws Exception {
        Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

测试结果 

这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 478974226
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 478974226

<4> 登记式/静态内部类

① 是否实现 Lazy loading:是  ② 多线程是否安全:是

这种方式可以达到双重校验锁一样的效果,但实现更加简单,对静态域使用的是延迟初始化,而对于双重检验锁来说,可以在实例域使用延迟初始化。

与饿汉式相同,这种方式同样利用了 classloader 机制来保证初始化实例只有一个线程,与饿汉式不同的是,饿汉式只要 SingletonObj 类被装载,那么就会被实例化,而这种方式 SingletonObj 类被装载后,不一定会被实例化。因为 InnerClass 类并没有被主动的调用,只有调用 getSingletonObj( ) 方法时,才会显式的装载 SingletonObj 类,从而实例化 SingletonObj 类。

测试代码

SingletonObj类

public class SingletonObj {
    private static class InnerClass {
        private static final SingletonObj SINGLETON_OBJ = new SingletonObj();
    }

    private SingletonObj() {}

    public static SingletonObj getSingletonObj() {
        return InnerClass.SINGLETON_OBJ;
    }
}

Test类 

public class Test {
    public static void main(String[] args) {
        Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

测试结果

这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 478974226
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 478974226

<5> 枚举

① 是否实现 Lazy loading:是  ② 多线程是否安全:是

当前利用枚举来实现单例模式还没有被广泛接纳使用,但是这是实现单例模式的最佳方法,它更简洁,自动支持序列化机制,绝对防止多次实例化。

测试代码

SingletonObj类

public enum SingletonObj {
    SINGLETON_OBJ;

    public void sayHello() {
        System.out.println("这里是枚举类类, SINGLETON_OBJ: " + SINGLETON_OBJ.hashCode());
    }
}

Test类

public class Test {
    public static void main(String[] args) {
        Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.SINGLETON_OBJ.hashCode());
        Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.SINGLETON_OBJ.hashCode());
        new Thread(r1).start();
        new Thread(r2).start();

        SingletonObj.SINGLETON_OBJ.sayHello();
    }
}

测试结果

这里是枚举类类, SINGLETON_OBJ: 478974226
这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 478974226
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 478974226

总结

1. 在使用单例模式时,声明的对象,构造函数都需要私有化,防止外界的访问。

2. 懒汉模式与饿汉模式的本质区别在于,懒汉模式在方法外只是声明对象,只有在调用方法时才会进行 new 对象,而饿汉模式则是直接声明创建对象。

3. 不建议使用懒汉模式,建议使用饿汉模式来实现单例模式。

4. 需要明确做到 Lazy loading时,可以使用登记式/静态内部类。

5. 如果涉及反序列化创建对象时,可以使用枚举。

6. 以上情况都不符合,可以考虑双重校验锁。

本文含有隐藏内容,请 开通VIP 后查看