单例模式(Singleton Pattern)-设计模式总结

tech2023-06-09  119

我们分别介绍四种写法,分别是饿汉式加载,还有懒汉式加载,还有静态内部类的写法,还有一种超级大神提供的写法——枚举类写法!!!

文章目录

单例模式一、简介二、`SHOw mE cODe !!`1. 饿汉式加载(☆☆☆☆☆推荐使用!☆☆☆☆☆)2. 懒汉式加载3. 静态内部类的写法4. 枚举类写法

单例模式

一、简介

单例模式,顾名思义,就是保证一个类只有一个实例,比如工厂类(Factory)、管理器类(manager),一般这样的类的实例我们只需要一个就够了,就不需要频繁的创建与销毁,所以我们要使用单例模式。

大体的实现思路也很简单,当这个类有实例的时候,就返回这个实例,没有这个实例就重新创建一个实例并且返回。

二、SHOw mE cODe !!

1. 饿汉式加载(☆☆☆☆☆推荐使用!☆☆☆☆☆)

代码:

创建一个静态的final实例将构造方法私有化,保证外部无法创建该类的实例在获得这个实例的时候就要调用该类的getInstance()方法,获取的就是我们上面创建的实例 /** * 饿汉式 * 类加载到内存后,就实例化一个单例,JVM保证线程安全 * 简单实用,推荐使用 * 唯一缺点:不管用到与否,类装载的时候就完成实例化 * * @author veeja */ public class Mgr01 { /** * 定义一个静态的实例 */ private static final Mgr01 INSTANCE = new Mgr01(); /** * 将构造方法私有化,这样以来就无法创建这个类的实例 */ private Mgr01() { } /** * 要想获得这个类的实例,就需要调用 getInstance方法, * 这样返回的就是我们创建好的静态的实例 INSTANCE, * 借此我们就实现了单例的设计模式。 */ public static Mgr01 getInstance() { return INSTANCE; } }

我们可以做一个小实验,来验证一下这个类是不是单例的:

// 创建两个对象 Mgr01 m1 = Mgr01.getInstance(); Mgr01 m2 = Mgr01.getInstance(); // 查看这两个对象 System.out.println("m1 = " + m1); System.out.println("m2 = " + m2); //查看这两个对象是否相等 System.out.print("m1 == m2 :"); System.out.println(m1 == m2);

输出结果:

m1 = com.veeja.singleton.Mgr01@1b6d3586 m2 = com.veeja.singleton.Mgr01@1b6d3586 m1 == m2 :true

但是这样也有一个缺点,就是无论我们使不使用这个类的实例,它都会被加载到内存中,所以我们称之为饿汉式。

2. 懒汉式加载

接下来我们介绍一种写法,称之为懒汉式加载,只有我们用到它的时候才会加载它,不用的时候就不会加载。

代码:

