JAVA架构师之路二:设计模式之工厂模式
日头没有辜负我们,我们也切莫辜负日头。——沈从文
代码世界中也存在以下顺口溜:
我单身,我骄傲,我为国家省套套。
我单身,我自豪,我为祖国省橡胶。
单例模式虽然简单,但真正懂的内行的人并不多,今天挑战全网最全的经典设计模式之单例模式。
定义
确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
隐藏其构造方法
属于创建型设计模式
适用场景
确保任何情况下都绝对只有一个实例
ServletContext、ServletConfig、ApplicationContext、DBPool
定义
系统初始化的时候就加载,不管有没有用到这个单例。
优点
执行效率高,性能高,没有任何的锁
缺点
某些情况下,可能会造成内存浪费
能够被反射破坏
代码
public class HungrySingleton {
private static final HungrySingleton singleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance() {
return singleton;
}
}
定义
系统初始化的时候不创建实例,只有用到的时候才创建实例。
优点
节省了内存
缺点
synchronized造成性能低下
能够被反射破坏
代码
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton(){}
/**
* 版本1
* @return
*/
public static synchronized LazySingleton getInstance() {
if (null == singleton) {
singleton = new LazySingleton();
}
return singleton;
}
}
代码
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton(){}
/**
* 版本2 相比版本1优化一点点
* @return
*/
public static LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (null == singleton) {
singleton = new LazySingleton();
}
}
return singleton;
}
}
陷阱案例
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton(){}
/**
* 版本3 双重判断
* @return
*/
public static LazySingleton getInstance() {
if (null == singleton) {
synchronized (LazySingleton.class) {
if (null == singleton) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
版本3看起来相比版本2优化了不少,但其实这种双重判断在生产环境有一个极大的漏洞陷阱,就是指令重排序,有需要了解的可以在评论区留言。解决方案也很简单,就是 volatile 关键字。它可以限制指令重排序。
正确写法
public class LazySingleton {
private volatile static LazySingleton singleton = null;
private LazySingleton(){}
/**
* 版本3 双重判断
* @return
*/
public static LazySingleton getInstance() {
if (null == singleton) {
synchronized (LazySingleton.class) {
if (null == singleton) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
双重判断的优点:性能高了,线程安全了。
缺点:代码可读性极差,不够优雅。
利用JVM加载类的顺序,静态内部类,只有用到的时候外部类用到静态内部类的时候才会加载。
优点
写法优雅,利用了Java的语法特点,性能高,避免了内存浪费
缺点
能够被反射破坏
public class LazyStaticInnerSingleton {
private LazyStaticInnerSingleton(){}
public static LazyStaticInnerSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyStaticInnerSingleton INSTANCE = new LazyStaticInnerSingleton();
}
}
这种写法本来应该够优雅,够完美,但是却有一个缺点是能被反射破坏,文章最后我会证明什么是能被反射破坏。那有没有写法能让这个单例不会被反射破坏?答案是有的!
public class LazyStaticInnerSingleton {
private LazyStaticInnerSingleton(){
if (null != LazyHolder.INSTANCE) {
throw new RuntimeException("不允许非法访问!");
}
}
public static LazyStaticInnerSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyStaticInnerSingleton INSTANCE = new LazyStaticInnerSingleton();
}
}
这种写法就解决了被反射破坏的问题。但是看起来不是那么的优雅。
定义
将每一个实例都缓存到统一的容器中,使用唯一标识获取实例。
优点
写法优雅,线程安全
缺点
和饿汉式类似,大量使用会造成内存浪费,根本原因在于枚举本身的特点。
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;
}
}
使用方法
public class Test {
public static void main(String[] args) {
EnumSingleton singleton = EnumSingleton.getInstance();
singleton.setData(new Object());
singleton.getData();
}
}
Spring设计者结合枚举式单例的写法和特点,写了一种自己的IOC 容器注册式单例。
public class ContainerSingleton {
private ContainerSingleton() {}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getInstance(String className) {
if (!ioc.containsKey(className)) {
Object instance = null;
try {
instance = Class.forName(className).newInstance();
} catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}
ThreadLocal单例肯定会用到ThreadLocal,根据ThreadLocal本身的特点,即同一线程内数据可见,那么这种单例就有本身的局限性,使用的很少。我曾经在token登陆的时候用到过。即前端会传一个token到后端,token能解析出登陆用户的信息。把解析后的信息放在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();
}
}
public class Test1 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = HungrySingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);//会输出false
}
}
解决方案就是:构造方法抛异常。
if (null != LazyHolder.INSTANCE) {
throw new RuntimeException("不允许非法访问!");
}
首先你必须知道什么是序列化。序列化就是JVM内存中的对象,序列化到磁盘文件,再读取到内存,不同进程的数据交互需要序列化才能传输。
以上的所有单例模式,解决了各种各样的问题,但都存在同一个问题,就是都会被序列化破坏。意思就是:系统中的单例,被序列化到磁盘,然后再加载到内存,那么这序列化前后两个单例,并不是同一个单例。这就是序列化破坏单例。
解决方案:在单例中加入以下方法:
private Object readResolve() {
// instead of the object we're on,
// return the class variable INSTANCE
return INSTANCE;
}
鉴于评论区有问为什么加入readResolve()
方法就能解决序列化破坏单例的问题,现在我详细讲一下:
public User deapClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (User)ois.readObject();
}
deapClone()
方法就是序列化的过程。读内存,写内存。看最后 ois.readObject()
,我们来看看readObject()
方法干了啥?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();
}
}
}
方法里面有个Object obj = readObject0(false);
的readObject0()
方法,我们再来看看这个方法做了啥?
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
方法太长 ,我粘贴主要的放啊case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));
如果是转化成对象,要执行有个checkResolve
方法,这里是不是和readResolve()
优点关系了?都有resolve
了,再看看readOrdinaryObject(unshared)
做了啥:
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);
}
}
return obj;
desc.hasReadResolveMethod())
检测了有没有readResolve()
方法,如果有,利用反射调用这个方法,这个方法返回值是单例这个对象,最后或者对象赋值给obj,返回。重写完成。看到这儿应该都懂了加入readResolve()
方法就能解决这个序列化破坏单例这个问题了吧!
感谢您阅读本文,如果您觉得文章写的对您有用的话,请您点击上面的“关注”,点个赞,这样您就可以持续收到《JAVA架构师之路》的最新文章了。文章内容属于自己的一点点心得,难免有不对的地方,欢迎在下方评论区探讨,你们的关注是我创作优质文章的动力。
JAVA架构师之路四:设计模式之原型模式