双重校验锁实现单例模式
为什么要用双重校验锁实现单例模式?
单例实现有饿汉模式与懒汉模式,懒汉模式能够延迟加载,使用较多,但是懒汉模式在多线程下会出现问题,即有可能会产生多个实例。
饿汉模式
public class HungrySingleton {
/**
* 定义一个变量存储实例,在饿汉模式中直接实例化
*/
private static HungrySingleton uniqueInstance = new HungrySingleton();
/**
* 私有化构造方法
*/
private HungrySingleton(){
}
/**
* 提供一个方法为客户端提供实例
*/
public static HungrySingleton getUniqueInstance(){
return uniqueInstance;
}
}
饿汉模式在类加载的时候就完成了实例化,避免了多线程的同步问题。
缺点是:因为类加载时就实例化了,没有达到Lazy Loading (懒加载) 的效果,如果该实例没被使用,内存就浪费了。
懒汉模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
//懒汉式 -- 其特点是延迟加载,即当需要用到此单一实例的时候,才去初始化此单一实例
public synchronized static Singleton getUniqueInstance(){
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
懒汉模式只有在方法第一次被访问时才会实例化,达到了懒加载的效果。
但是这种写法有个致命的问题,就是多线程的安全问题。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果,所以这种写法不可采用。
双重校验锁
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
if (uniqueInstance == null){
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
可以看到进行了两次校验
-
第一次判断是否为 null:
第一次判断是在Synchronized同步代码块外,理由是单例模式只会创建一个实例,并通过 getUniqueInstance 方法返回 Singleton 对象,所以如果已经创建了 Singleton 对象,就不用进入同步代码块,不用竞争锁,直接返回前面创建的实例即可,这样大大提升效率。 -
第二次判断 uniqueInstance 是否为 null:
第二次判断原因是为了保证同步;假若线程A通过了第一次判断,进入了同步代码块,但是还未执行,线程B就进来了(线程B获得CPU时间片),线程B也通过了第一次判断(线程A并未创建实例,所以B通过了第一次判断),准备进入同步代码块,假若这个时候不判断,就会存在这种情况:线程B创建了实例,此时恰好A也获得执行时间片,如果不加以判断,那么线程A也会创建一个实例,就会造成多实例的情况。所以,为了满足单例模式的要求,双重校验是必不可少的。
声明变量时为什么要用volatile关键字进行修饰?
private volatile static Singleton uniqueInstance;
volatile关键字可以防止JVM指令重排优化,使用了volatile关键字可用来保证其线程间的可见性和有序性
因为 uniqueInstance = new Singleton(); 这句话可以分为三步:
1. 为 uniqueInstance 分配内存空间;
2. 初始化 uniqueInstance ;
3. 将 uniqueInstance 指向分配的内存空间。
但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得
一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton,
但是此时 的 singleton 还没有被初始化。
使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。
volatile关键字的第二个作用,保证变量在多线程运行时的可见性:
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,
线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,
而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,就需要把变量声明为 volatile,
这就告诉 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
静态内部类实现单例模式
/**
很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,
调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,
并且这个过程也是线程安全的。
*/
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
枚举单例模式
以上方式是在不考虑反射机制和序列化机制的情况下实现的单例模式,但是如果考虑了反射,则上面的单例就无法做到单例类只能有一个实例这种说法了。事实上,通过Java反射机制是能够实例化构造方法为private的类的。这也就是我们现在需要引入的枚举单例模式。
public enum SingletonEnum {
INSTANCE;
public SingletonEnum getInstance(){
return INSTANCE;
}
}
以双重检索的单例模式为例子,利用反射创建出新的实例
public class TestSingleton {
public static void main(String[] args) throws Exception {
Singleton s1=Singleton.getUniqueInstance();
Singleton s2=Singleton.getUniqueInstance();
Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s3=constructor.newInstance();
System.out.println("实例一"+s1+"\n"+"实例二"+s2+"\n"+"实例三(反射)"+s3);
System.out.println("正常情况下,实例化两个实例是否相同:"+(s1==s2));
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s1==s3));
}
}
result:
实例一com.xy.springcloudeurekaclient.controller.Singleton@4b9af9a9
实例二com.xy.springcloudeurekaclient.controller.Singleton@4b9af9a9
实例三(反射)com.xy.springcloudeurekaclient.controller.Singleton@5387f9e0
正常情况下,实例化两个实例是否相同:true
通过反射攻击单例模式情况下,实例化两个实例是否相同:false
可以看到通过双重检索是无法避免反射的攻击的
检测枚举单例模式
public class TestSingleton {
public static void main(String[] args) throws Exception {
SingletonEnum singleton1=SingletonEnum.INSTANCE;
SingletonEnum singleton2=SingletonEnum.INSTANCE;
System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
System.out.println("通过反射实例化实例...");
Constructor<SingletonEnum> constructor= null;
constructor = SingletonEnum.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonEnum singleton3= null;
singleton3 = constructor.newInstance();
System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
}
}
result:
正常情况下,实例化两个实例是否相同:true
通过反射实例化实例...
Exception in thread "main" java.lang.NoSuchMethodException: com.xy.springcloudeurekaclient.controller.SingletonEnum.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.xy.springcloudeurekaclient.controller.TestSingleton.main(TestSingleton.java:28)
结果会报Exception in thread "main" java.lang.NoSuchMethodException。
出现这个异常的原因是因为EnumSingleton.class.getDeclaredConstructors()
获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,而且在反射在通过newInstance创建对象时,会检查该类是否用ENUM修饰,如果是则抛出异常,反射失败。所以枚举是不怕反射攻击的。
newInstance源码
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, 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(initargs);
return inst;
}
本文地址:https://blog.csdn.net/weixin_42214038/article/details/109577810
上一篇: Java Redis