单例模式
定义:保证一个类仅有一个实例,并提供一个全局访问点
类型:创建型
使用场景:与定义无异,想在任何时候情况下都只有一个实例。当然,如果是在单机模式下肯定不用过多讨论。一般都是集群模式下,
比如一些共享的计数器,连接池,线程池等。
优点:
- 内存开销少,只有一个实例。也可以避免对资源的多重浪费
- 设置全局访问点,严格的控制了访问。换句话说就是没办法去进行new操作,只能调用方法获取。
缺点
-优点及缺点。严格的控制访问导致扩展性差,基本只能靠改代码进行修改。
单例模式设计的重点
- 私有构造器
- 线程安全
- 延迟加载
- 序列化与反序列化安全
- 反射 -> 防止反射攻击
1. 懒汉模式
通过上图我们能看到执行看似没什么问题,但是仔细一看的话便能发现它是线程不安全的。因为代码比较简单所以是很难触发的。所以我们需要进行一下多线程debug。
设置之后我们分别对线程进行一下debug,手动模拟会出现问题的可能。
最后果然出现了不同的打印结果。知道了不安全的原因,那么如何解决自然变得很简单,我们只需要在静态方法前面加synchronized关键字即可,但是静态方法加锁就相当于这个类加锁。对于性能自然会不是很高。那么有没有让锁尽量不会起作用,还能延迟加载的方法呢?
自然是有,下面讲解一下双重检查
2. 双重检查模式
public class DoubleCheckLazySingleton {
//private static DoubleCheckLazySingleton lazySingleton = null;
private volatile static DoubleCheckLazySingleton lazySingleton = null;
private DoubleCheckLazySingleton() {
}
public synchronized static DoubleCheckLazySingleton getInstance() {
if (lazySingleton == null) {
synchronized (DoubleCheckLazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new DoubleCheckLazySingleton();
//因为指令重排可能会有一个隐患
//1 - 分配内存給这个对象
//3 - lazySingleton 指向刚分配的内存地址
//2 - 初始化对象
//-----------------------------
//3 - lazySingleton 指向刚分配的内存地址
}
}
}
return lazySingleton;
}
public static void main(String[] args) {
//只是为了快速用而已,实际的话不建议这么创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 2; i++) {
executorService.execute(DoubleCheckLazySingleton::getInstance);
}
executorService.shutdown();
}
}
通过代码可以看到我是把没有加volatile的代码注释掉了。原因下面会进行讲解。
通过图片我们能看到因为指令重排的原因,创建一个对象的指令可能会被重排序。如果出现上图的情况,那么就会导致程序报错。那么我们解决问题的方法无非两种:
1 禁止重排序
2 线程0的重排序,对其他线程不可见。
其实加volatile关键字就是方法 1。volatile通过加入内存屏障和禁止重排序优化来实现可见性。这个应该是线程安全性相关的知识,因为今天主要是说单例模式,所以简单说一下:volatile写操作的时候会将本地内存的共享变量刷新到主内存,而读操作会从主内存中去读共享变量。
3. 静态内部类模式
public class StaticInnerSingleton {
private static class InnerClass{
private static StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance(){
return InnerClass.staticInnerSingleton;
}
//私有构造方法
private StaticInnerSingleton() {
}
}
内部静态类模式就是上面2的解决方式。线程的重排序,对其他线程不可见。
(深入理解java虚拟机 p226)虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确的加锁、同步、如果多个线程去同时初始化一个类,那么只有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程的方法执行完毕。
类似于上图。只有一个线程是可以获取锁的,那么即使线程0去重排序,对于线程1也是不可见的。
4. 饿汉模式
饿汉比较简单就不详细说了。优点线程安全。缺点不是延迟加载,如果不用,会造成一定的开销。
public class HungrySingleton {
private final static HungrySingleton hungrySIngleton = new HungrySingleton();
public HungrySingleton() {
}
public HungrySingleton getHungrySIngleton() {
return hungrySIngleton;
}
}
序列化以及反射对单例模式的影响
因为后续会讲解枚举类型的单例,因为天然特性的原因。所以这里先讲一下序列化以及反射对单例模式的影响
序列化
private final static HungrySingleton hungrySIngleton = new HungrySingleton();
public HungrySingleton() {
}
public static HungrySingleton getHungrySIngleton() {
return hungrySIngleton;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton hungrySingleton = HungrySingleton.getHungrySIngleton();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(hungrySingleton);
File file = new File("singleton");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
HungrySingleton hungrySingletonTwo = (HungrySingleton) objectInputStream.readObject();
System.out.println(hungrySingleton);
System.out.println(hungrySingletonTwo);
System.out.println(hungrySingleton.equals(hungrySingletonTwo));
}
}
//打印结果
com.example.demo.singleton.HungrySingleton@1fb3ebeb
com.example.demo.singleton.HungrySingleton@5010be6
false
我们可以看出来这就违背了我们单例模式的初衷,因为我的得到了不一样的对象。解决方案也很简单我们只需要加一个方法就可以搞定了。
我们可以看到readResolve并不是灰色的,因为我的主题如果这个方法没有被调用的时候显示的是灰色的。那么为什么加一个readResolve就可以了呢,他有事在哪调用的?那接下来我们得看源码才能知道了。
HungrySingleton hungrySingletonTwo = (HungrySingleton) objectInputStream.readObject();
因为源码太多,我就不贴太多图了,主要的地方我再贴图。
点击readObject这个方法 -> 进去可以看到readObject0()这个方法->进去之后发现里面有一个switch 找到TC_OBJECT然后我们进入readOrdinaryObjectreadOrdinaryObject这个方法-> 因为源码太多,我就不贴太多图了,主要的地方我在贴图。
点击readObject这个方法 -> 进去可以看到readObject0()这个方法->进去之后发现里面有一个switch 找到TC_OBJECT然后我们进入readOrdinaryObject这个方法。在里面找到
obj = desc.isInstantiable() ? desc.newInstance() : null;
通过这行代码,我们看到了obj,然后看了一下obj最后会返回,那么就说明obj没啥可看的,我们的重点在于这个判断。点击进入isInstantiable方法
/**
* 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.
*/
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
cons是一个构造器点进去没有什么有效信息,那么只能看上方注解了。如果
serializable/externalizable在运行的时候被实例化就会返回true。
可以看到返回true之后 desc.newInstance()通过反射拿到一个新的对象肯定会和原来不一样。虽然现在知道了我们会新获得一个对象,但是还没有解决我们最初的疑问,所以我们接着往后看。
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);
}
}
通过上面代码我们看到if里面有一个hasReadResolveMethod()方法,看名字我们也猜出来这到底是干啥的。进入if里面之后看到 Object rep = desc.invokeReadResolve(obj);点击进入发现里面是通过反射拿到我们类里面声明的readResolve方法,至此我们也知道了readResolve是在哪被调用了。但是这还有个不好的地方就是每次都会有新的对象被生成,只不过后期调用readResolve方法被替换了而已。
反射攻击
public class HungrySingleton {
private final static HungrySingleton hungrySIngleton = new HungrySingleton();
public HungrySingleton() {
}
public static HungrySingleton getHungrySIngleton() {
return hungrySIngleton;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class jhwclass = HungrySingleton.class;
Constructor constructor = jhwclass.getDeclaredConstructor();
//放开私有权限
constructor.setAccessible(true);
HungrySingleton hungrySingleton = HungrySingleton.getHungrySIngleton();
HungrySingleton hungrySingletonTwo = (HungrySingleton) constructor.newInstance();
System.out.println(hungrySingleton);
System.out.println(hungrySingletonTwo);
System.out.println(hungrySingleton.equals(hungrySingletonTwo));
}
}
打印结果
com.example.demo.singleton.HungrySingleton@13221655
com.example.demo.singleton.HungrySingleton@2f2c9b19
false
在构造方法加上防御代码
静态内部类也可以用上面的方法。原因是两者都是在类加载的时候,实例就会生成。而懒汉加载就不能用了,因为无法确定哪个线程去进行加载,即使加了以一些防御性质的代码也不能保证,例如声明一个变量去当开关,还是可能会被反射进行更改。可以参考一下下面的代码。
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private static boolean flag = true;
private LazySingleton() {
if (flag) {
flag = false;
} else {
throw new RuntimeException("报错了,不能反射");
}
}
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
System.out.println(Thread.currentThread().getName() + "--lazySingleton:" + lazySingleton);
return lazySingleton;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Class jhwclass = LazySingleton.class;
Constructor a = jhwclass.getDeclaredConstructor();
a.setAccessible(true);
LazySingleton lazySingleton = LazySingleton.getInstance();
Field aa = lazySingleton.getClass().getDeclaredField("flag");
aa.setAccessible(true);
aa.set(lazySingleton, true);
LazySingleton lazySingletonTwo = (LazySingleton) a.newInstance();
}
}
5. 枚举模式
public enum EnumInstance {
one;
private String data;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public static EnumInstance getInstance() {
return one;
}
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumInstance enumInstance = EnumInstance.getInstance();
enumInstance.setData("jhw");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(enumInstance);
File file = new File("singleton");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
EnumInstance enumInstanceTwo = (EnumInstance) objectInputStream.readObject();
Class jhwclass = EnumInstance.class;
//Constructor aa = jhwclass.getDeclaredConstructor();
Constructor aaa = jhwclass.getDeclaredConstructor(String.class, int.class);
//aa.setAccessible(true);
aaa.setAccessible(true);
// EnumInstance enumInstanceTrd = (EnumInstance) aa.newInstance();
EnumInstance enumInstanceTrd = (EnumInstance) aaa.newInstance("jj", 1);
System.out.println(enumInstance.getData());
System.out.println(enumInstanceTwo.getData());
System.out.println(enumInstance.getData().equals(enumInstanceTwo.getData()));
}
}
//打印结果
jhw
jhw
true
//反射错误1
Exception in thread "main" java.lang.NoSuchMethodException: com.example.demo.singleton.EnumInstance.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.example.demo.singleton.EnumInstance.main(EnumInstance.java:48)
//反射错误2
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.example.demo.singleton.EnumInstance.main(EnumInstance.java:55)
通过上面的代码我们可以看到,通过反射拿到的也是同一个对象。源码跟上面一样,readEnum方法中->readStrirng方法。因为枚举类里面的名字是唯一的,那么拿到的常量肯定也是唯一的。
而Enum这个类也并没有无参的构造方法并且枚举类还不允许进行反射调用,上面的两个错误打印就是很好的说明。通过一些反编译的工具我们看一下enum,其中内部一些声明比如final,静态块是其优雅实现单例模式的基石。