单例模式分为懒汉式和饿汉式, 懒汉式是采用了延迟初始化的方式进行创建对象的, 而饿汉式是在类加载的时候就进行初始化的, 下面分别对两种模式进行一些分析
1. 首先, 来看一个最简单的懒汉式单例代码实现 :
/**
* 单例模式 : 懒汉模式
*
* @author 七夜雪
* 2018/11/14 17:21
*/
public class SingletonLazyInit {
private static SingletonLazyInit singleton;
// 单例模式构造器必须是私有的,防止外部使用new关键字构造新对象
private SingletonLazyInit() {
}
public static SingletonLazyInit getInstance(){
if (null == singleton){
singleton = new SingletonLazyInit();
}
return singleton;
}
}
上面的代码很简单, 在单线程情况下也是没有问题的, 但是在多线程的情况下就可能会存在问题, 比如说同时又两个线程t1, t2同时调用SingletonLazyInit的getInstance方法, t1执行到singleton = new SingletonLazyInit();这一句但是还没执行完时, t2判断singleton仍然为空, 扔能进入if代码块中, 这是t1对象创建成功并返回, 然后t2线程再进行对象创建, 这时t1和t2就获取的就不是一个对象了
2. 对于上面这种情况, 最简单的一种解决方案, 就是对getInstance方法进行加锁, 增加Synchronized关键字, 对静态方法进行加锁, 锁定的是当前类的class对象, 加锁之后的代码如下:
/**
* 单例模式 : 加锁单例
*
* @author 七夜雪
* 2018/11/14 17:21
*/
public class SingletonSynchronized {
private static SingletonSynchronized singleton;
private SingletonSynchronized() {
}
// 写法一
public static synchronized SingletonSynchronized getInstance(){
if (null == singleton){
singleton = new SingletonSynchronized();
}
return singleton;
}
// 写法二
// public static synchronized SingletonSynchronized getInstance(){
// synchronized (SingletonSynchronized.class){
// if (null == singleton){
// singleton = new SingletonSynchronized();
// }
// }
// return singleton;
// }
}
/**
* 单例模式 : 双重检查
* 防止并发情况下问题
* @author 七夜雪
* 2018/11/14 17:26
*/
public class SingletonDoubleCheck {
private static SingletonDoubleCheck singleton;
private SingletonDoubleCheck() {
}
public static SingletonDoubleCheck getInstance(){
if (null == singleton){
// 加锁保证这个代码块只有一个线程能够执行
synchronized (SingletonDoubleCheck.class){
// 避免在获取锁的过程中, 对象被其他线程创建, 所以再进行一次检查
if (null == singleton) {
singleton = new SingletonDoubleCheck();
}
}
}
return singleton;
}
}
- singleton = new SingletonDoubleCheck();首先这行看起来只有一句, 但其实是分成了三条指令进行执行的 :
- 分配内存给这个对象
- 初始化对象
- 设置singleton指向刚刚分配的内存地址
通过上面的分析可以知道, 上面的这种double check的单例模式仍然是存在问题, 那这个问题该如何解决呢?
4. 事实上解决上面的这个问题非常简单, 只需要修改一行代码 :
只需要对这一行声明private static SingletonDoubleCheck singleton;
增加一个volatile关键字即可 : private static volatile SingletonDoubleCheck singleton;
为何增加一个volatile就能解决这个问题呢, 简单来说就是volatile可以禁止2, 3两步进行指令重排序, 具体更多关于volatile的信息可以参考 : https://blog.csdn.net/love905661433/article/details/82833361
除了上面的volatile关键字之外, 还有第二种方式解决指令重排序造成的线程安全问题 :
/**
* 基于静态内部类的单例模式
*
* @author 七夜雪
* @create 2018-11-22 20:57
*/
public class StaticInnerClassSingleton {
// 注意私有的构造方法
private StaticInnerClassSingleton(){
}
public StaticInnerClassSingleton getInstance(){
return InnerClass.instance;
}
private static class InnerClass{
private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
}
为何使用这种静态内部类的方式就能解决指令重排序问题呢, 这是因为jvm在进行Class对象初始化的时候, 会增加Class对象的初始化锁, 所以哪个对象能够拿到静态内部类的初始化锁, 哪个对象就能完成对静态内部类的初始化, 所以即使存在指令重排序, 也不影响线程安全性问题, 如下图:
关于Class类的初始化问题, 这里提一下, 在发生下面几种情况下, 会对Class类进行初始化:
/**
* 单例模式 : 饿汉模式
* @author 七夜雪
* 2018/11/14 17:13
*/
public class Singleton {
private final static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
}
/**
* 单例模式 : 静态代码块饿汉模式
*
* @author 七夜雪
* 2018/11/14 17:13
*/
public class StaticHungrySingleton {
private static StaticHungrySingleton instance;
static {
instance = new StaticHungrySingleton();
}
public static StaticHungrySingleton getInstance() {
return instance;
}
private StaticHungrySingleton() {
}
}
是不是经过上面一系列优化之后, 觉得单例模式已经不存在安全性问题了, 事实上单例模式仍然可以被破坏, 下面来继续看
以饿汉式单例为例, 单例代码如下:
/**
* 单例模式 : 饿汉模式
* @author 七夜雪
* 2018/11/14 17:13
*/
public class Singleton implements Serializable {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
}
上面的单例的代码实现了一个序列化接口, 避免序列化的时候报错, 下面我们来使用序列化的方式来测试一下序列化是不是能破坏单例, 代码如下:
/**
* 破坏单例模式的测试
*
* @author 七夜雪
* @create 2018-11-22 21:51
*/
public class DestroySingletonTest {
/**
* 使用序列化方式破坏单例
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Singleton instance = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("sinleton.dat"));
oos.writeObject(instance);
oos.close();
File file = new File("sinleton.dat");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
输出结果:
pattern.creational.singleton.Singleton@135fbaa4
pattern.creational.singleton.Singleton@568db2f2
false
从输出结果可以看出, 序列化之后再进行反序列化的话, 得到的就不是同一个对象了, 这就破坏了单例模式的单例特性, 那应该如何解决这个问题呢, 解决的方法也很简单, 在Singleton类中加入一个readResolve()方法即可, 增加之后Singleton类代码如下:
import java.io.Serializable;
/**
* 单例模式 : 饿汉模式
* @author 七夜雪
* 2018/11/14 17:13
*/
public class Singleton implements Serializable {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
public Object readResolve(){
return instance;
}
}
然后增加了readResolve方法之后, 再执行一次上面的测试类, 结果如下:
pattern.creational.singleton.Singleton@135fbaa4
pattern.creational.singleton.Singleton@135fbaa4
true
发现反序列化对单例的破坏已经解决了, 为什么加了readResolve方法之后就可以了呢, 我们来看下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);
// ......................看到这个readObject0方法即可,下面的部分省略了........................
/**
* Underlying readObject implementation.
*/
private Object readObject0(boolean unshared) throws IOException {
// ..............无关紧要的代码这里省略了.................
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
switch (tc) {
// ..............其他case省略, 这里是Object类型.................
case TC_OBJECT:
// checkResolve方法和readOrdinaryObject就是处理这个问题的
return checkResolve(readOrdinaryObject(unshared));
// ..............已经找到关键代码了, 下面的代码这里就省略了.................
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
// ....................篇幅有限,这里只保存关键代码.........................
ObjectStreamClass desc = readClassDesc(false);
Object obj;
// hasReadResolveMethod这个就是判断原来的Singleton中是否有readResolve方法的, 这里是根据方法名判断的, 所以Singleton中方法名就只能是readResolve
//有这么一行代码 readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
// 这里就是重点了, 如果存在readResolve方法, 就通过反射调用readResolve方法返回
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
通过源码的分析, 可以指定为啥加了readResolve方法就可以解决序列化反序列化的问题了
同样的, 以上面的Singleton代码为例, 来试一下通过反射能不能破坏单例的特性, 反射的测试代码如下:
/**
* 使用反射方式破坏单例
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Singleton instance = Singleton.getInstance();
Class clazz = Singleton.class;
// 获取构造方法
Constructor constructor = clazz.getDeclaredConstructor();
// 由于构造方法是私有的, 所以这里先开放权限
constructor.setAccessible(true);
Singleton newInstance = (Singleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
输出结果:
pattern.creational.singleton.Singleton@1540e19d
pattern.creational.singleton.Singleton@677327b6
false
从输出结果可以看出, 通过反射,确实破坏了单例的特性, 那么如何解决这个问题呢, 可以在构造器中增加一个判断, 代码如下:
/**
* 单例模式 : 饿汉模式
* @author 七夜雪
* 2018/11/14 17:13
*/
public class Singleton implements Serializable {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {
// 防止反射破坏单例
if (instance != null) {
throw new RuntimeException("单例构造器不允许反射调用");
}
}
/**
* 防止序列化破坏单例
* @return
*/
public Object readResolve(){
return instance;
}
}
再次执行反射的测试代码, 得到如下结果:
Exception in thread “main” java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at pattern.creational.singleton.DestroySingletonTest.main(DestroySingletonTest.java:26)
Caused by: java.lang.RuntimeException: 单例构造器不允许反射调用
at pattern.creational.singleton.Singleton.(Singleton.java:19)
… 5 more
从结果可以看到, 现在就无法通过反射来破坏单例的特性了, 加了上面的判断之后, 保证了构造器只能执行一次, 而饿汉式的单例模式在类加载的时候, 对象就已经创建了, 所以可以通过这个方式来处理反射的问题, 但是如果使用懒汉模式的话, 就无法避免反射调用的问题了, 因为没有办法区分第一次调用构造方法是通过反射调用的, 还是通过getInstance方法触发的, 所以就没办法通过这个方式解决了
/**
* @author 七夜雪
* 使用枚举实现单例
* @create 2018-11-23 0:11
*/
public enum EnumSingleton {
INSTANCE;
private Object data;
public static EnumSingleton getInstance(){
return INSTANCE;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
测试代码 :
/**
* 使用序列化方式破坏单例
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
System.out.println("----------测试序列化方法对单例的破坏----------");
EnumSingleton instance = EnumSingleton.getInstance();
// 同时设置一个Object对象, 看下反序列化之后有没有被破坏
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("sinleton.dat"));
oos.writeObject(instance);
File file = new File("sinleton.dat");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumSingleton newInstance = (EnumSingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
System.out.println(instance.getData() == newInstance.getData());
}
输出结果:
----------测试序列化方法对单例的破坏----------
INSTANCE
INSTANCE
true
true
下面测试一下使用反射破坏单例模式, 测试代码如下:
/**
* 使用序列化方式破坏单例
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
System.out.println("----------测试序列化方法对单例的破坏----------");
EnumSingleton instance = EnumSingleton.getInstance();
Constructor constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton newInstance = (EnumSingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
输出结果:
Exception in thread “main” java.lang.NoSuchMethodException: pattern.creational.singleton.EnumSingleton.()
----------测试序列化方法对单例的破坏----------
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at pattern.creational.singleton.EnumTest.main(EnumTest.java:43)
执行上面的测试程序, 抛了个异常, 是没找到无参的构造方法, 对生成的class进行反编译之后发现, 只有如下构造方法:
private EnumSingleton(String s, int i)
{
super(s, i);
}
那下面修改一下测试代码, 再进行一次测试, 修改后测试代码如下:
/**
* 使用序列化方式破坏单例
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
System.out.println("----------测试序列化方法对单例的破坏----------");
EnumSingleton instance = EnumSingleton.getInstance();
Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingleton newInstance = (EnumSingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
再次执行测试代码, 输出结果如下 :
----------测试序列化方法对单例的破坏----------
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at pattern.creational.singleton.EnumTest.main(EnumTest.java:45)
发现通过反射调用的话, 依然会抛一个异常, 所以使用Enum来实现单例是安全的, 通过序列化和反射都无法破坏其单例特性
我们从JDK源码来看下, 为什么Enum具有这些特征
1. 首先看下为什么反射无法创建枚举类型的类, 看下newInstance源码, 源码中有下面这一句:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
可以看出, 如果对一个枚举类型使用反射调用newInstance生成对象的话, 就会直接抛一个异常出来, 保证了Enum类型无法通过反射创建对象
2. 然后我们看下为什么无法通过序列化破坏Enum的单例特性, 依然是看ObjectInputStream的readObject方法 :
方法调用顺序readObject->readObject0->readEnum, readEnum方法代码如下
//.....................忽略不重要的代码..........................
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
// 这里根据name来创建的, 而EnumSingleton只有一个INSTANCE
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
// 首先类编程了final类
public final class EnumSingleton extends Enum {
// 构造器是私有构造器
private EnumSingleton(String s, int i)
{
super(s, i);
}
// INSTANCE属性是static final修饰的
public static final EnumSingleton INSTANCE;
// 使用了静态代码块初始化INSTANCE
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
从反编译的结果来看, Enum很像是饿汉式的单例模式, 所以在Enum类加载的时候, 就已经完成了类的创建了
/**
* 容器单例
*
* @author 七夜雪
* @create 2018-11-23 1:44
*/
public class ContainerSingleton {
private static Map<String, Object> map = new HashMap<>();
private ContainerSingleton() {
}
public void putInstance(String key, Object instance) {
if (key != null && instance != null && !"".equals(key)){
if (!map.containsKey(key)){
map.put(key, instance);
}
}
}
public Object getInstance(String key) {
return map.get(key);
}
}
/**
* 线程单例-伪单例
*
* @author 七夜雪
* @create 2018-11-23 9:42
*/
public class TheadLocalSingleton {
private final static ThreadLocal<TheadLocalSingleton> threadLocal = new ThreadLocal<TheadLocalSingleton>(){
@Override
protected TheadLocalSingleton initialValue() {
return new TheadLocalSingleton();
}
};
public TheadLocalSingleton() {
}
public static TheadLocalSingleton getInstance(){
return threadLocal.get();
}
}
下面写个测试代码对线程单例这种情况做一个简单测试:
public static void main(String[] args) {
TheadLocalSingleton instance = TheadLocalSingleton.getInstance();
TheadLocalSingleton newInstance = TheadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + "---" + instance);
System.out.println(Thread.currentThread().getName() + "---" + newInstance);
new Thread(()->{
// jdk8的lambda式, 等价于new Thread(new Runnable(){...});
System.out.println(Thread.currentThread().getName() + "---" + TheadLocalSingleton.getInstance());
}).start();
}
输出结果 :
main—pattern.creational.singleton.TheadLocalSingleton@1540e19d
main—pattern.creational.singleton.TheadLocalSingleton@1540e19d
Thread-0—pattern.creational.singleton.TheadLocalSingleton@2314afc1
从输出结果可以看出, 两个main线程获取到的对象是同一个, 另外一个线程获取的对象是另外一个, 这就说明了这种情况是保证每个线程中对象是单例的
本文参考:
慕课网