内容简介:在面试中相信很多人会被问到:说说你最了解的三个设计模式,日常开发中使用过哪些设计模式等等。最近几篇文章就来学习一下设计模式,这是第一篇文章,也是最常见的模式——单例模式。单例模式(在实现单例时,要保证一个类仅有一个实例,就不能提供公有的构造方法,任由其他类创建实例,对应变量也需要为
在面试中相信很多人会被问到:说说你最了解的三个设计模式,日常开发中使用过哪些 设计模式 等等。最近几篇文章就来学习一下设计模式,这是第一篇文章,也是最常见的模式——单例模式。
什么是单例
单例模式( Singleton Pattern ),顾名思义,即保证一个类仅有一个实例,并在全局中提供一个访问点。
在实现单例时,要保证一个类仅有一个实例,就不能提供公有的构造方法,任由其他类创建实例,对应变量也需要为 static ,只在加载时初始化一次。另外呢,要在全局中都能访问到,还需要提供一个静态的公有方法来进行访问。
具体实现方式比较多,对于不同的场景,也应该选择不同的方式,例如是否需要保证线程安全,是否需要延迟加载。下面具体来看一下。
饿汉式(线程安全)
根据上面对单例模式实现的说明,可以很容易地想到如下实现:
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() { }
public static Singleton1 getInstance() {
return instance;
}
}
复制代码
这种方式在该类第一次被加载时,就会创建好该实例。这就是所谓的饿汉式,也就是,在想要使用实例时,立刻就能拿到,而不需要进行等待。
另外这种方式,由 JVM 保证其线程安全。但是这种方式可能会造成资源消耗,因为有可能这个实例根本就用不到,而进行不必要的加载。
懒汉式(非线程安全)
上述方式在类加载时就进行实例化,可能会造成不必要的加载。那么我们可以在其真正被访问的时候,再进行实例化,于是可以写出如下方式:
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() { }
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
复制代码
在 getInstance 方法中,第一次访问时,由于没有初始化,才去进行进行初始化,在后续访问时,直接返回该实例即可。这就是所谓的懒汉式,也就是,它不会提前把实例创建出来,而是将其延迟到第一次被访问的时候。
但是懒汉式存在线程安全问题,如下图:
在多线程场景下,如果有两个线程同时进入 if 语句中,则这两个线程分别创建了一个对象,在两个线程从 if 中退出时,就创建了两个不一样的对象。
懒汉式(线程安全)
既然普通的懒汉式会出现线程安全问题,那么给创建对象的方法加锁即可:
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() { }
public static synchronized Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
复制代码
上述这种做法虽然在多线程场景下也能正常工作,也具备延迟加载。但由于 synchronized 方法锁住了整个方法,效率比较低。于是,聪明的小伙伴,可以很容易想到,使用同步方法块,来减小加锁的粒度。
看下面两种做法,加锁粒度确实减小了,但是它们却并不能保证线程安全:
synchronized (Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
}
}
复制代码
由于指定重 排序 出现问题,后面介绍双重校验锁时会详细说。
if (instance == null) {
synchronized (Singleton4.class) {
instance = new Singleton4();
}
}
复制代码
如果 synchronized 加在 if 语句外面,这和普通的懒汉式做法一样,没有区别。如果有两个线程分别进入 if 语句,虽然也有加锁操作,但是两个线程都会执行实例化,也就是会进行两次实例化。
双重校验锁(线程安全)
于是引出了双重校验锁方式,可以先判断对象是否实例化,如果没有再进行加锁,再加锁之后,再次判断是否实例化,如果仍然没有实例化,才实例化对象。
这种做法的完整代码如下:
public class Singleton5 {
private static volatile Singleton5 instance;
private Singleton5() { }
public static Singleton5 getInstance() {
// 如果已经实例化,则直接返回,不用加锁,提升性能
if (instance == null) {
synchronized (Singleton5.class) {
// 再次检查,保证线程安全
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}
复制代码
可以看到,在 synchronized 语句前后,有两个 if 判断,这就是所谓的双重校验锁。
使用 volatile
其实,如果仅仅是双重校验的话,仍然不能保证线程安全问题。这就要分析 instance = new Singleton5(); 这段代码。
虽然代码只有一句,但在 JVM 中它其实被分为三步执行:
instance instance instance
但由于编译器或处理器可能会对指令重排序,执行的顺序就有可能变成 1->3->2 。这在单线程环境下不会出现问题,但是在多线程环境下可能会导致一个线程获得还没有初始化的实例。
例如,线程 A 执行了第 1 、 3 步后,此时线程 B 调用 getInstance() 方法,判断 instance 不为空,因此返回 instance 。但此时 instance 还未被初始化。
所以,就需要使用 volatile 关键字来修饰 instance ,禁止编译器的指令重排序,保证在多线程环境下也能正常运行。
静态内部类式(线程安全)
目前双重校验锁的做法看起来不错,使用延迟加载,在保证线程安全的同时,加锁粒度也比较小,效率还不错。那还有没有其他方法呢?
那就是使用静态内部类来实现,来看一下它的实现:
public class Singleton6 {
private Singleton6() { }
private static class InnerSingleton {
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance() {
return InnerSingleton.INSTANCE;
}
}
复制代码
在这种实现中,当外部类 Singleton6 类被加载时,静态内部类 InnerSingleton 并没有被加载。
而是只有当调用 getInstance 方法,从而访问类的静态变量时,才会加载内部类,从而实例化 INSTANCE 。并且 JVM 能确保 INSTANCE 只能被实例化一次,即它也是线程安全的。
枚举式(线程安全)
另外,使用枚举实现单例也是一种不错的方式,代码非常简单:
public enum Singleton6 {
INSTANCE();
Singleton6() { }
}
复制代码
枚举的实现中,类被定义为 final ,其枚举值被定义为 static final ,对枚举值的初始化放在静态语句块中。所以,对象在该类第一次被加载时实例化,这不仅避免了线程安全问题,而且也避免了下面提到的反序列化对单例的破坏。
单例与序列化
现在来看一下,对象在序列化和反序列化时,是否还能够保证单例。
这里使用双重校验锁实现的单例类,对 Singleton5 类添加 Serializable 接口,然后进行测试:
public class SingletonTest {
public static void main(String[] args) {
Singleton5 instance1 = Singleton5.getInstance();
try ( ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")) ){
oos.writeObject(instance1);
} catch (IOException e) {
e.printStackTrace();
}
Singleton5 instance2 = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("tempFile"))) ){
instance2 = (Singleton5) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(instance1 == instance2);
}
}
// false
复制代码
可以看到,对 Singleton5 进行反序列得到的是一个新的对象,如此就破坏了 Singleton5 的单例性。
我们可以在 Singleton5 类中添加一个 readResolve() 方法,并在该方法中指定要返回的对象的生成策略:
public class Singleton5 implements Serializable {
private static volatile Singleton5 instance;
private Singleton5() { }
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
// 添加 readResolve 方法
private Object readResolve() {
return instance;
}
}
复制代码
通过 debug 方法查看源码,在 readObject 方法的调用栈中,可以看到 ObejctStreamClass 类的 invokeReadResolve 方法:
如果定义了 readResolve 方法,会通过反射进行调用,根据指定的策略来生成对象。
有哪些好的单例模式实践
JDK#Runtime
该类用于获取应用运行时的环境。可以看到这是一个饿汉式的单例。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
复制代码
Spring#Singleton
在 Spring 中定义 Bean 时,可以指定是单例还是多例(默认为单例):
@Scope("singleton")
复制代码
查看其源码,单例模式实现如下:
public abstract class AbstractFactoryBean<T>
implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
private T singletonInstance;
@Override
public void afterPropertiesSet() throws Exception {
// 扫描配置时,单例模式
// 就会将 initialized 置为 true
if (isSingleton()) {
this.initialized = true;
// 调用子类方法创建对象
this.singletonInstance = createInstance();
this.earlySingletonInstance = null;
}
}
@Override
public final T getObject() throws Exception {
if (isSingleton()) {
return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
}
else {
return createInstance();
}
}
}
复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript & jQuery
David Sawyer McFarland / O Reilly / 2011-10-28 / USD 39.99
You don't need programming experience to add interactive and visual effects to your web pages with JavaScript. This Missing Manual shows you how the jQuery library makes JavaScript programming fun, ea......一起来看看 《JavaScript & jQuery》 这本书的介绍吧!