单例模式是非常容易在面试中出现的,不过,会单例模式并不是加分项,但连单例模式都不会,那绝对是一个减分项。
顾名思义,也就是从刚开始就给你初始化一个类对象
实现代码比较简单,我直接贴了
package com.markus.designmode.singleton;
/**
* Author:markusZhang
* Date:Create in 2020/6/10 20:27
* todo: 饿汉式
*/
public class Singleton1 {
private static Singleton1 singleton1 = new Singleton1();
private Singleton1(){}
public static Singleton1 getInstance(){
return singleton1;
}
}
懒汉式的思想就是你用到的时候,我再给你创建一个对象,同样代码也是比较简单的,我直接粘了
package com.markus.designmode.singleton;
/**
* Author:markusZhang
* Date:Create in 2020/6/10 20:30
* todo: 懒汉式 线程不安全
*/
public class Singleton2 {
private static Singleton2 singleton2;
private Singleton2(){}
public static Singleton2 getInstance(){
if(singleton2 == null){
singleton2 = new Singleton2();
}
return singleton2;
}
}
有没有发现图中标注的一行,有严重的线程不安全的场景,也就是当我们有多个线程去获取这个对象的时候,就加入有两个线程 thread1和thread2两个线程同时到达这个地方,并且都判断当前对象为空,那么他们会直接各自去创建一个对象,就会破坏单例模式,于是乎,保证线程安全的饿汉式就出现了
我们怎么保证线程安全的,第一种方式就是用synchronized关键字修饰方法,我们来看看怎么实现的
package com.markus.designmode.singleton;
/**
* Author:markusZhang
* Date:Create in 2020/6/10 20:43
* todo: 懒汉式 线程安全
*/
public class Singleton3 {
private static Singleton3 singleton3;
private Singleton3(){}
public synchronized static Singleton3 getInstance(){
if(singleton3 == null){
singleton3 = new Singleton3();
}
return singleton3;
}
}
他的实现机制就是,在一个获取实例的方法中检验两次是否为空,中间加上锁。我直接上代码了,然后总结下这块的常考点
package com.markus.designmode.singleton;
/**
1. Author:markusZhang
2. Date:Create in 2020/6/10 22:55
3. todo: 双重检验锁
*/
public class Singleton4 {
private static volatile Singleton4 singleton4;
private Singleton4(){}
public static Singleton4 getInstance(){
if (singleton4 == null){
synchronized (Singleton4.class){
if (singleton4 == null){
singleton4 = new Singleton4();
}
}
}
return singleton4;
}
}
我们知道volatile有两个特性,一是保证全局可见性、二是禁止指令重排。说完这个,我再说一下在实例化对象时的操作,例如:Singleton4 singleton4=new Singleton4();大家认为它是原子性的操作吗?其实不是的,它包括三个步骤:
为了达到性能优化,JVM会将顺序打乱,只要不影响最后结果就ok,但是在多线程情况下,这个情况极有可能造成大问题,就比如这个例子,原本是1,2,3执行,如果被打乱顺序了,变成1,3,2执行了,导致我对个线程去访问的时候,对象指向了一个地址之后,它不为空,但是这个对象并没有被初始化,这样就会破坏单例。而volatile可以禁止指令重排,设置一个内存屏障,在屏障前后的执行有顺序,可以避免这种情况。
第一个if的作用就是,第一次判断对象是否被创建过,如果被创建过就直接跳过,可以避免同步的过程,也就是加锁解锁的过程,可以提高效率
第二个if的话,它的作用就是,假如当前并没有创建单例对象,于是有两个线程去竞争锁,必然有一个竞争到锁,一个阻塞,那么这时持有锁的线程去创建了一个单例对象,然后完成任务去释放锁,试想一下,如果没有第二步判断的话,另一个没有获取锁的线程在当前释放锁之后去获得锁,那么它同样也会去创建一次对象,这样就破坏了单例模式。
在叙述这块的时候需要补充一个知识点。无论是非静态内部类还是静态内部类,在类加载时,只要没有被调用,它是不会被加载内存中的,只有在调用内部类时,才会进行类装载,所以在这个单例模式中起到了一个延时加载的效果
package com.markus.designmode;
public class Singleton5 {
static class CreateSingleton{
private static final Singleton5 INSTANCE = new Singleton5();
}
private Singleton5(){}
public static Singleton5 getInstance(){
return CreateSingleton.INSTANCE;
}
}
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。我对枚举还不是很熟,后期学习慢慢更新这块
package com.markus.designmode;
public enum Singleton6 {
INSTANCE;
public void whateverMethod(){
}
}
记得有一次面试,面试官问我在双重检验锁时,会有这样一个场景,当我们序列化的时候,考虑怎样修改这个方法,保证在反序列化过程中不会创建多个实例对象。第一种方法就是枚举,第二种就需要深入到源码去研究,在原本实现上加一个方法。
我们先来看下场景:
package com.markus.designmode;
import java.io.Serializable;
public class Singleton4 implements Serializable {
private static volatile Singleton4 instance;
private Singleton4(){}
public static Singleton4 getInstance(){
if (instance == null){
synchronized (Singleton4.class){
if (instance == null){
instance = new Singleton4();
}
}
}
return instance;
}
}
package com.markus.designmode;
import java.io.*;
public class SerializableDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton4.getInstance());
oos.close();
//反序列化
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton4 instance = (Singleton4) ois.readObject();
ois.close();
//验证单例模式,结果返回false
System.out.println(instance == Singleton4.getInstance());
}
}
通过序列化之后,反序列化创建了多个实例,最后结果返回false,是什么导致了这样的结果呢?我们来看下源码:
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();
}
}
}
private Object readObject0(boolean unshared) throws IOException {
//重点关注下这个方法里的解析对象的流程,其他流程忽略,我们顺着再点击进去
.....
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
.....
}
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
.......
//可以看到,反序列化创建对象是利用反射
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;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
......
return obj;
}
//我们先来看看这一行的条件
/**
* Returns true if represented class is serializable/externalizable and can
* be instantiated by the serialization runtime--i.e., if it is
* externalizable and defines a public no-arg constructor, or if it is
* non-externalizable and its first non-serializable superclass defines an
* accessible no-arg constructor. Otherwise, returns false.
*/
//这一大串的解释大概就是:如果表示的类是可序列化/外部化的,并且可以有序列
//化运行时实例化,或者如果它是可外部化并且定义了一个公共的无参构造函数,或
//者它是不可外部化的并且它的第一个非序列化超累定义了一个可访问的无参构造函
//数,这些情况返回true,否则返回false
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
private Object readOrdinaryObject(boolean unshared){
...
// 在 Singleton4 添加 readResolve 方法之后 desc.hasReadResolveMethod() 该方法执行为 true
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
// 通过反射调用 HungrySingleton 类中的 readResolve 方法返回,
// 即为我们的单例对象,所以这里讲此处返回的对象赋值给 obj,所以这里我们找到了答案
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;
}
//这个位于ObjectStreamClass类中的构造器中被赋予,如果涉及序列化的话,就需要有这个方法,如果没有这个方法的话,那么序列化通过反射就会创建一个新的Singleton对象
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
解决:在原先的单例类里加一个方法,代码如下
public class Singleton4 implements Serializable {
private static volatile Singleton4 singleton4;
private Singleton4(){}
public static Singleton4 getInstance(){
if (singleton4 == null){
synchronized (Singleton4.class){
if (singleton4 == null){
singleton4 = new Singleton4();
}
}
}
return singleton4;
}
//也就是加上这个方法,那么创建对象的时候,就是这个对象了
public Object readResolve(){
return singleton4;
}
}
序列化与反序列话破坏单例的情况就解决了。
我们先来补充下反射的概念,反射就是,在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个属性和方法。理解了这个概念,我们回想下,在写单例模式的方法中(枚举除外),我们会使构造器私有,防止外部能够通过构造器创建对象。但是,通过反射就可以调用它的私有构造器,我们来看下这个场景。比如用饿汉式创建单例对象,代码如下
package com.markus.designmode;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class ReflectDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = Class.forName("com.markus.designmode.Singleton1");
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton1 instance = (Singleton1) constructor.newInstance();
//结果返回false
System.out.println(instance == Singleton1.getInstance());
}
}
由此可知,反射可以破坏单例,那么如何解决呢?最显著的方法就是通过枚举实现单例模式,我们继续测试一下,将创建单例的类改为枚举方式,再进行测试,会出现这样的结果:
错误提示说,没有这个构造器。也就是枚举保证实例只被初始化一次