社交广场

1.单例模式常见的几种写法

单例模式

反射和反序列化破坏单例及解决方案

单例模式(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 ioc = new ConcurrentHashMap<>();

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);

}

}

}

以上就是几种比较常见的单例。