一、导引
1、概念
-
定义:
保证一个类仅有一个实例,并提供一个全局访问点
-
类型:
创建型
2、适用场景
想确保任何情况下都绝对只有一个实例
-
优点
- 在内存里只有一个实例,减少了内存开销
- 在内存里只有一个实例,减少了内存开销
- 设置全局访问点,严格控制访问
-
缺点
没有接口,扩展困难
-
重点
- 私有构造器
- 线程安全
- 延迟加载
- 序列化和反序列化安全
- 反射
二、懒汉式
类初始化的延迟加载解决方案。
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、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 在一个线程里获取的单例是唯一的。 每个线程 一个单例。