一、单例模式介绍
1、定义与类型
定义:保证一个类仅有一个实例,并提供一个全局访问点
类型:创建型
2、适用场景
想确保任何情况下都绝对只有一个实例
3、优点
在内存里只有一个实例,减少了内存开销
可以避免对资源的多重占用
设置全局访问点,严格控制访问
4、缺点
没有接口,扩展困难
5、重点
私有构造器:禁止从单例类外部构造对象
线程安全
延迟加载:使用时才创建
序列化和反序列化安全:序列化和反序列化会对单例模式进行破坏
反射:防御反射攻击
二、代码示例
1、懒汉式及多线程
注重延迟加载:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
但是存在线程安全问题,所以可以增加synchronized:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
2、Double Check双重检查
但是 synchronized 对性能存在影响,所以可以使用Double Check双重检查:
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
其中
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这一句代码包含三个步骤:
1.分配内存给这个对象
2.初始化对象
3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
在java语言规范中 允许在单线程内,不会改变单线程执行结果的重排序。
所以 2和3步可能会存在指令重排序,在单线程中,不会影响执行结果:
此时线程1访问对象,但是对象在线程0中还没有初始化完成,可能就会报异常。
解决方案:
方案1、不允许2、3步骤重排序:
使用volatile关键字:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
使用了volatile后,所有线程都可以看到共享内存的最新状态,保证了内存的可见性。用volatile关键字修饰的变量,在进行写操作时,会多出一些汇编代码,将当前处理器缓存行的数据写回到内存,其中涉及到缓存一致性协议。
方案2、允许重排序,但不允许其他线程看到这个重排序,即静态内部类
3、静态内部类
基于类初始化的延迟加载解决方案
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
原理:存在Class对象的初始化锁,并且非构造线程,是看不到指令重排序的。
线程0初始化Class,线程1看不到初始化过程。所以静态内部类这种方法的核心在于InnerClass这个类的对象初始化锁
补充:类在以下几种情况下被初始化,1.实例被创建(new、反射、序列化),2.静态方法被调用,3.静态成员被赋值,4.非常量静态成员被使用,5.顶级类中有嵌套的断言语句,6.子类被初始化
4、饿汉式
最简单的写法:
public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
优点是类加载的时候就完成了初始化,避免了线程同步的问题
缺点是没有延迟加载的效果,可能造成累成内存浪费
饿汉与懒汉之间最大的区别就是延迟加载:饿汉式很饿,一上来就想吃东西,马上就把对象创建好了;而懒汉式非常懒,不用它的时候都不会创建这个对象。
5、序列化破坏单例模式
以下序列化和反序列化 将会破坏单例模式:
// 实现序列化接口
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
测试类:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
// 将会输出两个不同的内存地址
System.out.println(instance);
System.out.println(newInstance);
}
}
解决方法:反序列化是通过反射生成对象,在这个过程中,会判断是否存在并调用readResolve方法
所以可通过增加readResolve方法防止反序列化:
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object reaResolve(){
// 返回单例对象
return hungrySingleton;
}
}
但是在这个过程中,仍然被创建了新的对象,只是最后没有返回而已。
6、反射攻击
public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class hungrySingletonClass = HungrySingleton.class;
Constructor declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
// 输出false
System.out.println(instance == newInstance);
}
}
对于饿汉式单例、静态内部类单例,因为是在类初始化时就创建了对象,所以可在构造器中进行反射防御:
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
// 反射防御,当类在初始化时,单例就会被初始化,为第一次调用;反射时,为第二次调用就会报错
if(hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
// 返回单例对象
return hungrySingleton;
}
}
而对于不是在类初始化时创建对象的单例模式,则无法防御反射攻击,例如懒汉式单例模式:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
if(lazySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
因为在被反射攻击的时候,单例可能还没有被创建,所以会产生不同实例,测试类:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 反射攻击
Class lazySingletonClass = LazySingleton.class;
Constructor declaredConstructor = lazySingletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
// 先反射
LazySingleton newInstance = declaredConstructor.newInstance();
// 后取单例,因为类中的实例仍为null,所以构造器的判断没有起到想要的作用
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
可以增加信号量进行控制:
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();
}
return lazySingleton;
}
}
但是信号量仍然可以被修改,以达到反射攻击:
public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, NoSuchFieldException, InvocationTargetException {
Class objectClass = LazySingleton.class;
Constructor c = objectClass.getDeclaredConstructor();
c.setAccessible(true);
LazySingleton o1 = LazySingleton.getInstance();
Field flag = o1.getClass().getDeclaredField("flag");
flag.setAccessible(true);
// 修改信号量
flag.set(o1,true);
LazySingleton o2 = (LazySingleton) c.newInstance();
System.out.println(o1);
System.out.println(o2);
// 返回false
System.out.println(o1==o2);
}
}
7、Enum枚举单例
枚举类型天然的可序列化机制,能够强有力得保证不会多次实例化的情况。即使在复杂的序列化或者反射攻击下,枚举模式都没有问题。
public enum EnumInstance {
INSTANCE{
protected void printTest(){
System.out.println("Geely Print Test");
}
};
protected abstract void printTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
在ObjectInputStream中,对于枚举类型,是通过枚举类直接获得唯一的枚举常量,没有创建新的对象,维护了枚举的单例属性:
而对于反射,在调用
objectClass.getDeclaredConstructor();
时会直接报错:
java.lang.NoSuchMethodException
原因在于Enum本身就只有一个构造器:
而如果调用
Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumInstance instance = (EnumInstance) constructor.newInstance("11",22);
也会直接报错:java.lang.IllegalArgumentException: Cannot reflectively create enum objects
如果通过jad反编译枚举类,可以看到:1.class类为final的;2.构造器为private;3.声明的枚举对象是static和final的;4.枚举对象在static代码块中实例化
所以枚举单例是最安全的单例模式
8、容器单例
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map singletonMap = new HashMap();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key,instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
容器单例与享元模式相似
优点:统一管理,节省资源,相当于缓存
缺点:存在线程安全问题
9、ThreadLocal线程单例
public class ThreadLocalInstance {
private static final ThreadLocal threadLocalInstanceThreadLocal
= new ThreadLocal(){
@Override
protected ThreadLocalInstance initialValue() {
return new ThreadLocalInstance();
}
};
private ThreadLocalInstance(){
}
public static ThreadLocalInstance getInstance(){
return threadLocalInstanceThreadLocal.get();
}
}
这个单例 并不能保证整个应用全局唯一,但能保存线程唯一。
ThreadLocal会为每一个线程提供一个变量副本,本身是基于ThreaLocalMap实现的,维持了线程间的隔离。原理是以空间换时间的方式,会创建很多对象,在一个线程里会创建唯一的一个对象。在多线程访问的时候,彼此不会相互影响。