单例模式 反射和反序列化破坏单例及解决方案 单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接 池也都是单例形式。 单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。 单例模式特点 1.单例类只能有一个实例; 2.单例类必须自己创建自己的唯一实例; 3.单例类必须给所有其他对象提供这一实例。 常见单例模式 单例模式的写法有很多种,这里主要提供四种:饿汉式单例、懒汉式单例、枚举式单例、容器式单例(枚举式单例和容器式单例都属于注册式单例) 饿汉式单例 在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题, Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例 优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。 缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。 package org.about.designpatterns.singleton.hungry; /** * @Package org.about.designpatterns.singleton.hungry * @Author * @Description 饿汉式单例 在类加载的时候就立即初始化,并且创建单例对象。 * 绝对线程安全,在线程还没出现以前就是实例化了,不可能存在线程安全问题 * @Version: V1.0 */ public class HungrySingleton { /** * 定义该单例属性 */ private static final HungrySingleton HUNGRY_SINGLETON; static { HUNGRY_SINGLETON = new HungrySingleton(); } /** * 构造方法私有化 */ private HungrySingleton() { // 防止反射破坏单例,下一篇会介绍 if (HUNGRY_SINGLETON != null) { throw new RuntimeException("不允许创建多个单例"); } } /** * 提供全局唯一访问点 * @return HUNGRY_SINGLETON */ public static HungrySingleton getInstance() { return HUNGRY_SINGLETON; } // /** // * 防止序列化破坏单例 // * 如果该单例实现了 Serializable 序列化接口,为了防止序列化破坏单例,需要重写 readResolve()方法 // * 下一篇会讲解序列化破坏单例 // * @return SINGLETON // */ // private Object readResolve() { // return HUNGRY_SINGLETON; // } } 懒汉式单例 被外部类调用的时候内部类才会加载单例对象,避免了饿汉式在类加载的时候就产生实例而产生性能消耗 双重检查锁写法 package org.about.designpatterns.singleton.lazy; /** * @Package org.about.designpatterns.singleton.lazy * @Author Epocher * @Description 懒汉模式: 双重检查锁 * * 知识点: 多线程下指令重排问题 volatile * (this) lazy = new LazyDoubleCheckSingleton(); * cpu执行上面的时候会转换为JVM指令:1.分配内存给这个对象; 2.属性初始化; 3.引用指向对象 * 上面语句为非原子性,所以上面可能会产生指令重排问题 即:正常情况下执行顺序为 1->2->3 但是实际情况下可能就是 1->3->2 * * (1)单线程情况下,指令重排没有影响; * (2)但在多线程情况下,假如线程(1)执行 lazy = new LazyDoubleCheckSingleton()语句时先1 再 3, * 但是此时系统调度线程(2),没来得及执行步骤2,但此时已有引用指向对象 * (即已经执行3 但是没有完成初始化2。此时对象处于半初始化状态,但对象此时已经不为null了。但是还没属性初始化) * 故线程2在第一次检查时不满足条件直接返回 lazy,此时 lazy虽然不为null 但是内部属性还没有完成初始化。 * volatile 关键字可保证 lazy = new LazyDoubleCheckSingleton();的语句执行顺序为 1 2 3 * 具体可以参考 volatile 的特性。这里就不做过多说明了 *
* @Date 2020-07-28 16:35
* @Version: V1.0
*/
public class LazyDoubleCheckSingleton {
/**
* LazyDoubleCheckSingleton 实例属性
* volatile 关键字禁止指令重排, 即为第二层锁
*/
private volatile static LazyDoubleCheckSingleton lazy;
/**
* 构造方法私有化
*/
private LazyDoubleCheckSingleton() {
// 这个是为了防止 通过反射机制从而破坏了单例模式
// 下一篇会做讲解
if (lazy != null) {
throw new RuntimeException("不允许创建多个单例!");
}
}
/**
* 提供全局唯一访问点
*
* synchronized 虽然在性能上已经有了优化,但是还是不可避免的产生内存的消耗
* 下面会介绍更加好的单例模式
*
* @return lazy
*/
public static LazyDoubleCheckSingleton getInstance() {
// Double Check Lock 双重监察锁的机制来实现,(DCL)
// 第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入 的时候才创建单例,避免重复创建
// lazy == null同时避免每个线程进来的时候首先竞争锁,从而消耗资源,这样每个线程进来先判断是否已经创建过,没创建对象的情况下才会竞争锁。这样对资源
if (lazy == null ) {
// synchronized 锁,synchronized保证只有一个线程进入
synchronized (LazyDoubleCheckSingleton.class) {
// 第二层检查,若不检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
if (lazy == null) {
// 利用 volatile 关键字防止指令重排
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
// /**
// * 防止序列化破坏单例
// * 如果该单例实现了 Serializable 序列化接口,为了防止序列化破坏单例,需要重写 readResolve()方法
// * 下一篇会讲解序列化破坏单例
// * @return SINGLETON
// */
// private Object readResolve() {
// return lazy;
// }
}
静态内部类写法
利用静态内部类加载的特性:不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法的时候才会加载静态内部类。
利用JVM底层特性,巧妙的避免了线程安全问题。性能相对来说很优秀
package org.about.designpatterns.singleton.lazy;
/**
* @Package org.about.designpatterns.singleton.lazy
* @Author
* @Description 静态内部类实现懒汉式单例 性能最好 全程没有用到 synchronized关键字
* @Version: V1.0
*/
public class LazyInnerClassSingleton {
/**
* 构造方法私有化
*/
private LazyInnerClassSingleton(){
// 这个是为了防止 通过反射机制从而破坏了单例模式
// 下一篇会做讲解
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个单例!");
}
}
/**
* 全局唯一访问点
* static 是为了使单例的空间共享
* final 保证这个方法不会被重写,重载
* @return LazyHolder.LAZY
*/
public static final LazyInnerClassSingleton getInstance() {
// 在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
/**
* 静态内部类 利用了静态内部类 加载时的特性
* 内部静态类不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法的时候才会加载静态内部类。
* JVM 底层的逻辑,完美的避免了线程安全问题
*/
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
// /**
// * 防止序列化破坏单例
// * 如果该单例实现了 Serializable 序列化接口,为了防止序列化破坏单例,需要重写 readResolve()方法
// * 下一篇会讲解序列化破坏单例
// * @return SINGLETON
// */
// private Object readResolve() {
// return LazyHolder.LAZY;
// }
}
饿汉模式与懒汉模式
时间和空间
饿汉式:是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
懒汉式:是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间
线程安全
饿汉式:线程绝对安全,因为在类加载的时候就已经初始化了对象实例。JVM只会装载一次,在并发发生之前就已经创建了唯一实例
懒汉式:存在线程安全问题。好的解决方案是 上面的双重检查锁和静态内部类写法
注册式单例
注册式单例:又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记
枚举式单例
不仅能解决多线程同步问题,而且能防止反序列化重新创建新的对象和反射破坏单例。
因为是java底层对枚举进行单独的处理。而枚举出现的也对较晚jdk1.5,所以使用的频率也不是特别高
,本质上也属于饿汉式单例,但是很多书籍都推荐的一种写法
package org.about.designpatterns.singleton.register;
/**
* @Package org.about.designpatterns.singleton.register
* @Author
* @Description 注册式: 枚举式单例
* @Version: V1.0
*/
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
容器式单例
相当于有一个容器装载所有实例,在实例产生之前先检查下容器有没有,如果有就直接取出来,如果没有就先new一个放进去,然后给后面的人用,SpringIoc容器就是一种注册登记式单例
登记式单例实际上维护了一种单例类的实例,将这些实例存放在一个Map中,对于已经登记过的实例,则从Map直接返回,没有登记的,则先登记,然后返回;
登记式单例内部实现其实还是用的饿汉式,因为其中的static方法块,它的的单例在类被装载时就被实例化了
不加锁的话会存在线程安全问题
package org.about.designpatterns.singleton.register;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Package org.about.designpatterns.singleton.register
* @Author
* @Description 容器式单例
* @Version: V1.0
*/
public class ContainerSingleton {
/**
* 构造方法私有化
*/
private ContainerSingleton() {}
private static Map
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;
}
// 存在就直接返回 object
return ioc.get(className);
}
}
}
以上就是几种比较常见的单例。