> 文档中心 > 【设计模式】周末在家肝了一天,终于写完这篇单例模式,看完还不会你打我~

【设计模式】周末在家肝了一天,终于写完这篇单例模式,看完还不会你打我~

本文目录

    • 饿汉式
    • 懒汉式
    • 双重检查锁单例
    • 静态内部类单例
    • 彩蛋:ThreadLocal单例模式
  • 大功告成 ~
        • 喜欢就一键三连 b( ̄▽ ̄)d

饿汉式

单例模式的通用写法,一般均指饿汉式。该写法在类加载的时候会立即初始化,并且创建单例对象。之所以说它线程安全,是因为在线程还未run起来之前就实例化了,不存在访问安全问题。

//饿汉式静态代码块单例模式public class HungryStaticSingleton {    private static final HungryStaticSingleton instance = new HungryStaticSingleton();    private HungryStaticSingleton(){}    public static HungryStaticSingleton getInstance(){ return  instance;    }}

饿汉式静态代码块写法:

//饿汉式静态代码块单例模式public class HungryStaticSingleton {    private static final HungryStaticSingleton instance; static { instance = new HungryStaticSingleton();    }    private HungryStaticSingleton(){}    public static HungryStaticSingleton getInstance(){ return  instance;    }}

这种写法使用了静态代码块机制。饿汉式适用于单例对象比较少的情况,可以保证绝对的线程安全,执行效率比较高。

其缺点也很明显,所有对象类在加载时候就会实例化。如果系统中有大批量的单例对象存在,而且单例对象的数量不确定的情况下,当系统初始化时会有大量的内存浪费。无论对象会不会使用到,都占用了一定的内存空间,造成系统资源的浪费。

懒汉式

懒汉式是为了解决饿汉式会带来的内存浪费问题。这种写法在对象被使用时才会被初始化。

//懒汉式单例模式在外部需要使用的时候才进行实例化public class LazySimpleSingletion {    //静态块,公共内存区域    private static LazySimpleSingletion instance;    private LazySimpleSingletion() {    }    public static LazySimpleSingletion getInstance() { // 判断instance是否为空 if (instance == null) {     instance = new LazySimpleSingletion(); } return instance;    }}

这种写法在多线程环境中会产生线程安全的问题吗?


可以看出,会存在一定的概率出现两种不同的结果,有可能两个线程获取到的对象是一致的,也有可能是不一致的。

因此,上面的单例会存在线程安全问题。

假设两个线程在同一时间同时进入getInstance() 方法,那么就会同时满足 if (null == instance) 条件,创建两个对象。

如果两个线程都继续往下执行,有可能后执行的线程的结果会覆盖先执行的线程的结果。

在这里插入图片描述

解决以上问题,仅需在getInstance() 方法前加把同步锁 synchronize,使这个方法变为同步方法。

//懒汉式单例模式在外部需要使用的时候才进行实例化public class LazySimpleSingletion {    //静态块,公共内存区域    private static LazySimpleSingletion instance;    private LazySimpleSingletion() {    }    // 加上synchronize关键字,变为同步方法    public synchronized static LazySimpleSingletion getInstance() { // 判断instance是否为空 if (instance == null) {     instance = new LazySimpleSingletion(); } return instance;    }}

如果线程数量骤增,懒汉式是否还适用?为什么?

当线程数量在短时间内剧增,使用synchronize加锁会导致线程阻塞,使程序性能下降。

举例:

如图所示,餐厅有5个分餐口,但是入口只有1条通道,这样的话会造成大量的堵塞,降低了用户体验。

在这里插入图片描述

是否有比上面更好的方案呢?

双重检查锁单例

在这里插入图片描述

如上图所示,将人群分开排队,进入餐厅后仍保持分流,如此一来效率会提升许多。

LazySimpleSingletion 进行改造,得到 LazyDoubleCheckSingleton1 ,代码如下:

