单例模式详解

单例模式详解

  • 1.简介
  • 2.要点
  • 3.构建方式
    • 3.1 饿汉方式(静态常量)(可用)
    • 3.2 饿汉方式(静态代码块)(可用)
    • 3.3 懒汉方式(线程不安全)(不可用)
    • 3.4 懒汉方式(线程安全,同步方法)(不推荐用)
    • 3.5 懒汉方式(线程安全,同步代码块)(不可用)
    • 3.6 懒汉方式(双重检查锁)(推荐用)
    • 3.7 静态内部类(推荐用)
    • 3.8 枚举(推荐用)
  • 4.优缺点
    • 4.1 优点
    • 4.2 缺点

相关文章链接:

Java开发的23种设计模式浅谈

Java开发的23种设计模式详解(创建型模式)

Java开发的23种设计模式详解(结构型模式)

Java开发的23种设计模式详解(行为型模式)

观前提示:

本文所使用Eclipse版本为Photon Release (4.8.0),Idea版本为ultimate 2019.1,JDK版本为1.8.0_141。

1.简介

单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。

单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”

Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”

2.要点

单例模式的要点有三个:

  1. 某个类只能有一个实例。
  2. 它必须自行创建这个实例。
  3. 它必须自行向整个系统提供这个实例。

从具体实现角度来说,就是以下三点:

  1. 单例模式的类只提供私有的构造函数。
  2. 类定义中含有一个该类的静态私有对象。
  3. 该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。

3.构建方式

  1. 饿汉方式。指全局的单例实例在类装载时构建。
  2. 懒汉方式。指全局的单例实例在第一次被使用时构建。

3.1 饿汉方式(静态常量)(可用)

package singleton;

public class HungrySingleton {

	private static HungrySingleton instance = new HungrySingleton();
	
	/**
	 * @Description 私有默认构造方法,防止被实例化
	 */
	private HungrySingleton() {}
	
	/**
	 * @Description 静态工厂方法,创建实例
	 * @return
	 */
	public static HungrySingleton getInstance() {
		return instance;
	}
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

3.2 饿汉方式(静态代码块)(可用)

package singleton;

public class HungrySingleton {

	private static HungrySingleton instance;
	
	static {
		instance = new HungrySingleton();
	}
	/**
	 * @Description 私有默认构造方法,防止被实例化
	 */
	private HungrySingleton() {}
	
	/**
	 * @Description 静态工厂方法,创建实例
	 * @return
	 */
	public static HungrySingleton getInstance() {
		return instance;
	}
}

优缺点同方法3.1。

3.3 懒汉方式(线程不安全)(不可用)

package singleton;

public class LazySingleton {

	private static LazySingleton instance = null;
	
	/**
	 * @Description 私有默认构造方法,防止被实例化
	 */
	private LazySingleton() {}
	
	/**
	 * @Description 静态工厂方法,创建实例
	 * @return
	 */
	public static LazySingleton getInstance() {
		if(instance == null) {
			instance = new LazySingleton(); 
		}
		return instance;
	}
}

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

3.4 懒汉方式(线程安全,同步方法)(不推荐用)

package singleton;

public class LazySingleton {

	private static LazySingleton instance = null;
	
	/**
	 * @Description 私有默认构造方法,防止被实例化
	 */
	private LazySingleton() {}
	
	/**
	 * @Description 静态工厂方法,创建实例
	 * @return
	 */
	public static synchronized LazySingleton getInstance() {
		if(instance == null) {
			instance = new LazySingleton(); 
		}
		return instance;
	}
}

解决方法3.3的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。

缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

3.5 懒汉方式(线程安全,同步代码块)(不可用)

package singleton;

public class LazySingleton {

	private static LazySingleton instance = null;
	
	/**
	 * @Description 私有默认构造方法,防止被实例化
	 */
	private LazySingleton() {}
	
	/**
	 * @Description 静态工厂方法,创建实例
	 * @return
	 */
	public static LazySingleton getInstance() {
		if(instance == null) {
			synchronized (LazySingleton.class) {
				instance = new LazySingleton();
			}
		}
		return instance;
	}
}

由于方法3.4实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

3.6 懒汉方式(双重检查锁)(推荐用)

package singleton;

public class LazySingleton {

	private static volatile LazySingleton instance = null;
	
	/**
	 * @Description 私有默认构造方法,防止被实例化
	 */
	private LazySingleton() {}
	
	/**
	 * @Description 静态工厂方法,创建实例
	 * @return
	 */
	public static LazySingleton getInstance() {
		if(instance == null) {
			synchronized (instance) {
				if(instance == null) {
					instance = new LazySingleton(); 
				}
			}
		}
		return instance;
	}
}

这种写法被称为“双重检查锁”,顾名思义,就是在getInstance()方法中,进行两次null检查。看似多此一举,但实际上却极大提升了并发度,进而提升了性能。在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了。

那么,这种写法是不是绝对安全呢?前面说了,从语义角度来看,并没有什么问题。但是其实还是有坑。说这个坑之前我们要先来看看volatile这个关键字。其实这个关键字有两层语义。第一层语义相信大家都比较熟悉,就是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化。大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题。

注意,前面反复提到“从语义上讲是没有问题的”,但是很不幸,禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。此前的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。

3.7 静态内部类(推荐用)

package singleton;

public class StaticSingleton {

	/**
	 * @Description 私有默认构造方法,防止被实例化
	 */
	private StaticSingleton() {}
	
	private static class StaticSingletonInstance {
		private static final StaticSingleton instance = new StaticSingleton();
	}
	
	public static StaticSingleton getInstance() {
		return StaticSingletonInstance.instance;
	}
}

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要StaticSingleton 类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在StaticSingleton 类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载StaticSingletonInstance 类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

3.8 枚举(推荐用)

package singleton;

public enum EnumSingleton {

	instance;
	public void doSomething() {
		
	}
}

优点:相对于其他单例来说枚举写法最简单,并且任何情况下都是单例的,JDK1.5之后才有的。

4.优缺点

4.1 优点

  1. 实例控制。单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
  2. 灵活性。因为类控制了实例化过程,所以类可以灵活更改实例化过程。

4.2 缺点

  1. 开销。虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。可以通过使用静态初始化解决此问题。
  2. 可能的开发混淆。使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用new关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。
  3. 对象生存期。不能解决删除单个对象的问题。在提供内存管理的语言中(例如基于 .NET Framework的语言),只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。在某些语言中(如 C++),其他类可以删除对象实例,但这样会导致单例类中出现悬浮引用。

你可能感兴趣的:(Java,设计模式)