单例模式的重点
- 私有化构造器
- 保证线程安全
- 延迟加载
- 防止序列化和反序列化破坏单例
- 防御反射攻击单例
饿汉式单例
饿汉式单例是指在单例类首次加载时就创建实例。
代码语言:javascript复制/**
* @Description TODO
* @Author Sitr
* @Date 2021/1/1 22:43
* @Version 1.0
**/
public class HunGry {
//在类加载时执行该语句,创建实例。
//final表示该实例不可修改,可以防止被使用反射机制修改。
private static final HunGry hungrySingleton;
static{
hungrySingleton = new HunGry();
}
//构造方法私有。这样外界就无法使用该构建方法,而只能使用上面所创建的类。
private HunGry(){}
//提供全局访问点,每当外界使用该类时,通过该方法使用。
public static HunGry getInstance(){
//返回实例
return hungrySingleton;
}
}
饿汉式单例,无论是否使用,都直接初始化。其缺点则是会浪费内存空间。因为假如整个实例都没有被使用,那么这个类依然会创建,这就白创建了。
于是,有了第二种单例方法:
懒汉式单例
懒汉式单例是在被外部调用时才会创建的单例。
代码语言:javascript复制/**
* @Description TODO
* @Author Sitr
* @Date 2021/1/1 22:53
* @Version 1.0
**/
public class LazySingle {
private static LazySingle lazySingle = null;
private LazySingle(){}
public static LazySingle getInstance(){
//若lazySingle为空,即创建该实例,否则直接调用已有对象。
if(lazySingle == null){
lazySingle = new LazySingle();
}
return lazySingle;
}
}
虽然懒汉式单例解决了内存空间浪费的问题,但是却带来了一个线程不安全的问题。
何以见得?举个简单的例子。 ExectorThread.java
代码语言:javascript复制/**
* @Description TODO
* @Author Sitr
* @Date 2021/1/1 23:00
* @Version 1.0
**/
public class ExectorThread implements Runnable {
public void run(){
LazySingle single = LazySingle.getInstance();
System.out.println(Thread.currentThread().getName() ":" single);
}
}
LazyTest.java
代码语言:javascript复制/**
* @Description TODO
* @Author Sitr
* @Date 2021/1/1 23:02
* @Version 1.0
**/
public class LazyTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("Exctor End");
}
}
运行:
代码语言:javascript复制Exctor End
Thread-0:com.edu.pattern.singleton.lazy.LazySingle@994e424
Thread-1:com.edu.pattern.singleton.lazy.LazySingle@781d0f57
可见,它会创建两个类,从而出现线程不安全的问题。
但是可以在getInstance方法上加一个synchronize来加锁。尽管synchronize自jdk1.6 之后性能优化了不少,但是仍然不可避免地在性能上存在一定问题。
所以,我们可以试着把synchronize判断里面,进行双重判断,也即双重检查锁。
代码语言:javascript复制/**
* @Description TODO
* @Author Sitr
* @Date 2021/1/1 23:14
* @Version 1.0
**/
public class LazyDoubleCheck {
private volatile static LazyDoubleCheck lazySingle = null;
private LazyDoubleCheck(){}
public static LazyDoubleCheck getInstance(){
if(lazySingle == null){
synchronized (LazyDoubleCheck.class){//把synchronize放到里面
if (lazySingle ==null){//再加一个判断
lazySingle = new LazyDoubleCheck();
}
}
}
return lazySingle;
}
}
聊聊指令重排序
众所周知,创建类的时候,是会转换成JVM指令执行的,以下为其执行顺序:
- 分配内存给这个对象
- 初始化对象
- 将初始化后的对象和内存地址建立关联,并复制
- 用户初次访问
而由于CPU执行是抢占式的,因此第2第3个容易混淆。有时候可能先执行3,然后再执行2。多线程租的时候,容易发生此问题。而要解决此问题,则常用一个指令 volatile。
我们还有没有更好地懒汉式单例方式?
内部类。
静态内部类单例
代码语言:javascript复制/**
* @Description TODO
* @Author Sitr
* @Date 2021/1/1 23:36
* @Version 1.0
**/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){}
public static final LazyInnerClassSingleton getInstance(){
return LazyHolder.lazy;
}
private static class LazyHolder{
private static final LazyInnerClassSingleton lazy = new LazyInnerClassSingleton();
}
}
使用静态内部类,前程没有使用synchronize,所以性能会好上一些。LayzHolder里面的逻辑需要等到外部调用时才会执行,巧妙地利用了内部类的特性。利用JVM底层执行逻辑,完美避免了线程安全问题。可以说是性能最优的写法。
虽然说,此时线程安全问题已经解决了,构造方法尽管是私有的,但是有可能被使用反射。 因此,我们可以在私有的构造方法改造为:
代码语言:javascript复制 private LazyInnerClassSingleton(){
if(LazyHolder.lazy != null){
throw new RuntimeException("不允许构建多个实例");
}
}
这个就可以解决由于反射而破坏单例的情况。
但是,尽管如此,仍然有情况可以破坏该单例模式。 便是 序列化。 假如,现在我们有一个实现了序列化接口的类。 SeriableSingleton.java
代码语言:javascript复制/**
* @Description 实现了序列化接口的懒汉式单例
* @Author Sitr
* @Date 2021/1/2 0:12
* @Version 1.0
**/
public class SeriableSingleton implements Serializable {
/**
* @Author Sitr
* @Description
* 序列化就是把内存中的状态通过转换,转换成字节码的形式
* 从而转换成一个IO流,写入到其他地方(可以是磁盘、网络IO) 内存中状态给永久保存下来。
*
* 而反序列化
* 将已经持久化的字节码内容,转换为IO流
* 通过IO流的读取,进而将读取的内容转化为Java对象
* 在转换过程中会重新创建对象
* @Date 0:13 2021/1/2
* @Param
* @return
**/
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){return INSTANCE;}
}
用于测试的类: SeriableSingletonTest.java
代码语言:javascript复制/**
* @Description 测试序列化破坏单例
* @Author Sitr
* @Date 2021/1/2 0:11
* @Version 1.0
**/
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果为:
代码语言:javascript复制com.edu.pattern.singleton.seriable.SeriableSingleton@5a10411
com.edu.pattern.singleton.seriable.SeriableSingleton@4eec7777
false
显然,此时单例模式被破坏了。 那么我们该如何解决呢? 我们可以从SeriableSingleton.java里面加入以下代码,重写readResolve方法
代码语言:javascript复制 private Object readResolve(){
return INSTANCE;
}
现在的SeriableSingleton.java如下:
代码语言:javascript复制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;
}
}
即可解决。
那么,问题来了,为什么重写了readResolve()就可以解决呢?我们可以来看一下源码。 在SeriableSingletonTest.java中,我们是通过
代码语言:javascript复制 s1 = (SeriableSingleton)ois.readObject();
来赋予s1对象的。先是readObject(),然后通过SeriableSingleton进行强转。先看一下readObject。
代码语言:javascript复制= = 我发现不同的版本的jdk其实对于序列化的源码是 不一样的。这里我按照我自己电脑上的版本为准
C:Program FilesJavajdk1.8.0_261bin>java -version
java version "1.8.0_261"
Java(TM) SE Runtime Environment (build 1.8.0_261-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, mixed mode)
我们可以看到readObject类的实现为:
代码语言:javascript复制 public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
}
/**
* Reads a String and only a string.
*
* @return the String read
* @throws EOFException If end of file is reached.
* @throws IOException If other I/O error has occurred.
*/
继续往下走,见readObject(Object.class)
代码语言:javascript复制private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, 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();
}
}
}
在这里,我们可以看到返回的Object.class来自
代码语言:javascript复制Object obj = readObject0(type, false);
这行代码,也即readObject0。我们继续查看readObject0内部源码。 我们重点看下面这段。
代码语言:javascript复制case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
这里有个checkResolve查询解析,而指向了readOrdinaryObject 读取二进制对象。我们继续往readOrdinaryObject往里看。 然后,我们就可以发现在这个方法里面的一段:
代码语言:javascript复制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);
}
在这里,isInstantiable() 用于检测类是否可以被初始化。如果可以被初始化,就使用newInstance()构建一个对象。如果不可以被初始化,就返回null。 isInstantiable()是如何检测的呢?
代码语言:javascript复制boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
这里是通过cons表示构造方法,如果构造方法为空,返回false,不会进行初始化。显然,我们前面的SeriableSingleton是有构造方法的。(尽管是私有,但这对jvm来说这就跟脱裤子放屁一样,没啥关系)所以这里的cons就不会为空,也就是说 会返回一个true,从而调用desc.newInstance()来创建对象。
因此,就会破坏单例模式。
那么如何去解决这个问题呢? 我们回到readOrdinaryObject这个方法里面,可以发现,在执行newInstance之后,会继续执行以下代码:
代码语言:javascript复制if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
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);
}
}
在这里会调用hasReadResolveMethod()查询是否有ReadResolve方法。如果有,则会执行desc.invokeReadResolve(obj)。如果没有,就不调用了,直接使用之前已经初始化的代码。
当然,可能会有同学好奇,hasReadResolveMethod()这个方法是咋样去检测有没有ReadResolve方法的? 我们点开hasReadResolveMethod()
代码语言:javascript复制boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
通过readResolveMethod是否为null来判断,而readResolveMethod的值则来源于同一个类里面的:
代码语言:javascript复制private Method readResolveMethod;
/** local class descriptor for represented class (may point to self) */
代码语言:javascript复制readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
通过反射来查找是否存在readResolve方法,存在则会返回true。 只要在这个变量readResolveMethod为true,就会执行invokeReadResolve方法,使用该方法得到的值来代替。 而invokeReadResolve方法:
代码语言:javascript复制Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
然后就好理解了,在这个方法里面通过invoke来使用已经实现的ReadResolve方法。 即:
代码语言:javascript复制return readResolveMethod.invoke(obj, (Object[]) null);
所以,可知,其实即使你重写了ReadResolve方法,但实际上还是创建了类两次,只不过是后来创建的覆盖了之前创建的对象。而之前反序列化出来的对象则会被GC回收。
你以为自己有多厉害,其实JVM早已看透了一切。
最好的单例其实,还是注册式单例。
注册式单例
注册式单例,是指将每一个实例都缓存到统一的容器中,使用唯一标识符获取实例。
枚举式单例
代码语言:javascript复制/**
* @Description 枚举式单例
* @Author Sitr
* @Date 2021/1/2 1:30
* @Version 1.0
**/
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;
}
}
枚举式单例很好地解决了反序列化的时候破坏单例的问题。 (实际上,枚举式单例是编译为饿汉式单例的) 那么问题又双来了,为啥枚举式单例就可以解决反序列化的问题呢? 继续点那个readObject,进入ObjectInputStream.java。这个我们就可以继续刚才反序列化的那个类ObjectInputStream.java里面提到的
代码语言:javascript复制case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
查看readEnum方法。可以看到是通过Enum.valueOf来确定枚举对象。
代码语言:javascript复制if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " name " does not exist in "
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
可以注意到正是这一行仅枚举一次。因为这里生成的枚举都是通过类名和枚举的名字来确定枚举值。通过这两个值来注册枚举值。既然值是确定的,单一的,那么生成的对象也必然是单例的。
代码语言:javascript复制Enum<?> en = Enum.valueOf((Class)cl, name);
但是,它到底是咋样通过枚举不会被反序列化破坏单例的呢?
我们继续。 其实是由于在jdk层面就帮我们保证了不会通过反射、序列化来创建枚举。 打开反射的类java.lang.reflect.Constructor.java,在里面的newInstance方法里,有这么一行:
代码语言:javascript复制if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
这行意思很明显,如果发现Modifier的枚举不为0,也即是一个枚举的类型,那么就抛出一个异常IllegalArgumentException("Cannot reflectively create enum objects")。
容器式单例
Spring所采用的单例模式。
代码语言:javascript复制/**
* @Description 容器式单例
* @Author Sitr
* @Date 2021/1/2 13:13
* @Version 1.0
**/
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(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;
}
return ioc.get(className);
}
}
}
容器式单例最大的优点就是对象方便管理,是懒加载的一种,如果不加上synchronize会存在线程安全问题。而加上synchronize之后,解决线程安全问题,但是会影响性能。而Spring正是采用这种方式实现单例模式。
ThreadLocal的单例模式
线程内部的线程安全的单例模式,保证线程内部的全局唯一,天生线程安全。但是在切换线程之后就不再是线程安全了,是个伪线程安全。
代码语言:javascript复制/**
* @Description ThreadLocal实现的单例模式
* @Author Sitr
* @Date 2021/1/2 13:35
* @Version 1.0
**/
public class ThreadLocalSingleton {
private ThreadLocalSingleton(){}
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
ThreadLocal单例模式是如何保证线程内全局唯一的? 我们查看java.lang.ThreadLocal.java的源码。
代码语言:javascript复制public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
我们可以看到 这里的get是通过ThreadLocalMap来获取的,我们继续查看ThreadLocalMap的get部分。
代码语言:javascript复制 public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到是使用map.set来设置的,通过this和value来注册单例对象。因此这也是一种注册类的单例。 这种单例模式常用于ORM框架配置数据源,实现ThreadLocal来实现多数据源动态切换。
单例模式的优点
单例模式在内存中只有一个实例,可以有效减少内存的开销,避免对资源的多重占用。同时设置了全局访问点,可以严格控制访问。
单例模式的缺点
缺点也很明显,不符合开闭原则。没有接口,扩展起来会比较困难。如果要扩展单例模式,只能通过修改代码来扩展。