public class LazyDoubleCheckSingleton1 {    private volatile static LazyDoubleCheckSingleton1 instance;    private LazyDoubleCheckSingleton1() {    }    // 参照 LazySimpleSingletion    public static LazyDoubleCheckSingleton1 getInstance() { synchronized (LazyDoubleCheckSingleton1.class) {     if (instance == null) {  instance = new LazyDoubleCheckSingleton1();     } } return instance;    }}

那么这种写法和没有加锁的 LazySimpleSingletion 并无差异,因此将 if 判断向上提一级,得到LazyDoubleCheckSingleton2

public class LazyDoubleCheckSingleton2 {    private volatile static LazyDoubleCheckSingleton2 instance;    private LazyDoubleCheckSingleton2() {    }    // 参照 LazySimpleSingletion    public static LazyDoubleCheckSingleton2 getInstance() { if (instance == null) {     synchronized (LazyDoubleCheckSingleton2.class) {  instance = new LazyDoubleCheckSingleton2();     } } return instance;    }}

LazyDoubleCheckSingleton2 进行调试发现,仍存在线程不安全问题


造成这种情况的原因在于,如果两个线程在同一时间都满足 if(null == instance) 条件,那么两个线程还是会执行 synchronize 中的代码,继续优化:

public class LazyDoubleCheckSingleton3 {    private volatile static LazyDoubleCheckSingleton3 instance;    private LazyDoubleCheckSingleton3() {    }    // 参照 LazySimpleSingletion    public static LazyDoubleCheckSingleton3 getInstance() { if (null == instance) {     // 检查是否要阻塞     synchronized (LazyDoubleCheckSingleton3.class) {  // 检查是否要重新创建实例  if (null == instance) {      instance = new LazyDoubleCheckSingleton3();  }     } } return instance;    }}

调试如下:

在这里插入图片描述

虽然双重检查锁单例解决了线程的安全与性能问题。当使用到 synchronize 关键字时还需上锁,对程序的性能还是存在影响。

静态内部类单例

public class LazyStaticInnerClassSingleton1 {    //使用 LazyStaticInnerClassSingleton1 的时候,默认会先初始化内部类    //如果没使用,则内部类是不加载的    private LazyStaticInnerClassSingleton1() {    }    // static是为了使单例的空间共享,保证这个方法不会被重写、重载    private static LazyStaticInnerClassSingleton1 getInstance() { //在返回结果之前,一定会先加载内部类 return LazyHolder.INSTANCE;    }    // 利用Java内部类的语法特点,默认不加载内部类    private static class LazyHolder { private static final LazyStaticInnerClassSingleton1 INSTANCE = new LazyStaticInnerClassSingleton1();    }}

该方式即解决了饿汉式的内存资源浪费及 synchronize 加锁的性能问题,内部类一定是在方法调用之前初始化,避免了线程安全问题。

那么,是否可以使用反射来调用构造方法,再调用 getInstance() 方法实现上面的单例模式?

public static void main(String[] args) {    try { // 使用反射进行破坏 Class<?> clazz = LazyStaticInnerClassSingleton1.class; // 通过反射获取私有构造方法 Constructor<?> c = clazz.getDeclaredConstructor(null); Object o1 = c.newInstance(); Object o2 = c.newInstance(); System.out.println(o1 == o2);    } catch (Exception e) { e.printStackTrace();    }}

调用结果如下:

很显然,这种方式在内存中创建了两个不同的实例

public class LazyStaticInnerClassSingleton2 {    //使用LazyStaticInnerClassSingleton2的时候,默认会先初始化内部类    //如果没使用,则内部类是不加载的    private LazyStaticInnerClassSingleton2() { if (LazyHolder.INSTANCE != null) {     throw new RuntimeException("不允许创建多个实例"); }    }    // static是为了使单例的空间共享,保证这个方法不会被重写、重载    private static LazyStaticInnerClassSingleton2 getInstance() { //在返回结果之前,一定会先加载内部类 return LazyHolder.INSTANCE;    }    // 利用Java内部类的语法特点,默认不加载内部类    private static class LazyHolder { private static final LazyStaticInnerClassSingleton2 INSTANCE = new LazyStaticInnerClassSingleton2();    }}

彩蛋:ThreadLocal单例模式

public class ThreadLocalSingleton {    private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton =     ThreadLocal.withInitial(ThreadLocalSingleton::new);    // 等价于上面的写法    /*private static final ThreadLocal threadLocalSingleton =     new ThreadLocal() {  @Override  protected ThreadLocalSingleton initialValue() {      return new ThreadLocalSingleton();  }     };*/    private ThreadLocalSingleton() {    }    public static ThreadLocalSingleton getInstance() { return threadLocalSingleton.get();    }    public static void main(String[] args) { System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~");    }}

调用结果:

可以看到,无论在主线程中调用多少次,获取到的实例都是同一个,但在两个子线程中获取到了不同的实例。

其实,ThreadLocal是将所有的对象全部放在ThreadLocalMap中,为每一个线程提供一个对象。

大功告成 ~

喜欢就一键三连 b( ̄▽ ̄)d