vlambda博客
学习文章列表

【设计模式系列(二)】彻底搞懂单例模式

文章中涉及到的代码,可到这里来拿:https://gitee.com/daijiyong/DesignPattern


概念:单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点


关键点是:单例类中有一个静态的变量,私有化构造方法,提供唯一全局调用方法,并选择一个时机进行初始化


属于创建型模式


它主要是为了解决:一个全局使用的类频繁地创建与销毁所造成的资源开销


单例模式比较简单,最复杂的地方在于如何保证多线程、序列化等情况下


仍然保证单例实例的唯一性


使用场景:

  • 配置文件,如ServletContext、ServletConfig、ApplicationContext、数据库连接池

  • 要求生产唯一序列号

  • WEB 中的计数器,不用每次刷新都在数据库中同步一次,可以用单例先缓存起来


1. 饿汉式



public class HungrySingleton { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}



在类加载的时候实例就初始化了


所以基于 classloader 机制很好的避免了多线程情况下的同步问题


还有一种写法是这样的



public class HungrySingleton { private static HungrySingleton instance; static { instance = new HungrySingleton(); } private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}


类加载顺序:

先静态后动态、先上后下、先属性后方法


所以这种写法也能满足在类加载的时候就能初始化的要求


懒汉式的优点是:执行效率高,性能高(没有加锁)


缺点:不过不是明确需要初始化这个实例,存在内存浪费的情况


2. 懒汉式单例


为了解决饿汉式中存在的内存浪费的情况


我们可以采用懒汉式



public class LazySimpleSingleton { private static LazySimpleSingleton instance; private LazySimpleSingleton() { } public static LazySimpleSingleton getInstance() { if (instance == null) { instance = new LazySimpleSingleton(); } return instance; }}


懒汉式在类加载的时候不会初始化单例


只有被被外部调用的时候才会创建


下面我们测试一下多线程情况下,这种单例的实现方式能否安全


写一个实现了Runable接口的类


在run方法中调用单例模式的实例



/** * @author daijiyong */public class TestLazySingletonThread implements Runnable { public void run() { System.out.println(Thread.currentThread().getName() + LazySimpleSingleton.getInstance()); }}


编写主函数,写三个线程进行测试



/** * @author daijiyong */public class Test { public static void main(String[] args) { Thread thread1 = new Thread(new TestLazySingletonThread()); Thread thread2 = new Thread(new TestLazySingletonThread()); Thread thread3 = new Thread(new TestLazySingletonThread()); thread1.start(); thread2.start(); thread3.start(); }}




显而易见,多线程下并没有保证线程安全


创建了多个单例的对象


其中线程1用的是一个实例


线程0和线程2用的另外一个实例


为什么会出现这个情况呢?


首先我们得知道,Java中线程类的start()方法,并不是会立马执行当前线程


仅仅是告知cpu,你需要执行当前线程,具体什么时候执行,看心情


所以虽然我们是按照thread1、thread2、thread3的顺序执行的start()方法,但是执行顺序却并一定是这样的


而且他们三个的执行顺序可能在不同的时间点,也是不一样的,完全随缘


所以,即便打印出来的两个线程的实例是一样的,也不代表这个单例只被创建了一次


也有可能是创建了两个实例,但是在返回结果之前,第二个实例已经创建完了,将第一个实例覆盖了


怎么优化?第一个想到的应该就是加锁



public class LazySimpleSingleton { private static LazySimpleSingleton instance; private LazySimpleSingleton() { } public synchronized static LazySimpleSingleton getInstance() { if (instance == null) { instance = new LazySimpleSingleton(); } return instance; }}


这样随便执行,就不会出现线程不安全的问题了


【设计模式系列(二)】彻底搞懂单例模式


但是当我这打了一个断点,查看三个进程执行情况的时候发现了下面的问题


线程0是执行runing状态,但是其他两个线程是监听monitor状态


