java设计模式——单例模式

名词解释

保证一个类在任何情况下都绝对只有一个事例,要隐藏所有的构造方法

常见用例

  • servletContext
  • servletConfig
  • ApplicationContext
  • DBPool

写法分类

饿汉式

以空间换时间,不存在线程安全问题

缺点:在类初始化的时候就已经创建了对象,浪费资源

写法1:

public class HungrySinglton {
    private HungrySinglton(){};
    private static HungrySinglton instance = new HungrySinglton();
    public static HungrySinglton getInstance(){
        return instance;
    }
}

写法2:

public class HungryStaticSinglton {
    private HungryStaticSinglton(){};
    private static final HungryStaticSinglton instance;
    static {
        instance = new HungryStaticSinglton();
    }
    public static HungryStaticSinglton getInstance(){
        return instance;
    }
}

懒汉式

以时间换空间

缺点:存在线程安全问题

简单写法

public class SimpleLazySinglton {
    private SimpleLazySinglton(){}
    private static SimpleLazySinglton instance = null;
    public static SimpleLazySinglton getInstance(){
        if(null == instance){
            instance = new SimpleLazySinglton();
        }
        return instance;
    }
}

改进(双重检查锁)

对实例方法添加 synchronized,不要添加到方法上,因为性能低下,可以加到方法体内

用双重检查锁的方式进行控制,这样就可以兼顾性能和线程安全

代码如下:

public class DoubleCheckLockSingleton {
     private volatile static DoubleCheckLockSingleton instance = null;
     private DoubleCheckLockSingleton(){}
     public DoubleCheckLockSingleton getInstance(){
         if(instance == null){
             synchronized (DoubleCheckLockSingleton.class){
                if(instance == null){
                    instance = new DoubleCheckLockSingleton();
                }
             }
         }
         return instance;
     }
}
注意点
volatile

引入volatile 是为了解决 DCL 失效问题

JVM实例化一个对象的时候会经理一下三个步骤
	1. 在堆内存开辟空间
	2. 在堆内存中实例化对象的各个参数
	3. 把对象指向内存空间
由于JVM存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,instance 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的 DCL失效问题
双重检查

第一次检查是为了判断不需要做无谓的创建判断,第二次检查是为了解决多线程下的线程安全问题

静态内部类

内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题,这种形式兼顾饿汉式的内存浪费,也兼顾synchronized 性能问题

public class InnerClassLazySingleton {
    //默认使用 InnerClassLazySingleton 的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private InnerClassLazySingleton(){}
    //每一个关键字都不是多余的
    //static 是为了使单例的空间共享
    //final 保证这个方法不会被重写,重载
    public static final InnerClassLazySingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return InnerCreateClass.lazyInstance;
    }
    //默认不加载
    private static class InnerCreateClass{
        private static final InnerClassLazySingleton lazyInstance
            = new InnerClassLazySingleton();
    }
}

反射破坏单例

public class LazyInnerClassSingletonTest {
    public static void main(String[] args) {
        try{
            Class<?> clazz = InnerClassLazySingleton.class;
            //通过反射拿到私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //强制访问,强吻,不愿意也要吻
            c.setAccessible(true);
            //暴力初始化
            Object o1 = c.newInstance();
            //调用了两次构造方法,相当于new 了两次
            //犯了原则性问题,
            Object o2 = c.newInstance();
            System.out.println(o1 == o2);
            // Object o2 = c.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
解决办法

在构造方法中做一些限制,比如:

private InnerClassLazySingleton(){
    if(InnerCreateClass.lazyInstance != null){
   		throw new RuntimeException("不允许创建多个实例");
    }
}

序列化破坏单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建,就会破坏单例对象的唯一性

可以通过如下代码来测试反序列化对单例的破坏

public class SerializableSingletonTest {
    public static void main(String[] args) {
        InnerClassLazySingleton s1 = null;
        InnerClassLazySingleton s2 =InnerClassLazySingleton.getInstance();
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("InnerClassLazySingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("InnerClassLazySingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (InnerClassLazySingleton)ois.readObject();
            ois.close();
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
解决办法

添加 readResolve 方法,这个方法不属于重写方法,只是 JVM反序列化创建对象时会通过反射去调用这个指定的方法,并将这个方法的返回值作为最终创建对象的值。所以可以在这个方法中再返回一次我们的单例,就能达到反序列化任然得到的是同一个实例对象的目的。

Object readResolve(){
        return instance;
    }
缺点
这种实际上还是每次调用反射都会实例化一次,只不过新创建的对象没有被返回而已。所以如果创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,

思考?

静态内部类会存在反序列化攻击吗?怎么处理:

解决办法:

同样可以通过添加 readResolve方法来处理,即处理反序列化破坏单例和处理反射破坏单例是不互相冲突的。

注册式单例(登记式单例)

枚举登记

代码如下

public enum EnumRegisterSingleton {

    INSTANCE_ONE,INSTANCE_TWO;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

测试代码如如下:

public class EnumRegisterSingletonTest {
    public static void main(String[] args) {
        EnumRegisterSingleton s1 = null;
        EnumRegisterSingleton s2 = EnumRegisterSingleton.INSTANCE_ONE;
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("EnumRegisterSingleton.INSTANCE_ONE.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("EnumRegisterSingleton.INSTANCE_ONE.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (EnumRegisterSingleton)ois.readObject();
            ois.close();
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果:true

原理

通过反编译后发现,生成class源码中实际上是已经实现了 饿汉式 的单例模式写法

static{
    INSTANCE = new EnumSingleton("INSTANCE", 0);
    $VALUES = (new EnumSingleton[] { INSTANCE });
}
优点
应对序列化破坏

枚举类型其实通过类名和Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次

查看 ObjectInputStream 》 readObject0()

private Object readObject0(boolean unshared) throws IOException {
	...
	case TC_ENUM:
		return checkResolve(readEnum(unshared));
	...
}

看到在readObject0()中调用了readEnum()方法,来看readEnum()中代码实现

String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
	try {
        @SuppressWarnings("unchecked")
		Enum<?> en = Enum.valueOf((Class)cl, name); //重点看这里
		result = en;
	} catch (IllegalArgumentException ex) {
		throw (IOException) new InvalidObjectException(
			"enum constant " + name + " does not exist in " +
			cl).initCause(ex);
	}
    if (!unshared) { //这个unshared 是 readEnum()方法的参数
    	handles.setObject(enumHandle, result);
    }
}
应对反射破坏

JVM 层面就规定了枚举不能通过反射来创建

查看 Constructor 》 newInstance() 方法

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
	throw new IllegalArgumentException("Cannot reflectively create enum objects"
);

容器式单例(Spring)

法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的

public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getBean(String className){
        synchronized (ioc) { //解决线程安全问题
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                	e.printStackTrace();
                }
                return obj;
            } else {
            	return ioc.get(className);
            }
        }
    }
}
Spring 中写法举例
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
    /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */
	private final Map<String, BeanWrapper> factoryBeanInstanceCache = new 				ConcurrentHashMap<>(16);
...
}

threadLocal形式的单例

ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全

代码如下:

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
            @Override
            protected ThreadLocalSingleton initialValue() {
            	return new ThreadLocalSingleton();
            }
         };
    	private ThreadLocalSingleton(){}
    	public static ThreadLocalSingleton getInstance(){
        	return threadLocalInstance.get();
        }
}

原理

ThreadLocal 将所有的对象全部放在ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

划重点

枚举式单例也是《EffectiveJava》书中推荐的一种单例实现写法。在JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值