单例模式
Posted zhouxuan-c
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式相关的知识,希望对你有一定的参考价值。
定义
保证一个类仅有一个实例,并提供一个全局访问点
类型
创建型
适用场景
想确保任何情况下都绝对只有一个实例
优点
- 内存中仅有一个实例,减少内存开销。特别是一个对象需要频繁创建、销毁时,且创建、销毁时的性能无法优化。
- 可以避免对资源的多重占用,例如对文件进行写操作时
- 设置了全局访问点,严格控制访问。即对外不让 new, 只能通过方法来创建单例对象
缺点
没有接口,扩展困难
重点
- 私有构造器 (不让外界 new)
- 线程安全
- 延迟加载
- 序列号、反序列化安全
- 反射 (防御反射)
Coding
懒汉式
/**
* 懒汉式: 延迟加载, 只有使用它时, 才初始化
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() { }
/**
* 线程不安全, 多线程环境下.
* 第一个线程走到 2 , 但还未执行好.
* 第二个线程走到 1 , 此处一判断, 因为刚才那个还没 new, 所有结果时 true
* 然后就 new 了两次, 最后返回的是后面一次的 new 出来
*/
public static LazySingleton getInstance() {
if (lazySingleton == null) { // 1
lazySingleton = new LazySingleton(); // 2
}
return lazySingleton;
}
}
然后,创建两个线程测试下。
public class T implements Runnable{
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " , " + lazySingleton);
}
}
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("End");
}
}
至于多线程 debug。右键小红点,选择 ‘Thread‘,然后走一个,点下拉框,就可以切换线程
多线程下出现问题,可以使用 synchronized 修饰
public static LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
但是同步锁比较消耗资源,而且 public synchronized static LazySingleton getInstance()
时,锁的是 class,范围挺大,对性能有一定影响。怎么在性能和安全性方面取得平衡呢?
双层检查的懒汉式
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() { }
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
// 此处注意指令重排序, 这一步可能会分为三部. 且 2 和 3 执行顺序可能会颠倒.
// 1. 分配内存给这个对象
// 2. 初始化对象
// 3. 设置 lazyDoubleCheckSingleton 指向刚分配的内存地址
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
具体就是 if 判断时, lazyDoubleCheckSingleton 并不为 null, 因为前一个线程已经将 lazyDoubleCheckSingleton 已经指向一个内存空间。但是并没有初始化对象, 所以接下来直接 return 出去用就可能出现问题了
但注意,指令重排序,是不会影响单线程的执行结果的。(intra-thread semantics)
怎么解决呢?
- 不允许重排序
- 允许一个线程的重排序,但是不让其他线程 "看到" 重排序
第一种解决方案,使用 volatile 修饰 lazyDoubleCheckSingleton 就行了.
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
在加上 volatile 后,重排序就会被禁止。多线程时,CPU 也有共享内存,所有的线程就都能看到共享内存的最新状态,保证了内存的可见性。(抱歉,关于 volatile 这里可能有错,我也没听懂,后面看并发编程再详学 TODO)
第二种解决方案,如何让线程一看不到线程零的重排序呢?使用静态内部类
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
}
JVM 在类的初始化阶段,即 class 加载后,被线程使用之前,都是类的初始化阶段。顾名思义,这个阶段会执行类的初始化(过些日子学 JMV)。在执行类的初始期间,JVM 会去获取一个锁,这个锁可以同步多个线程对一个类的初始化,即上图中绿色部分。对于这个特性,我们可以实现基于静态内部类的且线程安全的延迟初始化方案。
在这种实现模式中,对于右侧的 2、3,也就是橙色的框。这两个的重排序,前面的线程 1并不会看到。即非构造线程,是不会看到重排序的。
当 线程0 和 线程1 试图获取锁的时候,这是肯定只有一个能获得,假设 线程0 获得,然后执行静态内部类的初始化。这时,即使2、3之间存在重排序。但是,线程1 是无法看到重排序的。因为有一个 class 对象的初始化锁。线程1 正在绿色区域等待。
饿汉式
在类加载时,就完成实例化。也因为在类初始化时就加载好了,所以是没有线程安全问题的。
public class HungrySingleton {
private HungrySingleton() {}
// 这里也可以把它放到静态代码块中
private final static HungrySingleton hungrySingleton = new HungrySingleton();
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
使用序列号、反序列化破坏单例模式
工程中,一旦涉及序列号和反序列化的操作,请小心对单例的破坏
public class HungrySingleton implements Serializable {
private HungrySingleton() {}
private final static HungrySingleton hungrySingleton = new HungrySingleton();
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
public class Test {
public static void main(String[] args) throws Exception {
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);
System.out.println(newInstance);
// false
System.out.println(instance == newInstance);
}
}
这里没看明白,只知道反射啥的....(TODO)
反射攻击?
使用反射把构造器的私有权限打开,然后获取对象。
public class Test {
public static void main(String[] args) throws Exception {
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
HungrySingleton instance = HungrySingleton.getInstance();
// 置成 true,private权限就打开了. 注释掉就不能成功创建实例
constructor.setAccessible(true);
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
如何防止反射?
// 在私有构造方法中,加一个判断
private HungrySingleton() {
if (hungrySingleton != null) {
throw new RuntimeException("单例构造器禁止反射.");
}
}
但是如果对象已存在,再用反射会抛出异常,如果对象不存在,还是可以正常通过反射创建对象。如果你想在类中使用信号量,不管是 int 还是 布尔。也是不行的,反射也可以把它改掉。
// 修改 o1
HungrySingleton o1 = HungrySingleton.getInstance();
Field flag = o1.getClass().getDeclaredField("flag");
flag.setAccessible(true);
flag.set(o1, true);
Enum 枚举单例(推荐的)
public enum EnumInstance implements Serializable {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance() {
return INSTANCE;
}
}
public class Test {
public static void main(String[] args) throws Exception {
EnumInstance enumInstance = EnumInstance.getInstance();
enumInstance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
oos.writeObject(enumInstance);
File file = new File("test");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumInstance newInstance = (EnumInstance) ois.readObject();
System.out.println(enumInstance.getData());
System.out.println(newInstance.getData());
System.out.println(enumInstance.getData() == newInstance.getData());
}
}
反编译后会看到
final 不能被继承
构造器私有 不允许外部实例化
类变量是静态 没有延迟初始化,通过静态块,在类加载时,就把对象的初始化完成。因此也是线程安全的
以上是关于单例模式的主要内容,如果未能解决你的问题,请参考以下文章