写点什么

SingletonPattern- 单例模式

作者:梁歪歪 ♚
  • 2022 年 5 月 28 日
  • 本文字数:3595 字

    阅读完需:约 12 分钟

SingletonPattern-单例模式

单例模式,顾名思义,就是被单例的对象只能由一个实例存在。单例模式的实现方式是,一个类能返回一个对象的一个引用(永远是同一个对象)和一个获得该唯一实例的方法(必须是静态方法)。通过单例模式,我们可以保证系统中只有一个实例,从而在某些特定的场合下达到节约或者控制系统资源的目的。


这个看起来是最为简单的设计模式,其实它有许多坑~~~

单例模式

1、饿汉模式

最常见、最简单的单例模式写法之一。顾名思义,“饿汉模式” 就是很 “饥渴”,所以一上来就需要给它新建一个实例。但这种方法有一个明显的缺点,那就是不管有没有调用过获得实例的方法(本例中为 getInstance() ),每次都会新建一个实例。


package cn.liangyy;
/** * 饿汉式 * 类加载到内存后,就实例一个单例,JVM保证线程安全 * 优点:简单实用,推荐使用 * 缺点:不管用到与否,类装载时就完成实例化 */public class Mgr01 { //一开始就新建一个实例 private static final Mgr01 INSTSNCE = new Mgr01();
//默认构造方法 private Mgr01() {};
//获得实例的方法 public static Mgr01 getInstance(){ return INSTSNCE; }
//模拟其他业务方法 public void m() { System.out.println("m"); }
//具体用来测试 public static void main(String[] args) { Mgr01 m1 = Mgr01.getInstance(); Mgr01 m2 = Mgr01.getInstance(); System.out.println(m1 == m2); System.out.println(m1); System.out.println(m2); for (int i = 0; i < 100; i++) { System.out.println(Mgr01.getInstance().hashCode()); } }}
复制代码

2、懒汉模式

最常见、最简单的单例模式写法之二,跟 “饿汉模式” 是 “好基友”。再次顾名思义,“懒汉模式” 就是它很懒,一开始不新建实例,只有当它需要使用的时候,会先判断实例是否为空,如果为空才会新建一个实例来使用。


package cn.liangyy;
/** * lazy loading * 懒汉式 * 虽然达到了按需初始化的目的,但却带来了线程不安全的问题 */public class Mgr02 { private static Mgr02 INSRANCE;
private Mgr02(){}
public static Mgr02 getInstance(){ if (INSRANCE == null){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSRANCE = new Mgr02(); } return INSRANCE; }
public void m(){ System.out.println("m"); }

public static void main(String args[]){ //多线程测试 for (int i = 0;i < 100;i++){ new Thread(() -> { System.out.println(Mgr02.getInstance()); }).start(); } }}
复制代码


现在,问题来了,当我们多线程访问的时候,我们获得的实例对象还是同一个对象吗?答案很肯定不是的,我们一起来分析一下。


执行过程:


多线程的时候,执行到 if 判断,第一个线程 A 在判断中,另一个线程 B 也进来了,B 比 A 要快,它也进行 if 判断,因为还没有 new Mgr02,所以 INSTANCE = null,这时它继续往下执行,执行到 INSRANCE = new Mgr02();它就 new 了一个 Mgr02 对象,这时线程 A 执行完了,它也 new 了一个 Mgr02,这时就有两个 INSTANCE 对象,所以带来了更大的问题。这样我们用多线程测试并打印对象的 HashCode,便会发现结果中的对象并非一个,而是多个。


3、 线程安全的懒汉模式

这种方式用来解决懒汉模式的线程安全的


package cn.liangyy;
/** * 加锁解决Mgr02中出现的线程不安全问题 */public class Mgr03 { private static Mgr03 INSTANCE;
private Mgr03(){}
public static synchronized Mgr03 getInstance(){ if (INSTANCE == null){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr03(); } return INSTANCE; }
public void m(){ System.out.println("m"); }
public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Mgr03.getInstance().hashCode()); }).start(); } }}
复制代码

4、 双重检验锁(double check)

线程安全的懒汉模式解决了多线程的问题,看起来完美了。但是它的效率却下降了,每次调用获得实例的方法getInstance()时都要进行同步,但是多数情况下并不需要同步操作(例如我的 INSTANCE 实例并不为空可以直接使用的时候,就不需要给getInstance()加同步方法,直接返回 INSTANCE 实例就可以了)。所以只需要在第一次新建实例对象的时候,使用同步方法。于是在前面的基础上,又有了“双重检验锁”的方法。


package cn.liangyy;
/** * 懒汉式 * 使用双重检查保证线程安全,但也带来了效率的下降 * 这是目前好的解决方法之一 */public class Mgr06 { /** * volatile:保证线程可见性,不具有原子性 * 当将volatile加到变量上,该变量的初始化过程将不被指令重排序 */ private static volatile Mgr06 INSTANCE;
private Mgr06(){}
public static Mgr06 getInstance(){ if (INSTANCE == null){ //双重检查 synchronized (Mgr06.class){ if (INSTANCE == null){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr06(); } } } return INSTANCE; }
public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Mgr06.getInstance().hashCode()); }).start(); } }}
复制代码


  • 这里需要注意的是,我们需要给实例加一个volatile关键字,它的作用是防止编译器在自行优化代码的时候将指令重排序。如果一旦重排序,实例半初始化状态时的值就会提前初始化给实例。

5、 静态内部类

上面的方法,修修补补,实在是太复杂了,还涉及到 JVM 的一些知识,而且volatile关键字对 JDK 的版本也有要求。所以我们得换一种方法,即“静态内部类”。这种方式利用 JVM 自身的机制来保证线程安全,因为 Mgr07Holder 是私有的,除了getInstance()之外没有其他的方式可以访问实例对象了,而且只有在调用getInstance()时才会去真正的创建实例对象。(这里有点类似“懒汉模式”)


package cn.liangyy;
/** * 静态内部类方式(JVM帮我们保证线程安全) * JVM保证单例 * 加载外部类时不会加载内部类,这样可以实现懒加载 */public class Mgr07 { private Mgr07(){}
private static class Mgr07Holder{ private final static Mgr07 INSTANCE = new Mgr07(); }
public static Mgr07 getInstance(){ return Mgr07Holder.INSTANCE; }
public void m(){ System.out.println("m"); }
public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Mgr07.getInstance().hashCode()); }).start(); } }}
复制代码

6、 枚举单例

当然,除了“静态内部类”的方式,还有简单的不能再简单的“枚举单例”。我们可以通过Mgr08.INSTANCE来访问实例对象,这比getInstance()要简单的多,而且创建枚举默认就是线程安全的,还可以防止反序列化带来的问题。这种方式虽然不常用,但是最为推荐。悄悄告诉你们这么优(niu)雅(bi)的方法,来自于新版《Effective Java》这本书。


package cn.liangyy;
/** * 枚举单例(完美的单例模式) * 不用担心线程安全的问题 * 不仅可以解决线程同步,还可以防止反序列化 */public enum Mgr08 { //唯一的取值,下面的调用只能调用这一个,没有争议 INSTANCE;
//剩下的都是业务方法 public void m(){ System.out.println("m"); }
public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Mgr08.INSTANCE.hashCode()); }).start(); } }}
复制代码


发布于: 刚刚阅读数: 5
用户头像

梁歪歪 ♚

关注

还未添加个人签名 2021.07.22 加入

还未添加个人简介

评论

发布
暂无评论
SingletonPattern-单例模式_设计模式_梁歪歪 ♚_InfoQ写作社区