第一次调用的时候,INSTANCE为null,就new一个对象第二次调用的时候,INSTANCE不为NULL,直接返回之前创建的对象 package com.veeja.singleton; /** * Lazy loading * 懒汉式加载 * 这样就可以按需要初始化,但是也会引发多线程访问时的问题 * * @Author veeja * 2020/9/3 18:02 */ public class Mgr02 { /** * 创建一个实例,但是不初始化 */ public static Mgr02 INSTANCE; /** * 私有化构造方法,保证从外部无法创建实例 */ private Mgr02() { } /** * 要想获得这个类的实例,就要通过 getInstance() 这个方法 * * @return */ public static Mgr02 getInstance() { // 创建之前先判断一下,如果INSTANCE为null,就创建一个实例赋给INSTANCE if (INSTANCE == null) { INSTANCE = new Mgr02(); } // 返回这个INSTANCE return INSTANCE; } }

这样就解决了使用的时候在加载到内存中,但是也带来了多线程访问的问题,我们模拟一下多线程访问这个类并创建实例(为了效果更明显,我们在getInstance()的时候休眠1毫秒):

package com.veeja.singleton; /** * Lazy loading * 懒汉式加载 * 这样就可以按需要初始化,但是也带来了线程不安全的问题 * * @Author veeja * 2020/9/3 18:02 */ public class Mgr02 { /** * 创建一个实例,但是不初始化 */ public static Mgr02 INSTANCE; /** * 私有化构造方法,保证从外部无法创建实例 */ private Mgr02() { } /** * 要想获得这个类的实例,就要通过 getInstance() 这个方法 * * @return */ public static Mgr02 getInstance() { // 创建之前先判断一下,如果INSTANCE为null,就创建一个实例赋给INSTANCE if (INSTANCE == null) { try { // 休眠 1 毫秒 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr02(); } // 返回这个INSTANCE return INSTANCE; } /** * 程序入口 * * @param args */ public static void main(String[] args) { // 创建 100 个线程 for (int i = 0; i < 100; i++) { new Thread(() -> { // 输出每个线程下创建的INSTANCE的哈希值 System.out.println(Mgr02.getInstance().hashCode()); }).start(); } } }

我们看一下运行结果(部分):

259540504 178378090 153128127 1932546621 1426590918 834649078 930626000 544002391 118531371 733365800 72786614 ... ...

我们可以看到,我们创建的实例已经不是一个了。 虽然我们的懒汉式加载解决了一点点问题,可是它却带来了更多的问题! 怎么才能完善它呢?

我们可以通过加锁来解决!通过给方法加一个synchronized来解决:

public static synchronized Mgr02 getInstance() { ... }

我们再来测试一下:

1481742375 1481742375 1481742375 1481742375 1481742375 1481742375 1481742375 1481742375 1481742375 ... ...

我们可以看到,这样我们创建的实例已经是一个对象了。

但是加了synchronized也带来了另一个问题,就是效率的下降!

那怎样提高效率呢?我们可以减小同步代码块来提高效率: 比如:

public static Mgr02 getInstance() { // 创建之前先判断一下,如果INSTANCE为null,就创建一个实例赋给INSTANCE if (INSTANCE == null) { // 通过减小同步代码块的方式来提高效率,但是不可行 synchronized (Mgr02.class) { try { // 休眠 1 毫秒 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr02(); } } // 返回这个INSTANCE return INSTANCE; }

但是,上面的代码运行的时候,又不同步了。所以上面的代码是错误的!

因为又带来了多线程访问的问题! 我们为了解决这个问题,还要再做一次判断,也就是改为双重判断:

public static Mgr02 getInstance() { // 创建之前先判断一下,如果INSTANCE为null,就创建一个实例赋给INSTANCE if (INSTANCE == null) { // 通过减小同步代码块的方式来提高效率,但是不可行 synchronized (Mgr02.class) { // 双重判断!!!! if (INSTANCE == null) { try { // 休眠 1 毫秒 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr02(); } } } // 返回这个INSTANCE return INSTANCE; }

但是这里,还有一个指令重排的问题,我们为了解决这个问题,需要给INSTANCE加一个volatile关键字。

private static volatile Mgr02 INSTANCE;

3. 静态内部类的写法

这样既保证了懒加载,又保证了单例,而线程安全则由JVM来保证。这是完美的写法之一!!!

代码:

我们在类的内部创建一个静态内部类我们在静态内部类中定义一个Mgr03的实例 INSTANCEgetInstance()方法返回的就是我们在静态内部类中定义的实例只有在调用这个方法的时候,才会加载这个实例进入内存 package com.veeja.singleton; /** * 静态内部类方式 * JVM保证单例 * 加载外部类时不会加载内部类,这样可以实现懒加载 * <p> * 这样既保证了懒加载,又保证了单例,而线程安全则由JVM来保证 * 这是完美的写法之一!!! * * @Author veeja * 2020/9/4 17:04 */ public class Mgr03 { /** * 私有化构造方法,在外部无法实例化这个类 */ private Mgr03() { } /** * 使用静态内部类 * 在外部类Mgr03被加载的时候,内部类Mgr03Holder是不会被加载的 * 只有当我们调用 getInstance()方法的时候,这个静态内部类才会被加载 */ private static class Mgr03Holder { // 我们在静态内部类中定义一个Mgr03的实例 INSTANCE private final static Mgr03 INSTANCE = new Mgr03(); } /** * 只有在调用这个方法的时候,才会加载Mgr03Holder这个类 * * @return */ public static Mgr03 getInstance() { return Mgr03Holder.INSTANCE; } }

我们来多线程访问一下:

public static void main(String[] args) { // 创建 100 个线程 for (int i = 0; i < 100; i++) { new Thread(() -> { // 输出每个线程下创建的INSTANCE的哈希值 System.out.println(Mgr03.getInstance().hashCode()); }).start(); } }

输出结果:

153128127 153128127 153128127 153128127 153128127 153128127 153128127 153128127 153128127 153128127 ... ...

4. 枚举类写法

超级大神写法!!!

借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

因为枚举类没有构造方法,所以就算拿到class文件,也没法构造它的对象,它的反序列化返回的只是INSTANCE这样一个值。

代码很简单:

public enum Mgr04 { INSTANCE; }

我们来实验一下:

public static void main(String[] args) { // 创建 100 个线程 for (int i = 0; i < 100; i++) { new Thread(() -> { // 输出每个线程下创建的INSTANCE的哈希值 System.out.println(Mgr04.INSTANCE.hashCode()); }).start(); } }

我们可以看到,也是单例的。

259540504 259540504 259540504 259540504 259540504 259540504 259540504 259540504 259540504 259540504 259540504 259540504 259540504 259540504 259540504 ... ...

是不是过于简洁了?

用枚举类来实现单例,枚举类中只有一个取值就是INSTANCE,当我们想使用这个实例的时候,就直接调用这个类的INSTANCE就行了。是不是超级简洁。

最新回复(0)