单例?
一个类只有一个对象实例,并对外提供一个获取实例的方法。一句话就能概括单例这个设计模式,真的只有这么简单吗?
单例模式分为两种方案,饿汉式和懒汉式
一、饿汉式
- 私有的构造方法
- 只要当类加载的时候就初始化单例对象
public class Hungry {
private static Hungry hungry = new Hungry();
private Hungry(){
}
public static Hungry newInstance(){
return hungry;
}
}
由于变量由static
修饰,所以该对象由多个线程共享,并且在类加载阶段只初始化一次。
二、懒汉式
- 私有的构造方法
- 当需要使用实例对象时就创建
public class LazyMan {
private static volatile LazyMan lazyMan = null;
private LazyMan() {
}
public static LazyMan newInstance() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
}
单线程情况下上面的代码不存在安全问题,但是放在多线程并发情况下呢?由于饿汉式是在类加载时初始化的对象,所以它在多线程情况下是线程安全的,但是懒汉式是对外提供方法创建对象,所以在并发情况下存在多线程同时操作共享资源的情景,下面我们假设一个场景:
- 线程A调用
newInstance()
初始化对象 - 线程A判空后进入
if
代码块,此时还没有完成实例化过程 - 线程B进来调用
newInstance()
方法,同时判空后进入if代码块 - 线程A执行
new LazyMan()
- 线程B执行
new LazyMan()
在这种情况下new
了两次对象,破坏了单例
在多线程情况下如何保证线程安全,不用说,第一反应肯定是加锁,下面我们来加锁:
public class LazyMan {
private static volatile LazyMan lazyMan = null;
private LazyMan() {
}
public static synchronized LazyMan newInstance() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
}
跑10个线程
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
很明显现在懒汉式是线程安全的,但是上文的synchronized
是直接锁的方法,这种方案锁的粒度太大,如果方法体中有大量的业务代码不需要同步,方法的性能,效率会非常低下,所以下面我们用同步代码块来降低锁的粒度
public class LazyMan {
private static volatile LazyMan lazyMan = null;
private LazyMan() {
}
public static LazyMan newInstance() {
if (lazyMan == null) {
synchronized(LazyMan.class) {
lazyMan = new LazyMan();
}
}
return lazyMan;
}
}
那么思考下这种方法在并发条件下是线程安全的吗?
答案不安全!
思考下面的场景:
1、线程A进入newInstance()
,判断为空,拿到锁
2、线程B进入newInstance()
,判断为空,发现锁被占有,等待
3、线程A new
完对象后释放掉锁
4、线程B往下执行拿到锁,new
对象
此时也是new了两个对象,也破坏了单例
测试:
同样跑10个线程
site.kexing.lock.LazyMan@37bafe8f
site.kexing.lock.LazyMan@1cd6fdf0
site.kexing.lock.LazyMan@27566eaf
site.kexing.lock.LazyMan@56b19245
site.kexing.lock.LazyMan@6b92174a
site.kexing.lock.LazyMan@5c98f75b
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@58ae7869
site.kexing.lock.LazyMan@4686939d
site.kexing.lock.LazyMan@1646a158
DCL懒汉式(双重检测锁)
public class LazyMan {
private static volatile LazyMan lazyMan = null;
private LazyMan() {
}
public static LazyMan newInstance() {
if (lazyMan == null) {
synchronized(LazyMan.class) {
if(lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
为什么对象要加volatile
?
首先聊聊volatile
有哪些特性:
- 可见性
- 非原子性
- 禁止指令重排
我们来看看new LazyMan()
底层到底做了什么?
- 为
lazyMan
分配内存空间 - 调用构造方法初始化
lazyMan
- 将
lazyMan
指向分配的内存空间,此时的lazyMan
才不为null
CPU为了提高程序编译后指令的效率,往往会将指令重排,达到CPU认为最优的方案,上面的123可能会被重排为132
如果这个操作底层的指令被重排为132,思考下面场景:
- 线程A进入双重检测锁,执行到指令重排后的指令3(注意此时
lazyMan
已经指向了内存空间,不为null
) - 线程B进来,发现
lazyMan
不为null
,直接return
- 此时
return
的对象还没有经过指令2构造初始化,也就是一块没有填充值的内存空间
而volatile
关键字可以避免指令重排,始终按序执行
两层检测并加锁可有效避免线程安全问题,第一层判断主要是为了减少线程争夺资源,如果不为空后则不会去抢夺锁,降低CPU压力
真的安全吗?
别忘了Java
中有一种技术叫做反射
通过反射破坏单例
public class LazyMan {
private static volatile LazyMan lazyMan = null;
private LazyMan() {
}
public static LazyMan newInstance() {
if (lazyMan == null) {
synchronized(LazyMan.class) {
if(lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
//正常调用方法创建
LazyMan lazyMan1 = LazyMan.newInstance();
//拿到class
Class<LazyMan> lazyManClass = LazyMan.class;
//拿到构造器
Constructor<LazyMan> declaredConstructor = lazyManClass.getDeclaredConstructor(null);
//反射构造器创建
LazyMan lazyMan = declaredConstructor.newInstance();
System.out.println(lazyMan);
System.out.println(lazyMan1);
}
}
编译运行:
site.kexing.single.LazyMan@b4c966a
site.kexing.single.LazyMan@2f4d3709
通过反射拿到构造器创建了两个实例对象
解决方案,信号量法:
这种方法不论是通过反射还是调用提供的方法只能构造出一个实例!
public class LazyMan {
private static Boolean kexing = false;
private static volatile LazyMan lazyMan = null;
private LazyMan() {
synchronized (LazyMan.class){
if(kexing == false){
kexing = true;
}else {
throw new RuntimeException("请不要试图使用反射破坏单例");
}
}
}
//双重检测锁
public static LazyMan newInstance() {
if (lazyMan == null) {
synchronized(LazyMan.class) {
if(lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
只要是第一次创建实例,信号量kexing
置为true
,随后进来的都会走else
抛出异常
同样的,执行上面同样的main
方法进行测试:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
at site.kexing.single.LazyMan.main(LazyMan.java:54)
Caused by: java.lang.RuntimeException: 请不要试图使用反射破坏单例
at site.kexing.single.LazyMan.<init>(LazyMan.java:17)
... 6 more
很nice,有效的阻止了重复创建实例
真的安全吗??
如果狂徒张三通过不正当手段知道了程序是通过这个方案保证单例,那么这个方案也会变得不堪一击,要知道,一个类在反射面前是光着身子的!
如何破解上文的信号量法?
很简单,拿到字节码class对象后通过getDeclaredFields()
拿到类的变量域对象数组,遍历一下,field.getName()
拿到成员变量名,field.get(field.getName())
拿到成员变量的值,通过set(Object obj, Object value)
修改,始终保证信号量为true,狂徒张三:就这?
枚举单例(终极方案)
public enum EnumLazyMan{
INSTANCE;
public static EnumLazyMan newInstance() {
return INSTANCE;
}
}
狂徒张三:反射试试?
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:493)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
at site.kexing.single.EnumLazyMan.main(EnumLazyMan.java:29)
Cannot reflectively create enum objects
张三:…
我们点进源码看看为什么会这样
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
Class<?> caller = override ? null : Reflection.getCallerClass();
return newInstanceWithCaller(initargs, !override, caller);
}
/* package-private */
T newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller)
throws InstantiationException, IllegalAccessException,
InvocationTargetException
{
if (checkAccess)
checkAccess(caller, clazz, clazz, modifiers);
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(args);
return inst;
}
抽出重点:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
在Java圣经《Effective Java》中,Joshua Bloch这么说:
A single-element enum type is often the best way to implement a singleton.
枚举是一般情况下最好的Java实现单例的方法
It is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks.
枚举单例可以有效防御两种破坏单例(使单例产生多个实例)的行为:反射攻击与序列化攻击