设计模式:单例模式

一、导引

1、概念
  • 定义:

    保证一个类仅有一个实例,并提供一个全局访问点

  • 类型:

    创建型

2、适用场景

​ 想确保任何情况下都绝对只有一个实例

  • 优点

    1. 在内存里只有一个实例,减少了内存开销
    2. 在内存里只有一个实例,减少了内存开销
    3. 设置全局访问点,严格控制访问
  • 缺点

    没有接口,扩展困难

  • 重点

    1. 私有构造器
    2. 线程安全
    3. 延迟加载
    4. 序列化和反序列化安全
    5. 反射

二、懒汉式

​ 类初始化的延迟加载解决方案。

1、coding
// 懒汉式单例类
public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton() { }
    
    // 加 synchronized 同步获取实例,线程安全
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}
// 线程执行类
public class T implements Runnable {
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + "  " + instance);
    }
}
// 测试类
public class Test {
    public static void main(String[] args)  {
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
        System.out.println("program end");
    }
}
2、多线程 debug

​ 在断点上,点击鼠标右键出现如下选择框。
在这里插入图片描述

  • Suspend: 挂起方式。
  • All:只会 debug 到本线程(主线程)的断点
  • Thread:每个线程执行到断点都会被挂起,等到被处理
  • Make Default:设置为默认挂起方式
3、双重检查懒汉式单例

DoubleCheck

public class LazyDoubleCheckSingleton {
    // 添加 volatile关键字  禁止指令重排序
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton() { }
    
    // doubleCheck 避免直接在方法上加锁,大幅减少加锁、解锁的开销
    public static LazyDoubleCheckSingleton getInstance(){
        // 当两个线程分别执行到 9和11行时,由于指令重排序,仍存在出错的风险
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    // new LazyDoubleCheckSingleton() 非原子操作,实际分为下面三步执行
                    //1.分配内存给这个对象
                    //3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
                    //2.初始化对象
                         // intra-thread semantics  (2、3步骤可以重排序)
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}
4、Double Check 问题
  • 单线程时
    在这里插入图片描述

  • 多线程时
    在这里插入图片描述

  • volatile关键字 禁止指令重排序

5、静态内部类单例模式
public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = 
            new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    
    private StaticInnerClassSingleton(){
        if(InnerClass.staticInnerClassSingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
}
  • 原理
    在这里插入图片描述

类的初始化阶段:类加载器加载后,线程使用前,在此期间执行类的初始化,JVM 会获取一个锁去同步多个线程对一个 类的初始化(图中绿色框),防止类被多次多次初始化,同时由于初始化锁,线程0、1是不能够同时看到在类初始化阶段的重排序的。

  • 类被立即初始化的五种情况
    1. 有一个类的实例被创建
    2. 类中声明的一个静态方法被调用
    3. 类中声明的一个静态成员被赋值
    4. 类中声明的一个静态成员被使用且该静态成员不是常量成员
    5. 类为一个顶级类并且在类中有嵌套的断言语句

三、饿汉式

1、coding
public class HungrySingleton implements Serializable,Cloneable {
    // 声明为 final的对象,必须在初始化完成时就以赋值
    private final static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){}
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    
    // 加上这个方法,反序列化获得的对象和单例的对象是同一个对象
	private Object readResolve(){
        return hungrySingleton;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }
}
2、序列化
// Test
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.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
  • 没有实现 readResolve() 方法时:
    在这里插入图片描述

  • 原因

    反序列化是通过反射的方式来调用 newInstance() 重新生成的对象,自然和单例的对象不同。

    反序列化 在用反射创建对象后,返回对象前,会先检查 类是否实现了 readResolve(),如果类实现了, 会直接反射调用该方法, 然后返回该方法返回的对象。 ==> 加上 该方法,在序列化的过程中仍会创建新对象,只是没有返回。

3、反射攻击
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);

HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);

在这里插入图片描述

4、反射防御
  • 构造函数 反射防御

    这种防御对于在类加载阶段就已经创建好的单例是有效的。(饿汉式和静态内部类是有效的,对于懒汉式类的延迟加载是无效的)

private HungrySingleton(){
    if(hungrySingleton != null){
        throw new RuntimeException("单例构造器禁止反射调用");
    }
}

在这里插入图片描述

四、枚举单例

1、coding
public enum EnumInstance {
    INSTANCE {
        protected  void printTest() {
            System.out.println("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;
    }
}
  • Test
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(
    new FileOutputStream("singleton_file"));
oos.writeObject(instance);
// 反序列化
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(
    new FileInputStream(file));
EnumInstance newInstance = (EnumInstance) ois.readObject();
// 比较
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());

在这里插入图片描述

  • 枚举单例 可以避免 在反序列化 创建新的对象。 也可以避免反射攻击。
2、反编译 枚举类
// 调用枚举对象的方法
EnumInstance instance = EnumInstance.getInstance();
instance.printTest();
  • 编译 和 反编译 枚举类

    编译时 生成两个.java文件 EnumInstance.java EnumInstance$1.java,并自动删除原来的文件

    反编译生成两个 .class EnumInstance.class EnumInstance$1.class

// EnumInstance.class
import java.io.PrintStream;

public abstract class EnumInstance extends Enum {
	public static final EnumInstance INSTANCE;
	private Object data;
	private static final EnumInstance $VALUES[];

	public static EnumInstance[] values() {
		return (EnumInstance[])$VALUES.clone();
	}
	public static EnumInstance valueOf(String s) {
		return (EnumInstance)Enum.valueOf(main/com/                             design/pattern/creational/singleton/EnumInstance, s);
	}
	private EnumInstance(String s, int i){
		super(s, i);
	}
	protected abstract void printTest();

	public Object getData(){ return data; }
	public void setData(Object obj)	{ data = obj; }

	public static EnumInstance getInstance() {
		return INSTANCE;
	}
    
	static {
		INSTANCE = new EnumInstance("INSTANCE", 0) {
			protected void printTest() {
				System.out.println("Print Test");
			}
		};
		$VALUES = (new EnumInstance[] {
			INSTANCE
		});
	}
}

// EnumInstance$1.class
static class EnumInstance$1 extends EnumInstance {
	protected void printTest() {
		System.out.println("Print Test");
	}

	EnumInstance$1(String s, int i) {
		super(s, i, null);
	}
}

五、容器单例

1、coding
public class ContainerSingleton {
    private ContainerSingleton() { }
    private static Map<String,Object> singletonMap = 
        // 线程不安全,但使用 hashTable 同步锁会影响效率
        new HashMap<String,Object>();

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

六、“单例” ThreadLocalInstance

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> 
        threadLocalInstanceThreadLocal
             = new ThreadLocal<ThreadLocalInstance>(){
        @Override
        protected ThreadLocalInstance initialValue() {
            return new ThreadLocalInstance();
        }
    };
    private ThreadLocalInstance() { }

    public static ThreadLocalInstance getInstance(){
        return threadLocalInstanceThreadLocal.get();
    }
}

​ ThreadLocal 在一个线程里获取的单例是唯一的。 每个线程 一个单例

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值