当执行的线程特别多的时候,会就导致有大量的线程处于等待监听状态


synchronized关键字,是解决了线程安全问题


但是导致在同一个时间只能有一个调用,性能会极速下降


为了解决这个,我们还有一个办法


双重检验方法



public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton instance; private LazyDoubleCheckSingleton() { } public static LazyDoubleCheckSingleton getInstance() { //是否要线程阻塞 if (instance == null) { synchronized (LazyDoubleCheckSingleton.class) { //是否要创建实例 if (instance == null) { instance = new LazyDoubleCheckSingleton(); } } } return instance; }}


第一个检验是为了判断,是否已经初始化了单例


如果已经创建了,则不进行线程阻塞,直接返回


第二个检验是为了判断,在没有初始化的情况下,需要初始化,此时是否因为线程阻塞的原因,已经初始化了


如果已经初始化,则直接返回,如果没有,则进行线程初始化


除了双重检验,还有一个更好的方法来实现懒汉模式


静态内部类


public class LazyStaticInnerClassSingleton { private LazyStaticInnerClassSingleton() { //防止通过Java反射机制创建实例 if (LazyHolder.INSTANCE != null) { throw new RuntimeException("不允许非法访问"); } } private static LazyStaticInnerClassSingleton getInstance() { return LazyHolder.INSTANCE; } private static class LazyHolder { private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton(); }}



这个看起来是饿汉式的写法


但是其实是懒汉式的


这个主要是利用了java类加载的时候,默认是不会加载内部类的机制


只有在调用使用内部类的时候才会对内部类实现初始化


 classloader 机制保证了初始化 instance 时只有一个线程


这样就很好的解决了线程不安全的问题


优雅、高级、装*


【设计模式系列(二)】彻底搞懂单例模式


3. 枚举式单例



public enum EnumSingleton { /** * 实例 */ INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; }}


这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法


它更简洁,利用枚举类自身特点


不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化


为什么呢?


我们可以看一下源码


【设计模式系列(二)】彻底搞懂单例模式



从Java的源码可以看到


Java在底层就禁止了通过反射的方法创建枚举对象


所以在原生层面就解决了反射机制破坏单例模式的问题


那枚举类是如何保证线程安全的呢?


这个问题问的好,我们在看看源码




由此可知,枚举类在加载的时候


会将每一个枚举类的实例元素放到一个Map当中


这个操作在程序启动、类加载的时候就完成了


之后每次取对象,都是从map中拿的


所以绝对线程安全


但是枚举方式跟饿汉方式一样


是存在内存浪费的情况的


为了解决这个问题,还有一种写法


4. 容器式单例



public class ContainerSingleton { private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>(16); private ContainerSingleton() { } public static Object getInstance(String className) { if (!ioc.containsKey(className)) { try { synchronized (ioc) { Object instance = Class.forName(className).newInstance(); ioc.put(className, instance); } } catch (Exception e) { e.printStackTrace(); } } return ioc.get(className); }}


将每一个实例都缓存到统一的容器当中,使用唯一标识获取实例



当容器中不存在时,则对容器加锁并创建,之后返回


如果已经存在,则直接返回


使用这种方法可以完美的解决多线程和反射带来的问题


5. ThreadLocal单例



public class ThreadLocalSingleton { private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() { @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; private ThreadLocalSingleton() { } public static ThreadLocalSingleton getInstance() { return threadLocalInstance.get(); }}


ThreadLocal单例能够保证在一个线程内部的全局唯一,天生线程安全


跨线程的时候,保证不是同一个单例实例


这种实现方式的应用场景非常清晰了


用在多线程中,保证在一个线程中的单例实现


6. 总结


单例模式


优点:

减少内存开销

避免对资源的多重占用

设置全局访问点,严格控制访问


缺点:

没有接口,扩展困难,如果要扩展,只能修改代码,违背了开闭原则


文/戴先生@2020年7月2日


---end---


更多精彩推荐