设计模式里单例模式是最面试官最喜欢问的问题,下面我将从是否懒加载(懒汉,饿汉),是否线程安全,能否防止反射构建三个角度来阐述单例模式的几种写法,并给出推荐的两种写法。
懒汉+线程不安全
//1.懒汉+线程不安全
public class Singleton {
//私有构造方法
private Singleton() {}
//单例对象
private static Singleton instance = null;
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
单例的私有构造方法禁止通过构造方法新建对象,Singleton signleton = New Singleton()
会编译报错,只能通过静态工厂方法获取对象。当多个线程同时访问getInstance()
方法,存在竞争,有可能产生多个实例,因此是线程不安全的。
这里简单介绍一下“懒汉模式”,“饿汉模式”的字面含义。
在该版本中,该单例对象是静态变量,该单例类初始化时,赋值为null
,是懒加载,通常称为懒汉模式。如果该静态变量,类初始化时,赋值为new Singleton()
,不是懒加载,通常称为饿汉模式。
懒汉+线程安全 改造1
//2.懒汉式,线程安全
public class Singleton {
private Singleton() {}
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
针对上述线程不安全,最简单的方法就是给getInstance()
方法加synchronized
关键字,对其方法进行加锁,这样能够保证线程安全。给静态方法加锁,锁的是类本身,多线程情况下存在严重的性能问题。
懒汉+线程安全 改造2
// 3.懒汉式,线程安全(性能改进)
public class Singleton {
private Singleton() {}
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
针对上述多线程性能问题,做了两个改进,1)这里对singleton
变量使用volatile
关键字进行修饰,volatile
能够禁止JVM的指令重排序,同时一个线程对singleton
变量修改其它线程能立即看到。2)将synchronized
关键字从getInstance()
方法上放到方法内部,只锁方法里if
条件下的代码块,大大降低了多线程情况下的竞争。
饿汉+线程安全
//4.饿汉+线程安全
pulic class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getSingleton() {
return singleton;
}
}
基于classloader避免多线程同步问题,因此是线程安全的。类装载的时候就加载,运行时快,缺点是没有实现懒加载。
懒汉+线程安全(静态内部类实现)
//5.静态内部类
public class Singleton {
private Singleton() {}
private static SingletonHolder {
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
针对上述没有实现懒加载问题,这里使用静态内部类,拥有4的全部优点,并且实现了懒加载。推荐大家使用(不能防止反射构建多个实例)。
懒汉+线程安全(枚举实现)
//6.懒汉+线程安全(枚举实现)
public enum SingletonEnum {
INSTANCE;
}
代码足够简洁,《Effective Java》作者推荐使用枚举实现单例模式。下面我们来分析一下代码,枚举类里有一个隐式的私有构造方法private Singleton () {}
,并且JVM 会阻止反射获取枚举类的私有构造方法。
关于反射攻击,这里补充介绍一下。
import org.testng.Assert;
import org.testng.annotations.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Created by hahazei on 2020/3/16.
*/
public class SingleTest {
@Test
public void test_singleton1() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class objClass = Singleton.class;
//获取类的构造器
Constructor constructor = objClass.getDeclaredConstructor();
//把构造器私有权限放开
constructor.setAccessible(true);
//正常的获取实例方式
Singleton singleton = Singleton.getInstance();
//反射创建实例
Singleton reflectSingleton = (Singleton) constructor.newInstance();
Assert.assertEquals(singleton, reflectSingleton);//false
}
@Test
public void test_singleton_enum() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Class objClass = SingletonEnum.class;
//获取类的构造器
Constructor constructor = objClass.getDeclaredConstructor(); //报错java.lang.NoSuchMethodException: SingletonEnum.<init>()
//把构造器私有权限放开
constructor.setAccessible(true);
//正常的获取实例方式
SingletonEnum singletonEnum = SingletonEnum.INSTANCE;
//反射创建实例
SingletonEnum reflectSingletonEnum = (SingletonEnum) constructor.newInstance();
Assert.assertEquals(singletonEnum, reflectSingletonEnum);
}
}
test_singleton1()
方法中的Singleton
类是本文中前5中单例,都会报不相等,所以前5中不能防止反射构建多个实例
test_singleton_enum()
方法中获取类的构造器Constructor constructor = objClass.getDeclaredConstructor();
这一行就报错,无法通过放射来构建实例,因此能够防止放射攻击。推荐使用。