内容简介:在面试中相信很多人会被问到:说说你最了解的三个设计模式,日常开发中使用过哪些设计模式等等。最近几篇文章就来学习一下设计模式,这是第一篇文章,也是最常见的模式——单例模式。单例模式(在实现单例时,要保证一个类仅有一个实例,就不能提供公有的构造方法,任由其他类创建实例,对应变量也需要为
在面试中相信很多人会被问到:说说你最了解的三个设计模式,日常开发中使用过哪些 设计模式 等等。最近几篇文章就来学习一下设计模式,这是第一篇文章,也是最常见的模式——单例模式。
什么是单例
单例模式( 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(); } } } 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。