单例设计模式——我的理解
简单来说,就是用来创建独一无二的,只有一个实例对象,并且对外提供一个访问该实例的全局访问点
使用单例模式有什么好处
单例模式只生成一个实例,减少了系统性能开销。当一个实例的创建需要较多资源时,比如读取配置文件、产生其他依赖对象等,可以通过启动应用时直接产生一个永驻内存的单例对象来避免重复创建对象产生的性能消耗。
应用场景
- 项目中读取配置文件的类
- 数据库连接池
- Spring中的Beans默认是单例
- Windows的任务管理器
等等…
实现方式
- 饿汉式
- 懒汉式
- 双锁检测式
- 静态内部类
- 枚举
- 我认为,静态内部类的实现方式基本上集合了所有懒汉式和饿汉式的优点。最推荐使用的实现方式。
1-饿汉式
- 优点:线程安全,调用效率高
- 缺点:存在资源浪费,不能延时加载
/**
* 饿汉式单例
* 线程安全,调用效率高但是存在资源浪费,不能延时加载
*/
public class SingletonDemo {
//static对象在类加载时被创建,类加载的过程中是天然的线程安全的模式
private static SingletonDemo instance = new SingletonDemo();
private SingletonDemo(){
//若通过反射漏洞多次访问构造方法,一定会在instance不为null时另外创建其他对象,在构造方法中增加判断条件即可.
//注意,虽然构造方法是私有的,但是通过Constructor对象的setAccessible(true)是可以跳过私有检查的.
if(instance != null){
throw new RuntimeException("Instance is not null");
}
}
//instance已经在线程安全的状态下创建过了,所以getInstance方法不需要加syncohronized关键字就是线程安全的.
public static SingletonDemo getInstance(){
return instance;
}
}
2-懒汉式
- 优点:线程安全,资源利用率高,延时加载
- 缺点:并发效率低。懒汉式因为getInstance加了锁,是所有实现方式中效率最低的。
/**
* 懒汉式延时加载
* 线程安全,资源利用率高但getInstance被加锁了,并发效率低.
*/
class SingletonDemo02{
// 类加载时不创建对象
private static SingletonDemo02 instance;
private SingletonDemo02(){}
// 由于多线程调用getInstance时存在线程A和线程B同时执行到if(instance == null)部分,会导致两个线程分别创建一个对象,违反单例的初衷,所以加锁.
public static synchronized SingletonDemo02 getInstance(){
// 当用到这个对象时getInstance方法被调用,才会创建对象,避免了资源浪费.
if (instance == null){
instance = new SingletonDemo02();
}
return instance;
}
}
3-双锁检测式
双锁检测机制是对懒汉式的一种改进,想要通过锁定局部代码不锁方法的形式改良懒汉式并发访问效率差的问题
理想很丰满,但是***因为JVM内部结构的问题,该方法容易出错,不能使用。***
/**
* 双重检测锁机制
* 将synchronized块放到了方法内
*/
class SingletonDemo03{
// 类加载时不创建对象
private static SingletonDemo03 singleton;
private SingletonDemo03(){}
public static SingletonDemo03 getInstance(){
if(singleton == null){
// 同步代码块
synchronized(SingletonDemo03.class){
if(singleton == null)
singleton = new SingletonDemo03();
}
}
return singleton;
}
}
4-静态内部类
静态内部类的实现方式基本上集合了所有懒汉式和饿汉式的优点。最推荐使用的实现方式。
/**
* 静态内部类方式
* 线程安全,调用效率高,懒加载
*/
class SingletonDemo04{
// 静态内部类在SingletonDemo04加载的时候不会同其他static对象一起加载,在用到的时候才加载
public static class SingletonInstance{
static {
System.out.println("inner class loaded");
}
public static final SingletonDemo04 instance = new SingletonDemo04();
}
private SingletonDemo04(){}
public static SingletonDemo04 getInstance(){
return SingletonInstance.instance;
}
}
5-枚举
枚举是通过JVM内部机制实现的,比较纯天然。在效率上略低于静态内部类的方式,也不能懒加载。没有其他缺点。
/**
* 通过枚举创建单例对象
*/
enum SingletonDemo05{
//本身就是单例对象
INSTANCE;
//用户可添加其他操作
public static void singletonOperation(){
}
}
注意事项
- 反射漏洞
存在问题:
反射通过Class.forName()是可以创建任何一个类的构造器的,而构造器又可以通过setAccessible(true)来访问该类的private成员和方法,导致单例模式被打破。解决办法如下:
解决办法:
在构造器中设置一个instance是否为空的检测。因为单例模式是在instance为空时创建对象,不为空时直接返回现有的对象。也就是说构造器实际上只会在instance为空时调用一次 。方法中的判断条件可以有效防止反射漏洞。
//若通过反射漏洞多次访问构造方法,一定会在instance不为null时另外创建其他对象,在构造方法中增加判断条件即可.
//注意,虽然构造方法是私有的,但是通过Constructor对象的setAccessible(true)是可以跳过私有检查的.
private SingletonDemo06(){
if (instance != null){
throw new RuntimeException("Instance is not null");
}
}