相关文章链接:
Java开发的23种设计模式浅谈
Java开发的23种设计模式详解(创建型模式)
Java开发的23种设计模式详解(结构型模式)
Java开发的23种设计模式详解(行为型模式)
观前提示:
本文所使用Eclipse版本为Photon Release (4.8.0),Idea版本为ultimate 2019.1,JDK版本为1.8.0_141。
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。
单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”
Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
单例模式的要点有三个:
从具体实现角度来说,就是以下三点:
package singleton;
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
/**
* @Description 私有默认构造方法,防止被实例化
*/
private HungrySingleton() {}
/**
* @Description 静态工厂方法,创建实例
* @return
*/
public static HungrySingleton getInstance() {
return instance;
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
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。
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)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
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就行了。方法进行同步效率太低要改进。
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)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
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版本前,双重检查锁形式的单例模式是无法保证线程安全的。
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帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
package singleton;
public enum EnumSingleton {
instance;
public void doSomething() {
}
}
优点:相对于其他单例来说枚举写法最简单,并且任何情况下都是单例的,JDK1.5之后才有的。