单例设计模式讲解内容(由于最近工作也是比较饱和时间有限,后续会将文章相关源码托管到git上、请大家原谅)
1、单例模式模式的应用场景
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并 提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 常见的如:ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接 池也都是单例形式。
2、常见的单例模式写法以及如何在多线程下保证单例的安全
a、饿汉式单例
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。
优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。
饿汉单例实现一:
public class DszSingleton {
//先静态、后动态
//先属性、后方法
//先上后下
private static final DszSingleton dszSingleton = new DszSingleton ();
private DszSingleton (){
//即使构造私有了,但是还是要防止反射强吻攻击
if(null != dszSingleton ){
throw new RuntimeException("不要乱搞,别以为不知道你在用反射");
}
}
public static DszSingleton getInstance(){
return DszSingleton ;
}
}
饿汉单例(静态代码加载机制)实现二:
public class DszSingleton {
private static final DszSingleton dszSingleton ;
static { //由此加载
dszSingleton = new DszStaticSingleton();
}
private DszStaticSingleton(){
//即使构造私有了,但是还是要防止反射强吻攻击
if(null != dszSingleton ){
throw new RuntimeException("不要乱搞,别以为不知道你在用反射");
}
}
public static DszStaticSingleton getInstance(){
return dszSingleton;
}
}
b、懒汉式单例
懒汉式单例的特点是:被外部类调用的时候内部类才会加载,下面看懒汉式单例的简单
懒汉单例实现一:
//懒汉式单例 //在外部需要使用的时候才进行实例化
public class LazySimpleSingleton {
//静态块,公共内存区域
private static LazySimpleSingleton lazy = null;
private LazySimpleSingleton(){
//即使构造私有了,但是还是要防止反射强吻攻击
if(null != lazy){
throw new RuntimeException("不要乱搞,别以为不知道你在用反射");
}
}
public static LazySimpleSingleton getInstance(){
if(lazy == null){ //当然这里在多线程下是不安全的,下面会给大家展示这种解决方案
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
懒汉单例(优化)实现二:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazy = null;
private LazyDoubleCheckSingleton(){
//即使构造私有了,但是还是要防止反射强吻攻击
if(null != lazy){
throw new RuntimeException("不要乱搞,别以为不知道你在用反射");
}
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazy == null){//第一层判定
synchronized (LazyDoubleCheckSingleton.class){
if(lazy == null){//第二层判定
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}
c、注册式单例
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标 识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。
->枚举式单例
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
//------MainTest------
public static void main(String[] args) {
try {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData() == instance2.getData());
}catch (Exception e){
e.printStackTrace();
}
}
运行结果:
为什么枚举可以保证单例的安全?让我们来分析一下原理,这里大家需要安装一下Java 反编译工具 Jad
找到工程所 在的 class 目录,复制 EnumSingleton.class
然后切回到命令行,切换到工程所在的 Class 目录,输入命令 jad EnumSingleton.class,我们会在 Class 目录下会多一个 EnumSingleton.jad 文件。
打开 EnumSingleton.jad 文件我们惊奇又巧妙地发现有如下代码:
原来,枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现。
所以序列化和反射都是无法破坏枚举生成的单例,这里序列化不能破坏枚举单例就不分析了,其实跟下面讲的序列化readResolve类似场景,大家可以仿照去分析一下。
这里来分析一下反射为什么不能破坏枚举单例原理:
测试代码会报异常:
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("duasnhouzhi ",666);
}catch (Exception e){
e.printStackTrace();
}
}
这时错误已经非常明显了,告诉我们 Cannot reflectively create enum objects,不能 用反射来创建枚举类型。还是习惯性地想来看看 JDK 源码,进入 Constructor 的 newInstance()方法:
@CallerSensitive
public T newInstance(Object... var1) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!this.override && !Reflection.quickCheckMemberAccess(this.clazz, this.modifiers)) {
Class var2 = Reflection.getCallerClass();
this.checkAccess(var2, this.clazz, (Object)null, this.modifiers);
}
if ((this.clazz.getModifiers() & 16384) != 0) {
//这里意思是在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型, 直接抛出异常。
//这里就给大家分析了为什么java发序列化为什么不能破坏枚举单例
throw new IllegalArgumentException("Cannot reflectively create enum objects");
} else {
ConstructorAccessor var4 = this.constructorAccessor;
if (var4 == null) {
var4 = this.acquireConstructorAccessor();
}
Object var3 = var4.newInstance(var1);
return var3;
}
}
->容器缓存式单例
//Spring中的做法,就是用这种注册式单例
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getInstance(String className){
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。
d、利用ThreadLocal实现单例
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
这里简单讲一下ThreadLocal为什么实现单例,这是根据他的特点决定的,它是一个独占线程安全工具。具体大家可以先去了解一下,这里就不做具体分析了。
e、内部类实现单例
//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
//完美地屏蔽了这两个缺点
public class LazyInnerClassSingleton {
//默认使用LazyInnerClassGeneral的时候,会先初始化内部类
//如果没使用的话,内部类是不加载的
private LazyInnerClassSingleton(){
if(LazyHolder.LAZY != null){
throw new RuntimeException("不允许创建多个实例");
}
}
//每一个关键字都不是多余的
//static 是为了使单例的空间共享
//保证这个方法不会被重写,重载
public static final LazyInnerClassSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
内部类单例调用示例:
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
try{
//很无聊的情况下,进行破坏
Class<?> clazz = LazyInnerClassSingleton.class;
//通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问
c.setAccessible(true);
//暴力初始化
Object o1 = c.newInstance();
//调用了两次构造方法,相当于new了两次
//犯了原则性问题,
Object o2 = c.newInstance();
System.out.println(o1 == o2);
}catch (Exception e){
e.printStackTrace();
}
}
}
3、反射破坏单例解决方案及原理分析
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
try{
Class<?> clazz = LazyInnerClassSingleton.class;
//通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问,设置为true反射就可以强制访问构造方法了
c.setAccessible(true);
//暴力反射调用构造,初始化单例对象
Object o1 = c.newInstance();
//调用了两次构造方法,相当于 new 了两次
Object o2 = c.newInstance();
//比较两次出来的对象是否内存地址一致,显然是不等的。false
System.out.println(o1 == o2);
}catch (Exception e){
e.printStackTrace();
}
}
注:if(null != lazy){ throw new RuntimeException("不要乱搞,别以为不知道你在用反射");}这段代码就是为了防止以上反射去破坏单例环境
4、序列化破坏单例的原理及解决方案
import java.io.Serializable;
public class SeriableSingleton implements Serializable {
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
//以下代码为了防止序列化破坏单例(去掉一下代码序列化就可以成功破坏单例环境),为什么?我们接下来一起分析一下
private Object readResolve(){
return INSTANCE;
}
}
序列化破坏原理分析
首先看一下序列化破坏的java实现代码
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);//A代码
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton)ois.readObject();//B代码
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上是序列化破坏单例的实例教程,调用结果 false
运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其 实很简单,只需要增加 readResolve()方法即可。
我 们 一 起 来 看 看 JDK 的 源 码 实 现 以 一 清 二 楚 了 。 我 们 进 入 ObjectInputStream 类的 readObject()方法,代码如下:
//一下是jdk源码
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
我们发现在readObject中又调用了我们重写的readObject0()方法。进入readObject0() 方法源码,代码如下:
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
//代码效果参考:http://www.zidongmutanji.com/zsjx/124353.html
depth++;
totalObjectRefs++;
try {
switch (tc) {
......
//这里就是为什么反序列可以控制单例,只要实现readResolve()方法
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
......
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
我们看到 TC_OBJECTD 中判断,调用了 ObjectInputStream 的 readOrdinaryObject() 方法,我们继续进入看源码: private Object readOrdinaryObject(boolean
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
//这里调用点进去看看
obj = desc.isInstantiable() ? desc.newInstance() : null;
代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。意味着只要有无参构造方法就会实例化。
这时候,其实还没有找到为什么加上 readResolve()方法就避免了单例被破坏的真正原因。
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
。。。。。。
return obj;
}
//代码效果参考:http://www.zidongmutanji.com/bxxx/213352.html
我再回到 ObjectInputStream 的 readOrdinaryObject()方法继续往下看:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
。。。。。。
if (obj != null &&
handles.lookupException(passHandle) == null &&
//判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod()方法,来看代码:
desc.hasReadResolveMethod())
进入代码看看如下:
逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法 ObjectStreamClass()方法中给 readResolveMethod 进行赋值,来看代码:
上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在 再 回 到 ObjectInputStream 的 readOrdinaryObject() 方 法 继 续 往 下 看 , 如 果 readResolve()存在则调用 invokeReadResolve()方法,来看代码:
我们可以看到在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。
通过 JDK 源码分析我们可以看出,虽然,增加 readResolve()方法返回实例,解决了单 例被破坏的问题。
但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两 次,只不过新创建的对象没有被返回而已。
那如果,创建对象的动作发生频率增大,就 意味着内存分配开销也就随之增大
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
。。。。。。
return obj;
}
5、单例总结
单例的实现几种形式:饿汉式、懒汉式、内部类、枚举、容器、ThreadLocal等六种形式(当然还有其他形式大家可以自行了解)。
破坏单例的常见方式:序列化、java反射。掌握以上哪些单例形式可以被反射或序列化破坏,以及如何才能防止这两种方式的破坏原理。