Java设计模式-单例模式(Singleton Pattern)
目录
- 什么是单例模式
- 单例模式的实现方式及线程安全问题
- Struts2单例模式的应用
单例模式可以非常有效的节省系统开支
一、什么是单例模式
1.1 什么是单例模式?
单例就是单个实例,在进程所分配的内存中仅能存在唯一一个对象。
单例模式就是通过技术手段,来实现这个对象在内存中是唯一的。
1.2 为什么要使用单例模式?
实例的创建需要系统资源的开支,特别是类功能负责、读取IO、网络、数据库连接等操作更是非常大的开支,那么创建一个实例,所有的功能都使用该实例是不不是就可以节省系统开支。
二、单例模式的实现方式及线程安全问题
第一种:饿汉式
package org.singleton;
/**
* 单例模式:饿汉式
*/
public class HungryPrinter {
// 打印机实例
private static final HungryPrinter instance = new HungryPrinter();
// 私有构造函数,禁止使用new创建对象
private HungryPrinter(){}
// 获取打印机对象
public static HungryPrinter getInstance(){
return instance;
}
}
饿汉式的有以下几个特点,这三个特点共同完成了单个实例的限制:
- 使用private私有化实例;final限制修改对象;static在类加载时初始化Printer对象
- 私有化构造函数,禁止主动new对象
- 只能通过getInstance获取实例
static关键字决定了,类加载时便会初始化Printer对象,哪怕没有使用这个对象,这是一种资源浪费。
第二种:懒汉式
package org.singleton;
public class LazyPrinter {
// 打印机实例
private static LazyPrinter instance;
// 私有构造函数,禁止使用new创建对象
private LazyPrinter(){}
public static LazyPrinter getInstance(){
// 只有当对象为空时,才会创建对象
if (instance == null){
instance = new LazyPrinter();
}
return instance;
}
}
上面的代码是不是看来很完美,解决了饿汉式的资源浪费问题,但引入了新的问题,多线程安全!多么头疼的词语。什么情况下会引起多线程安全?
- 存在多线程同时访问一个资源(同时访问instace)
- 访问非方法内部的局部变量(instance就是非方法内部的局部变量)
刚写的懒汉式代码完全符合,假设T1与T2两个线程,同时调用了getInstance方法,T1运行到if (instance == null)处后CPU时间片用完了,这时候轮到T2,T2执行的时候T1还没有成功创建,所以成功创建了对象,当T1重新拿回执行权时,恰好也执行了new操作,又重新创建了对象, 这样就破坏了单例模式。
第三种:双锁检测(double check)
package org.singleton;
public class DoubleCheckPrinter {
// 打印机实例
private static volatile DoubleCheckPrinter instance;
// 私有构造函数,禁止使用new创建对象
private DoubleCheckPrinter(){}
public static DoubleCheckPrinter getInstance(){
if (instance == null){
synchronized (DoubleCheckPrinter.class){
if(instance == null){
instance = new DoubleCheckPrinter();
}
}
}
return instance;
}
}
volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
为什么使用volatile?
让我们来看一下这行代码: instance = new DoubleCheckPrinter() ,执行完这一行代码可以分成三个步骤:
- step1:在内存中分配一块空间。
- step2:对内存空间进行初始化。
- step3:把对象在内存中的位置指向 instance。
如果按照 CPU 或者 JIT 编译器能够按照片正常的指令执行的话,是不需要 volatile,但是 CPU 和 JIT 即时编译器为了能获得性能上的提升,往往会对字节码指令进行重排序,这就会导致 step2 和 step3 执行的顺序颠倒。执行步骤就变成了:
- step1:在内存中分配一块空间。
- step3:把对象在内存中的位置指向 instance。
- step2:对内存空间进行初始化。
现在假设有两个线程T1、T2,T1 线程执行完重排序后的 step3 ,CPU 的执行权被 T2 获得。这个时候,instance 已经不为 null 了,他指向了内存中的一块地址。T2 执行到第一个 if 的时候,发现 instance 不为 null,就直接返回,但是这个 instance 并没有被初始化,这就会导致 T2 在执行的过程中发生不可预知的错误。
现在来看第二个问题,为什么要使用 double check?
现假设有两个 T1 和 T2,T1 执行到注释2处,CPU 的执行权被 T2 抢夺走,T2 执行完成之后创建了一个对象实例,并且释放 Java 的类锁。这个时候 T1 又重新获得了 CPU 的执行权,并且获得了类锁。如果没有第二个 if 的判断,T1 又会重新创建一个 实例对象,这样就破坏了单例。
明白了为什么会有第二个 if ,现在来看为什么会有第一个 if?其实不难看懂,现在假设有一个线程 T3 ,如果没有第一个 if,它就会直接尝试获取锁资源。要知道,锁资源是非常宝贵的,如果每个线程一来就直接申请锁资源,而不是先对 instance 进行判断,这势必会对程序的性能造成影响。
第四种:静态内部类
package org.singleton;
public class InnerClassPrinter {
// 私有构造函数,禁止使用new创建对象
private InnerClassPrinter(){}
public static InnerClassPrinter getInstance(){
return InnerClassHolder.INSTANCE;
}
private static final class InnerClassHolder {
private static final InnerClassPrinter INSTANCE = new InnerClassPrinter();
}
}
感觉和饿汉模式很类似,就是把实例化的过程放到了内部类,解决了类加载时便会初始化实例的缺陷,同时也解决了懒汉式的线程安全问题。
第五种:枚举单例
黑客的思维总是反人类的,上面写的就真的线程安全吗?实例的产生除了new还有其它方式吗?很显然是有的,反射、反序列化可以实现!我们用Double Check验证下
package org.singleton;
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) throws Exception {
DoubleCheckPrinter printer = DoubleCheckPrinter.getInstance();
DoubleCheckPrinter printer1 = DoubleCheckPrinter.getInstance();
System.out.println("通过getInstance获取的对象是否为同一个对象:" + (printer == printer1));
Class<?> cls = DoubleCheckPrinter.getInstance().getClass();
Constructor constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
DoubleCheckPrinter printer2 = (DoubleCheckPrinter) constructor.newInstance();
System.out.println("通过反射获取的对象是否为同一个对象:" + (printer == printer2));
}
}
// 执行结果如下
通过getInstance获取的对象是否为同一个对象:true
通过反射获取的对象是否为同一个对象:false
可以发现破坏了单例模式,同样的反序列化也能破坏单例,需要先让DoubleCheckPrinter类实现序列化接口
package org.singleton;
public class Test {
public static void main(String[] args) throws Exception {
// 将DoubleCheckPrinter对象序列化和到printer.txt
DoubleCheckPrinter printer = DoubleCheckPrinter.getInstance();
FileOutputStream fileOutputStream = new FileOutputStream(new File("printer.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
oos.writeObject(printer);
// 将printer.txt的对象反序列化到Java中
FileInputStream inputStream = new FileInputStream("printer.txt");
ObjectInputStream oosInput = new ObjectInputStream(inputStream);
System.out.println(printer == oosInput.readObject());
}
}
// 运行结果
false
那有没有安全的单例呢,有!来看看枚举单例
package org.singleton;
public enum EnumPrinter {
INSTANCE;
public static EnumPrinter getInstance() {
return INSTANCE;
}
}
package org.singleton;
public class Test {
public static void main(String[] args) throws Exception {
// 将EnumPrinter2对象序列化和到printer.txt
EnumPrinter printer = EnumPrinter.getInstance();
FileOutputStream fileOutputStream = new FileOutputStream("printer.txt");
ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
oos.writeObject(printer);
// 将printer.txt的对象反序列化到Java中
FileInputStream inputStream = new FileInputStream("printer.txt");
ObjectInputStream oosInput = new ObjectInputStream(inputStream);
System.out.println(printer == oosInput.readObject());
}
}
不要奇怪,代码就是这样写。为什么可以保持单例呢?这要从readObject开始分析,readObject会调用ObjectInputStream.readEnum()方法
通过Enum.valueOf方法获取实例,在enum中有个enumConstantDirectory方法,该方法可以获取存放enum类型的常量Map
如果有则返回,没有便会调用getEnumConstantsShared方法返回名为enumConstants的数组,将数组组装为Map
流程图如下
所以Java在运行时,维护了一个枚举类型的对象的Map,以此来实现单例。
Java Enum 类型的语法结构尽管和 java 类的语法不一样,应该说差别比较大。但是经过编译器编译之后产生的是一个 class 文件。该 class 文件经过反编译可以看到实际上是生成了一个类,该类继承了 java.lang.Enum类,而且INSTANCE是final,类中有values()方法,这就解释了前面反射调用EnumPrinter的values方法
# 在class目录执行javap命令
javap EnumPrinter.class
# 输出如下
Compiled from "EnumPrinter.java"
public final class org.singleton.EnumPrinter extends java.lang.Enum<org.singleton.EnumPrinter> {
public static final org.singleton.EnumPrinter INSTANCE;
public static org.singleton.EnumPrinter[] values();
public static org.singleton.EnumPrinter valueOf(java.lang.String);
public static org.singleton.EnumPrinter getInstance();
static {};
}
三、Struts2单例模式的应用
在Struts2中非常典型的关于单例模式的对比就是Action与Servlet的对比,先说结论:
- Action是多例
- Servlet是单例
3.1 Action的分析
Action中每次请求产生一个Action的对象。原因是:struts2的Action中包含数据,例如你在页面填写的数据就会包含在Action的成员变量里面。如果Action是单实例的话,这些数据在多线程的环境下就会相互影响。接下来从代码上分析下:
从web.xml中定义的FilterDispatcher类开始分析,代码跳转到Dispatcher.serviceAction方法,通过ActionProxy通过DefaultActionInvocation的createAction来实例化Action类,最终通过ObjectFactory.buildBean方法实例化Action类。
ObjectFactory.buildBean方法实例化Action类,所以Action是多例,而不是单例
通过查看生成的Action对象的ID也可以看出每次请求,创建的Action的ID是不同的,如下2个图
3.2 Servlet的分析
首先,Servlet用的是单例模式,从形式上看属于“饿汉式模式”
Servlet本质是也只是一个普通的Java类,可以创建很多对象,只不过在loadServlet方法中控制只允许创建一个实例,loadServlet方法中调用instanceManager.newInstance实现实例的创建
在Servlet规范中,对于Servlet单例与多例定义如下:
“Deployment Descriptor”, controls how the servlet container provides instances of the servlet.For a servlet not hosted in a distributed environment (the default), the servlet container must use only one instance per servlet declaration. However, for a servlet implementing the SingleThreadModel interface, the servlet container may instantiate multiple instances to handle a heavy request load and serialize requests to a particular instance.
含义是如果一个Servlet没有被部署在分布式的环境中,一般web.xml中声明的一个Servlet只对应一个实例。而如果一个Servlet实现了SingleThreadModel接口,就会被初始化多个实例。但一般不建议实现,因为每个用户的请求都会创建一个Servlet实例,系统开销非常大。我们来看下SingleThreadModel如何实现的多Servlet实例
StandardWrapper.allocate()方法如下:
if (!singleThreadModel) {
// Load and initialize our instance if necessary
if (instance == null) {
// 省略代码
instance = loadServlet();
// 省略代码
}
// 省略代码
if (singleThreadModel) {
if (newInstance) {
// Have to do this outside of the sync above to prevent a
// possible deadlock
synchronized (instancePool) {
instancePool.push(instance);
nInstances++;
}
}
}
synchronized (instancePool) {
while (countAllocated.get() >= nInstances) {
// Allocate a new instance if possible, or else wait
if (nInstances < maxInstances) {
try {
// 可以看到通过instancePool来保存所有的Servlet实例,loadServlet会返回一个实例
instancePool.push(loadServlet());
nInstances++;
} catch (ServletException e) {
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
throw new ServletException
(sm.getString("standardWrapper.allocate"), e);
}
} else {
try {
instancePool.wait();
} catch (InterruptedException e) {
// Ignore
}
}
}
if (log.isTraceEnabled())
log.trace(" Returning allocated STM instance");
countAllocated.incrementAndGet();
return instancePool.pop();
}
// 省略代码
通过阅读代码,可以发现instancePool中维护了所有的Servlet实例,intancePool是一个Stack,在loadServlet方法中创建
Tomcat创建的Servlet会被Struts2进一步封装为HttpServlet,同样的也是单例模式,下图为简易流程图
以上便是Java中单例模式的使用。