一、创造型设计模式
1.1 单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式的类结构图
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
这种很常见的应用在了spring的applicationContext.xml,在Servlet中的ServletContext、ServletConfig、
1.1.1 饿汉式单例模式
Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线
程还没出现以前就是实例化了,不可能存在访问安全问题。
- 优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
- 缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。
package com.alibaba.design.singlepattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/28-15:51 * 先静态后动态 * 先属性后方法 * 先上后下 */ public class HungrySingleton { private static final HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton(){ } public static HungrySingleton getInstance(){ return hungrySingleton; } }
或者也可以用下面这样,在静态代码块中去实例化。
package com.alibaba.design.singlepattern; //饿汉式静态块单例 public class HungryStaticSingleton { private static final HungryStaticSingleton hungrySingleton; static { hungrySingleton = new HungryStaticSingleton(); } private HungryStaticSingleton(){ } public static HungryStaticSingleton getInstance(){ return hungrySingleton; } }
1.1.2 懒汉式单例模式
(类加载时不初始化)
(1)线程不安全的情况
创建一个最简单的懒汉式单例模式,在不加synchronized的前提下是容易出现线程不安全的状况
package com.alibaba.design.singlepattern.lazy; /** * Created by Tom. */ //懒汉式单例 //在外部需要使用的时候才进行实例化 public class LazySimpleSingleton { private LazySimpleSingleton(){} //静态块,公共内存区域 private static LazySimpleSingleton lazy = null; public /*synchronized*/ static LazySimpleSingleton getInstance(){ if(lazy == null){ lazy = new LazySimpleSingleton(); } return lazy; } }
创建线程池,开辟线程
package com.alibaba.design.singlepattern.test; import com.alibaba.design.singlepattern.lazy.LazySimpleSingleton; /** * @author zhouyanxiang * @create 2020-07-2020/7/28-16:23 */ public class ThreadSingletonTest implements Runnable{ @Override public void run() { LazySimpleSingleton singleton = LazySimpleSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + " : " + singleton); } }
测试懒汉式单例模式的线程
package com.alibaba.design.singlepattern.test; import com.alibaba.design.singlepattern.lazy.LazySimpleSingleton; /** * @author zhouyanxiang * @create 2020-07-2020/7/28-16:22 */ public class LazySimpleSingletonTest { public static void main(String[] args) { System.out.println("============="); Thread t1 = new Thread(new ThreadSingletonTest()); Thread t2 = new Thread(new ThreadSingletonTest()); Thread t3 = new Thread(new ThreadSingletonTest()); Thread t4 = new Thread(new ThreadSingletonTest()); Thread t5 = new Thread(new ThreadSingletonTest()); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); System.out.println("============="); } }
正常情况下应该是每次都会是相同的实例
不过如果运行多次,也有一定的概率出现不同的实例
如图所示,出现了多个不同的单例,由此可见在多线程的情况下是容易出现多个实例的,这样是不安全的。
(2)线程安全的情况
1. 实例化的方法上加synchronized同步锁
为了解决这个不安全的状况,可以在实例化方法那里加上synchronized关键字
package com.alibaba.design.singlepattern.lazy; /** * Created by Tom. */ //懒汉式单例 //在外部需要使用的时候才进行实例化 public class LazySimpleSingleton { private LazySimpleSingleton(){} //静态块,公共内存区域 private static LazySimpleSingleton lazy = null; public synchronized static LazySimpleSingleton getInstance(){ if(lazy == null){ lazy = new LazySimpleSingleton(); } return lazy; } }
然后我开辟了100个线程随机测试,测试多次每次得到的实例对象都是同一个,解决了线程不安全的问题
package com.alibaba.design.singlepattern.test; import com.alibaba.design.singlepattern.lazy.LazySimpleSingleton; /** * @author zhouyanxiang * @create 2020-07-2020/7/28-16:22 */ public class LazySimpleSingletonTest { public static void main(String[] args) { System.out.println("============="); // Thread t1 = new Thread(new ThreadSingletonTest()); // Thread t2 = new Thread(new ThreadSingletonTest()); // Thread t3 = new Thread(new ThreadSingletonTest()); // Thread t4 = new Thread(new ThreadSingletonTest()); // Thread t5 = new Thread(new ThreadSingletonTest()); // t1.start(); // t2.start(); // t3.start(); // t4.start(); // t5.start(); for (int i = 0; i < 100; i++) { Thread t = new Thread(new ThreadSingletonTest()); t.start(); } System.out.println("============="); } }
2. 双检锁/双重校验锁(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
在私有成员变量的时候加上volatile关键字,初始化赋值null,真正的静态实例化方法中来个双重判断
package com.alibaba.design.singlepattern.lazy; /** *@author zhouyanxiang * @create 2020-07-2020/7/28-18:22 */ public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazy = null; private LazyDoubleCheckSingleton(){} public static LazyDoubleCheckSingleton getInstance(){ if(lazy == null){ synchronized (LazyDoubleCheckSingleton.class){ if(lazy == null){ lazy = new LazyDoubleCheckSingleton(); //1.分配内存给这个对象 //2.初始化对象 //3.设置lazy指向刚分配的内存地址 //4.初次访问对象 } } } return lazy; } }
3. 登记式/静态内部类
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟 饿汉式单例模式方式不同的是:饿汉式单例模式方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比饿汉式单例模式 方式就显得很合理。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){ } public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
4. 效率很高且线程安全的实现方式
package com.alibaba.design.singlepattern.lazy; /** * @author zhouyanxiang * @create 2020-07-2020/7/28-17:51 */ //懒汉式单例 //这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题 //完美地屏蔽了这两个缺点 //史上最牛B的单例模式的实现方式 public class LazyInnerClassSingleton { //默认使用LazyInnerClassGeneral的时候,会先初始化内部类 //如果没使用的话,内部类是不加载的 private LazyInnerClassSingleton(){ if(LazyHolder.LAZY != null){ throw new RuntimeException("不允许创建多个实例"); } } //每一个关键字都不是多余的 //static 是为了使单例的空间共享 //保证这个方法不会被重写,重载 public static final LazyInnerClassSingleton getInstance(){ //在返回结果以前,一定会先加载内部类 return LazyHolder.LAZY; } //默认不加载 private static class LazyHolder{ private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }
1.1.3 枚举
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。不能通过 reflection attack 来调用私有构造方法。
package com.alibaba.design.singlepattern; /** * @author zhouyanxiang * @create 2020-07-2020/7/28-21:37 */ public enum EnumSingleton { Instance; public void doSomeThing(){ System.out.println("Something has been done"); } }
单例模式小结
单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。
单例模式看起来非常简单,实现起来其实也非常简单。
经验之谈:一般情况下,不建议使用线程不安全的和直接在实例化上加上synchronized关键字的懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 3 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用第 2 种双检锁方式。