单例设计模式

Posted figsprite

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例设计模式相关的知识,希望对你有一定的参考价值。

   技术图片

 

 

   JVM将整个运行环境当做一个单例对象。

要点:

  1. 某个类只能有一个实例。
  • 构造器私有化
  1. 它必须自行创建这个实例
  • 含有一个该类的静态变量来保存这个唯一实例
  1. 它必须自行向整个系统提供这个实例
  • 对外提供获取该实例对象的方式
  1. 直接暴露
  2. 用静态变量的get方法获取

几种常见形式:

  1. 饿汉式:直接创建对象,不存在线程安全问题
    1. 直接实例化饿汉式(简洁直观)
    2. 枚举式(最简洁)
    3. 静态代码块的饿汉式(适合复杂实例化)
  2. 懒汉式:延迟创建对象
    1. 线程不安全(适用于单线程)
    2. 线程安全(使用与多线程)
    3. 静态内部类形式(适用于多线程)

饿汉就是很着急,想吃东西,无论我当前要不要这些实例,它都着急着把它创建出来。

懒汉就是很懒,不到万不得已,不会去做,

 

我们先来总结一下刚刚提到的几点:

  • 构造器虚拟化
  • 自行创建,用静态变量存储
  • 向外提供这个实例(因为是用静态变量存储,所以可以通过类名调用)
  • 强调单例,可以使用final修饰

先来写饿汉式的单例,直接创建这个对象,无论是否需要它:

    什么时候不需要这个静态对象呢?比如该类中含有静态方法,我可以通过类名直接调用,这时就没有这个对象啥事了,比如下图中的test()方法,但由于我们的INSTANCE是静态变量,在类加载时就已经创建,是不管test()用没用到的,都会创建出来。

技术图片

技术图片

 

在JDK1.5引入枚举之后,这种方式有更简洁的写法。

枚举类型,表示该类型对象是有限的几个,我们可以限定为一个,就成了单例。

非常简洁,但效果和我们之前写的第一种方法完全一样,获取方式也是可以通过类名.INSTANCE的方式获取。

技术图片

 

第三种方式,看起来和第一种没啥太大区别,无非就是实例化使用静态代码块的方式实现,那我们什么时候回用到它?

比如说,我们有属性需要初始化:

技术图片

 

这样写自然没问题,不过如果我们是通过配置文件的方式配置的,那该怎么办?

技术图片

 

很显然,一二种方法很难做到这一点,第三种方法就可以在加载类时,将配置文件加载进来

技术图片

 

 技术图片

 

为什么说饿汉模式不存在线程安全?因为JAVA中这些静态初始化的代码都会合成为<clinit>,<clinit>是线程安全方法,他们三个都是在类初始化的时候,直接创建的对象的。

 

接下来说说懒汉式:

要等到需要用这个单例时,我们才创建它,这样最初的时候instance应该是空的,为了避免外界在它还是空的时候通过类名.的方式获取,所以把instance设定为private。

在getInstance中,如果instance是空的,那么创建,如果不是,直接返回。

技术图片

 

做一个测试:

 

                Singleton4 singleton1 = Singleton4.getInstance();
		Singleton4 singleton2 = Singleton4.getInstance();
		System.out.println(singleton1==singleton2);
		System.out.println(singleton1);
		System.out.println(singleton2);            

  技术图片

 

在单线程情况下,这个是没有问题的,如果是多线程呢?

也可以做一个测试,通过线程池的方式:

 

                Callable<Singleton4> c = new Callable<Singleton4>() {

			@Override
			public Singleton4 call() throws Exception {
				
				return Singleton4.getInstance();
			}
			
		};
		
		ExecutorService es = Executors.newFixedThreadPool(2);
		Future<Singleton4> f1 = es.submit(c);
		Future<Singleton4> f2 = es.submit(c);
		
		Singleton4 s1 = f1.get();
		Singleton4 s2 = f2.get();
		
		System.out.println(s1==s2);
		System.out.println(s1);
		System.out.println(s2);

  

为了看出效果,我们在创建单例时加一个休眠代码:

	public static Singleton4 getInstance() {
		if(instance==null) {
			
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			instance = new Singleton4();
		}
		return instance;
	}

  

结果很显然,在线程1发现instance没有初始化时,会进入if块中初始化instance,在线程1还没有来得及创建instance时,切换到线程2,它也发现instance没有初始化,也进入if块中,因此出现了两个instance,不符合单例的要求

技术图片

 

我们可以用一个很暴力的方式,直接用synchronize包住这些代码,将Singlgton.class作为锁对象

package singleton;

public class Singleton5 {
	private static Singleton5 instance;

	private Singleton5() {

	}

	public static Singleton5 getInstance() {
		synchronized (Singleton5.class) {
			if (instance == null) {

				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				instance = new Singleton5();
			}
			return instance;
		}
	}

}

  

    做个测试:

技术图片

 

    当然这还不是最优版,最初来的时候,instance还没初始化,为了安全我们使用了synchronize,但是后面的获取行为就没有必要使用这个及其影响性能的synchronize了,所以用一个if可以大大提高性能

package singleton;

public class Singleton6 {
	private static Singleton6 instance;

	private Singleton6() {

	}

	public static Singleton6 getInstance() {
		if(instance==null) {
			synchronized (Singleton6.class) {
				if (instance == null) {

					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					instance = new Singleton6();
				}
				
			}
		}
		
		return instance;
		
	}

}

  

 

    最后一种是又简单,又有效的方式,在静态内部类加载初始化时,才创建INSTANCE实例对象,静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载初始化的

package singleton;

public class Singleton7 {
	;
	
	private Singleton7() {
		
	}
	
	private static class Inner{
		private  static final Singleton7 INSTANCE = new Singleton7();
	}
	
	public static Singleton7 getInstance() {
		return Inner.INSTANCE;
	}
}

  

以上是关于单例设计模式的主要内容,如果未能解决你的问题,请参考以下文章

常用代码片段

性能比较好的单例写法

片段作为 Android 中的单例

单例片段或保存网页视图状态

从 Viewpager2 片段访问父片段函数

你熟悉的设计模式都有哪些?写出单例模式的实现代码