目录
二、设计模式之单例模式
所属类型 | 定义 |
---|---|
创建型 | 确保某一个类,只有一个实例,并且自行实例化并向整个系统提供该实例 |
单例模式能帮我们干什么?
主要解决什么问题?
单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升整体代码的性能。
优缺点
优点
- 可以保证一个类只有一个实例
- 获得了一个指向该实例的全局访问节点
- 仅在首次请求单例对象时对其进行初始化
缺点:
- 违反了“单一职责原则”(该模式同时解决了两个问题)
- 该模式在多线程中需特殊处理,避免多个线程多次创建单例对象
单例模式使用的场景
1. 降低New对象的性能损耗。如:配置类提供单例
2 控制某些共享资源的访问权限(连接数据库、访问特殊文件)
3. 某些全局的属性或变量想保持其唯一性,可使用
4. 程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式
单例模式的实现
饿汉式(线程安全)
通过事先准备好一个单例(馒头),当获取单例实例的时候,去保证单例结果。因为依赖静态static控制。只有在该类加载的时候创建对象。保证了单例性。
它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。
优点:线程安全,没有加任何锁、执行效率比较高。
缺点:类加载的时候就初始化,不管后期用不用都占着空间,浪费了内存。饿汉式单例适合用在单例类比较少的情况下,在实际项目中,有可能会存在很多的单例类,如果我们都使用饿汉式单例的话,对内存的浪费会很大,所以,我们要学习更优的写法。
实现难度: ⭐️
package design_pattern_23.singleton;
/**
* 饿汉式
*/
public class Sington {
/** 1. 实现初始化好静态实例*/
private static final Sington INSTANCE = new Sington();
/** 2. 保证创建工作只有自实例化完成 */
private Sington(){}
/** 3. 提供给系统调用,并是同一个实例*/
public static Sington getInstance(){
return INSTANCE;
}
public String print(String info){
System.out.println(info + " : Instance="+this);
return "";
}
// Test : 期望所有打印的实例地址都是一样的
public static void main(String[] args){
Sington.getInstance().print("1");
Sington.getInstance().print("2");
Sington.getInstance().print("3");
Sington.getInstance().print("4");
}
}
懒汉式(线程不安全)
实现难度: ⭐️
懒加载模式,只有在具体调用的时候,去建造第一个单例。后续有的话,就直接返回实例。
线程不安全:多线程导致方法临界区的执行不能原子化完成。必然导致线程不安全。
package com.kongxiang.raindrop.dp.type.create.singleton;
/**
* 懒汉式(线程不安全)
*/
public class LazySingleton {
/** 1. 声明静态单例变量 holder */
private static LazySingleton INSTANCE = null;
/** 2. 自实例控制 */
private LazySingleton() {
}
/** 暴露给系统的实例获取静态方法*/
public static LazySingleton getInstance() {
if(INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
懒汉式(线程安全)
难度: ⭐️
处理线程不安全(保证原子性),想到的第一种方式就是加锁,用锁来控制方法的原子性操作。
synchronized 或 Lock
/** 暴露给系统的实例获取静态方法 加锁控制线程安全*/
public static synchronized LazySingleton getInstance() {
if(INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
双重检查锁
难度: ⭐️ ⭐️
上面这种方式成功解决了线程安全问题,但在线程数量比较多的情况下,大量线程会阻塞在方法外部,导致程序性能下降。为了兼顾性能和线程安全问题,我们可以通过双重检查锁的方式来创建懒汉式的单例:
/**
* 暴露给系统的实例获取静态方法 加锁控制线程安全
*/
public static LazySingleton getInstance() {
if (INSTANCE == null) {
// 线程阻塞等待
synchronized (LazySingleton.class) {
// 阻塞结束,发现已经存在,就直接返回
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
}
}
return INSTANCE;
}
静态内部类方式
难度: ⭐️ ⭐️
理解难度:⭐️ ⭐️ ⭐️
用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。
这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。内部类一定是要在 方法调用之前初始化,巧妙地避免了线程安全问题。
public class InnerClassSingleton {
private InnerClassSingleton(){}
/**
* 懒汉式(该方法调用时,去加载内部类,完成类静态字段初始化创建)
* @return
*/
public static final InnerClassSingleton getInstance(){
return InnerClass.INSTANCE;
}
/**
* 饿汉式静态类创建(保证线程安全)
*/
public static class InnerClass {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
}
注册式单例模式
注册式单例模式又叫登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识 获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
枚举式单例模式【建议】
实现难度: ⭐️
理解难度:⭐️ ⭐️
使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
- 解决序列化破坏的问题
- 解决反射破坏问题。 jdk代码里直接判断枚举类型反射构造器创建直接抛错。
/**
* 注册式单例
* 1. 单元素枚举类单例
*/
public enum EnumSingleton{
INSTANCE ;
public void todosomething(){
System.out.println("hellow ");
}
}
容器式单例(线程不安全)
适用于单例非常多的情况,容易管理。
public class ContainerSingleton {
private static final Map<Class,Object> ioc = new ConcurrentHashMap<>();
private ContainerSingleton(){}
public static Object getBean(Class clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
synchronized (ioc){
if(!ioc.containsKey(clazz)){
Constructor declaredConstructor = clazz.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance();
ioc.put(clazz,o);
}else {
ioc.get(clazz);
}
}
return ioc.get(clazz);
}
}
可以破坏单例的方式
反射
反射可以访问
private
方法,构造器,变量。使单例的自实例化变得困难。
尽量处理:在构造器中强制只能调用一次。后续多次调用直接抛错处理.
private Singleton(){
if(InnerClass.INSTANCE != null){
throw new RuntimeException();
}
}
序列化
序列化 读取出来的对象,是一个新建对象。
总结
- 单例模式,多种实现。简单掌握各个的特点。心中有数即可
- 实际开发过程中,建议使用枚举。因为大神都在用枚举做单例
- 使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
- 枚举不需要考虑线程安全,其本身enum关键字就是线程安全的。
- 不用考虑 反射和序列化攻击。jdk做了安全策略。