茴字有四种写法,单例模式有很多种写法。
目录
(一)单例模式-静态-饿汉式
(二)单例模式-静态代码块-饿汉式
(三)单例模式-线程不安全的懒汉式
(四)单例模式-线程安全的懒汉式(不推荐)
(五)单例模式-同步代码块懒汉式(不推荐)
(六)单例模式-双重检查的懒汉式(推荐)
(七)单例模式-静态内部类(推荐)
单例模式的应用场景
那么什么是单例模式呢?
也就是某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。比如Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象。SessionFactory并不是轻量级的,一般情况下,一个项目只需要一个SessionFactory就够了,这就是会使用到单例模式。
思路:
私有化构造器(外部将不能通过new的方式去创建)本类内部创建静态的对象实例(在类加载时对象实例已创建)提供得到实例的静态方法这种方式的单例模式,写法简单,类在加载时就完成了实例化,避免了线程的问题。但缺点也很明显,实例在类记载时已创建,没有实现懒加载的效果,如果实例不用便一直存在,造成内存浪费。
/** * 饿汉式(静态变量) * * @author Claw * @date 2020/7/4 12:17. */ public class Singleton1 { /** * 1.私有化构造器,私有化的目的是不让外部通过new的方式创建对象实例 */ private Singleton1() { } /** * 2.本类内部创建对象实例,在类加载时,对象实例已创建 */ private static final Singleton1 instance = new Singleton1(); /** * 3.提供一个公有的静态方法,返回实例对象 * @return */ public static Singleton1 getInstance() { return instance; } public static void main(String[] args) { Singleton1 instance = Singleton1.getInstance(); Singleton1 instance2 = Singleton1.getInstance(); // 打印出instance和instance2 结果为true System.out.println(instance == instance2); } }这个方式和单例模式-静态-饿汉式是类似的,创建了一个类加载时就创建的单例对象,线程安全,未实现懒加载。
/** * 饿汉式(静态代码块) * * @author Claw * @date 2020/7/4 12:17. */ public class Singleton2 { /** * 1.构造器私有化 */ private Singleton2() { } /** * 2.内部创建对象实例 */ private static Singleton2 instance; /** * 3.静态代码块中创建单例对象 */ static { instance = new Singleton2(); } /** * 4.提供得到实例对象的方法 */ public static Singleton2 getInstance(){ return instance; } public static void main(String[] args) { Singleton2 instance = Singleton2.getInstance(); Singleton2 instance2 = Singleton2.getInstance(); System.out.println(instance == instance2); } }饿汉式在类加载的时候就创建好了,如何做到在用的时候才创建呢?在当调用获取对象实例方法的时候创建就好了,然后再获取对象实例前,先判断对象实例是否存在。
但这样很明显是有线程安全问题的,当多个线程进入方法,都会判断到当前实例未空,接着创建了多个实例,这不是我们想要的结果。
/** * 单例模式-懒汉式 * @author Claw * @date 2020/7/5 22:03. */ public class LazySingleton { /** * 本类创建对象实例 */ private static LazySingleton instance; /** * 私有化构造器 */ private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { // 如果实例未被创建,才创建实例 instance = new LazySingleton(); } return instance; } }既然是线程不安全的,那么用synchronized修饰不就好了吗?像这样在提供对方实例的方法上用synchronized修饰。
虽然确实解决了线程不安全的问题,但是效率会很低,每个线程在想获取实例的时候,都会进行同步。而其实这个方法只执行一次实例化代码就够了,后面的线程想获得该实例,直接return就行了。方法进行同步效率太低。
因此不建议使用这种方式。
/** * 单例模式-懒汉式 * @author Claw * @date 2020/7/5 22:03. */ public class SafeLazySingleton { /** * 本类创建对象实例 */ private static SafeLazySingleton instance; /** * 私有化构造器 */ private SafeLazySingleton() { } public static SafeLazySingleton getInstance() { if (instance == null) { // 如果实例未被创建,才创建实例 instance = new SafeLazySingleton(); } return instance; } }这个方式在得到对象实例的方法里添加了同步代码块,可仍然没有解决问题,需要获得实例对象的线程都还是需要进入同步方法。
/** * 单例模式-同步代码块懒汉式 * @author Claw * @date 2020/7/5 22:03. */ public class SynchronizedLazySingleton { /** * 本类创建对象实例 */ private static SynchronizedLazySingleton instance; /** * 私有化构造器 */ private SynchronizedLazySingleton() { } /** * 提供得实例对象的方法 * @return */ public static SynchronizedLazySingleton getInstance() { if (instance==null){ // 增加同步代码块来解决线程安全问题 synchronized (SynchronizedLazySingleton.class){ instance = new SynchronizedLazySingleton(); return instance; } } return instance; } }本类创建对象实例的时候,使用了volatile来修饰,并且在获取对象实例的方法里进行了两次检查,一次是加锁的检查实例对象是否为空,第二次是在加锁的情况下检查实例对象是否为空。这样就解决了上面的单例模式产生的问题:每个线程在进入获取对象实例的方法都要进行同步,降低了效率。
检查实例对象是否为空,如果不为空则直接返回。
如果对象实例为空,进入同步代码块,这样保证了只有一个线程进来,这时再次判断实例对象是否为空,然后创建实例对象,返回对象。
为什么要用volatile来修饰实例变量呢?volatile可以在多线程下保证可见性,以及防止指令重排。
假设在单例对象还未被创建时,AB线程都进入到了if(instance==null)的判断里,只有一个线程能进入同步代码块,假设这个线程是A,它创建完实例对象后,因为volatile的可见性,线程B正在同步代码块外等待的时候,得知实例对象已经存在,不再为空了,它就不会再进入同步方法,而是拿好已经实例化的对象return回去了。
那么防止指令重排是什么意思呢?当instance不为null时,仍可能指向一个被部分初始化的对象。
这段代码并不是一个原子性的操作。
类的初始化过程:1.分配对象的内存空间 2.初始化对象 3.设置对象指向刚分配的内存地址
初始化对象的操作会依赖于分配内存空间的操作,但是设置对象指向分配的内存地址的操作不依赖于初始化对象的操作。
也就是,经过指令重排以后,顺序是这样的
1.分配对象的内存空间 2.设置对象指向分配的内存地址 3.初始化对象
当指令重排以后,引用实例对象指向内存空间空间时,实例对象还未被初始化,此时如果另外一个线程调用方法时,由于instance指向了一块内存空间,从而if条件就判断为false,方法返回instance的引用,但是得到却是未完成初始化的实例对象。
利用静态内部类来实现单例模式,利用静态内部类的特性:静态内部类可以自由使用外部类的对象和方法,并且不会因为外部类的加载而加载,只有使用时才加载,利用这个特性实现了懒加载。
把单例对象放在静态内部类进行初始化,当静态内部类初始化时,单例对象也进行了初始化。即虚拟机执行执行类构造器<clinit>方法的过程,虚拟机会保证一个类的<clinit>方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的<clinit>()方法,其他线程都会阻塞等待,利用这个特性,因此是线程安全的。
/** * 单例模式-静态内部类 * * @author Claw * @date 2020/7/5 22:03. */ public class Singleton { /** * 构造器私有化 */ private Singleton() { } /** * 提供对外得到实例对象的方法 * @return */ public static Singleton getInstance(){ return InnerSingleton.instance; } /** * 静态内部类 * 静态内部类可以自由使用外部类的所有变量和方法 * 静态内部类不会随着外部类的初始化而初始化,它需要单独去加载和初始化的 * 当第一次执行getInstance方法时,静态内部类会被初始化 */ private static class InnerSingleton { private static Singleton instance = new Singleton(); } public static void main(String[] args) { for (int i = 0; i < 500; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"::"+getInstance()); },"线程"+i).start(); } } }单例模式的类只允许一个类的实例存在,许多时候整个系统只需要拥有一个全局对象,这样有利于协调系统整体行为,比如把配置文件存在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂情况下的配置管理。
单例模式适合使用的场景:
有频繁实例化然后销毁的情况,也就是频繁的new对象,可以考虑单例模式。创建对象耗时过多或者消耗资源过多,却又经常用到的对象频繁创建IO资源的对象,例如数据库连接池访问本地文件