【设计模式】周末在家肝了一天,终于写完这篇单例模式,看完还不会你打我~
本文目录
-
- 饿汉式
- 懒汉式
- 双重检查锁单例
- 静态内部类单例
- 彩蛋: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中,为每一个线程提供一个对象。