目录
一.什么是单例模式
单例模式(Singleton Pattern))是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
隐藏其所有的构造方法。
属于创建型模式。
单例模式的优点
- 在内存中只有一个实例,减少了内存开销。
- 可以避免对资源的多重占用。
- 设置全局访问点,严格控制访问。
单例模式的缺点
- 没有接口,扩展困难。
- 如果要扩展单例对象,只有修改代码,没有其他途径。违反开闭原则
单例模式适用用场景:确保任何情况下都绝对只有一个实例。ServletContext、ServletConfig、ApplicationContext、DBPool等都采用单例模式。
单例模式的常见写法:
- 饿汉式单例
- 懒汉式单例
- 注册式单例
- ThreadLocal单例
二.饿汉式单例
特点: 在单例类首次加载时就创建实例
优点: 执行效率高,性能高,没有任何的锁
缺点: 因为实例在我们类加载的时候就被初始化了,如果我们并不使用这个实例,就会造成这块内存空间浪费。
代码示例:
public class HungrySingleton {
//加载类的时候就创建实例
private static final HungrySingleton instance = new HungrySingleton();
//构造器私有化 防止被调用
private HungrySingleton() {
}
//返回一个实例
public static HungrySingleton getInstance() {
return instance;
}
}
三.懒汉式单例
特点: 被外部类调用时才创建实例
优点:节省空间(因为只有当我们第一次主动调用创建实例的方法时才会被创建)
缺点: 两层if判断 双层检查 可读性较差
代码示例:
public class LazySimpleSingleton {
//先声明实例 实际调用的时候才会被创建
private static LazySimpleSingleton instance;
private LazySimpleSingleton(){}
public static LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
这中实现方式存在线程不安全的问题
如果有两个线程同时调用getInstance()方法可能存在问题
运行结果可能是同一个实例,也可能是不同的实例
同一个实例
- 正常顺序执行
- 后者覆盖前者(a,b线程同时进入if语句,然后a执行了new 实例但是并没有执行到return,这时候b又执行了new实例,然后a,b执行return的话就是后者创建的实例,前者的会被覆盖)
不同实例
a,b同时进去if语句,然后a执行了new 实例并执行到return把实例返回;这时候b又执行了new实例,然后又进行了返回。
优化: 加入synchronized
public class LazySimpleSingleton {
//先声明实例 实际调用的时候才会被创建
private static LazySimpleSingleton instance;
private LazySimpleSingleton(){}
// public static LazySimpleSingleton getInstance() {
// if (instance == null) {
// instance = new LazySimpleSingleton();
// }
// return instance;
// }
//加入synchronized 保证线程安全 但是执行效率大大降低了
public synchronized static LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
虽然加入synchronized 保证了线程安全 但是执行效率大大降低了。(当一个线程拿到这个方法时,别的线程在执行这段方法时都要阻塞)
继续优化: 双重检查
public class LazyDoubleCheakSingleton {
private volatile static LazyDoubleCheakSingleton instance;
private LazyDoubleCheakSingleton(){}
public static LazyDoubleCheakSingleton getInstance() {
if (instance == null) { //第一重检查 若实例已经存在就不需要执行 提高效率
//只阻塞第一次
synchronized (LazyDoubleCheakSingleton.class) {
//还得进行一次判断 防止再次创建实例
if (instance == null) { //第二重检查 保证单例
instance = new LazyDoubleCheakSingleton();
//指令重排序的问题
}
}
}
return null;
}
}
为什么要加两层判断,就加一层不可以么?
if (instance == null) {
//只阻塞第一次
synchronized (LazyDoubleCheakSingleton.class) {
instance = new LazyDoubleCheakSingleton();
}
}
如果有两个线程a,b,当线程执行到synchronized 代码块的时候,线程b刚进入if语句,但是b会被阻塞,b需要等待线程a执行完代码块释放了锁,然后b才可以继续执行。这时线程a new完实例并且返回,因为a释放了锁,所以b执行代码块,又重新new了一个实例并返回。所以我们要进行双层检查。
匿名内部类的写法:
public class LazyInnerClassSinglton {
private LazyInnerClassSinglton() {
//防止被反射创建对象
if (InnerClass.instance != null) {
throw new RuntimeException("禁止非法访问");
}
}
public static LazyInnerClassSinglton getInstance() {
return InnerClass.instance;
}
private static class InnerClass {
private static final LazyInnerClassSinglton instance = new LazyInnerClassSinglton();
}
}
四.注册式单例
特点: 将每一个实例都缓存到统一的容器中,使用唯一标识获取实例
枚举式单例优点:
- 不能被反射破坏(枚举类型的构造器是有参的,JDK底层已经定义好了如果是枚举类型就不可以用反射的方式创建枚举对象);
- 线程也安全(枚举类型在类声明的时候就被存储到map中了)。
枚举式单例缺点:相当于懒汉式,枚举类型在类声明的时候就被存储到map中了。
代码演示:
枚举式单例
public enum EnumSinglton {
/*
*/
INSTANCE;
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public static EnumSinglton getInstance() {
return INSTANCE;
}
}
容器式单例
public class ContainerSinglton {
private ContainerSinglton() {}
//定义个容器 把创建好的实例放在容器当中,如果容器中存在需要的实例,直接获取这个实例
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
private static Object getInstance(String className) {
Object instance = null;
if (ioc.containsKey(className)) {
//如果存在 说明创建过 直接返回
instance = ioc.get(className);
} else {
//说明第一次创建 创建实例 并注册到ioc中
try {
instance = Class.forName(className).newInstance();
//注册到容器中
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
}
return instance;
}
}
五.ThreadLocal单例
特点: 每个线程里获取的实例都是相同的。(不同的线程获取的实例就会不同)
代码演示:
public class ThreadLocalSinglton {
private static final ThreadLocal<ThreadLocalSinglton> instance =
new ThreadLocal<ThreadLocalSinglton>(){
@Override
protected ThreadLocalSinglton initialValue() {
return new ThreadLocalSinglton();
}
};
private ThreadLocalSinglton() {}
public static ThreadLocalSinglton getInstance() {
return instance.get();
}
}
六.如何防止被反射和反序列化破坏
1.防止被反射破坏
/**
* 防止被反序列化破坏 ObjectInputStream 的readObject方法在反序列化之前会判断类是否有 readResolve()
* 如果没有就会调用newInstance() 就会重新创建对象导致单例被破坏
* @return
*/
private Object readResolve() {
return instance;
}
2.防止被反射破坏
在构造器里加入判断
private LazyInnerClassSinglton() {
//防止被反射创建对象
if (InnerClass.instance != null) {
throw new RuntimeException("禁止非法访问");
}
}