一、 单例的定义
单例设计模式(Singleton Design Pattern),一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式。
二、如何保证实例的唯一
1、防止外部初始化
2、由类本身进行实例化
3、保证实例化一次
4、对外提供获取实例的方法
5、线程安全
三、单例的经典实现方式
1、饿汉式
//饿汉式
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
2、懒汉式
//懒汉式
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
3、双重检测
//双重检测
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
4、静态内部类
//静态内部类
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
5、单元素枚举
//单元素枚举
public enum Singleton {
INSTANCE;
}
几种实现方式的比较:
实现方式 | 延迟加载 | 并发效率 | 抗反射攻击 | 抗反序列化攻击 |
饿汉式 | 否 | 高 | 否 | 否 |
懒汉式 | 是 | 低 | 否 | 否 |
双重检测 | 是 | 高 | 否 | 否 |
静态内部类 | 是 | 高 | 否 | 否 |
单元素枚举 | 否 | 高 | 是 | 是 |
四. 单例的攻击与防御
1、攻击
1)反射 破解单例模式
java的访问控制是停留在编译层的,只在编译的时候进行访问控制的检查。通过反射的手段可以访问类中的成员,比如私有构造方法。
public static void main(String[] args) throws Exception {
//正常获得单例模式对象
Singleton s1 = Singleton.getInstance();
System.out.println(s1);
//通过反射获得单例模式对象
Class cl = (Class)
Class.forName("cnblogs.bean.Singleton");
Constructor constructor = cl.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s2 = constructor.newInstance();
System.out.println(s2);
System.out.println("s1 == s2? " + (s1 == s2));
}
2)反序列化 破解单例模式
public static void main(String[] args) {
//Singleton 序列化须实现Serializable接口,否则序列化时会报错
Singleton s1 = Singleton.getInstance();
System.out.println(s1);
//先序列化后反序列化
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(s1);
InputStream is = new ByteArrayInputStream(os.toByteArray());
ObjectInputStream ois = new ObjectInputStream(is);
Singleton s2 = (Singleton) ois.readObject();
System.out.println(s2);
System.out.println("s1 == s2? " + (s1 == s2));
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
2、防御
1)针对反射进行防御
在私有构造器中,判断实例是否已创建;但不能根本上解决反射攻击,因为可以反射获取 instance 后再置为null;
private Singleton(){
if(instance !=null){
throw new RuntimeException("正在实例化对象!") ;
}
}
2)针对反序列化进行防御
反序列化时,如果单例类中定义了readResolve(),则直接返回此方法指定的对象,而不在创建新的对象
private Object readResolve(){
return instance;
}
3)使用枚举(天然抵御反射与反序列化)
a、枚举类没有传统意义上的构造函数,因此对这种反射攻击免疫;
b、在序列化的时候,Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法;
五、集群环境下的单例
经典的单例模式可以理解是进程内唯一的;对于 Java 语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(Class Loader);
实现集群环境下的单例,可以把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加分布式锁,避免其他进程再将其获取。在进程使用完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁。
六、单例应用场景
1、唯一递增 ID 号码生成器
public class IdGenerator {
// AtomicLong是一个Java并发库中提供的一个原子变量类型,
// 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
// 比如下面会用到的incrementAndGet().
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
// IdGenerator使用举例
long id = IdGenerator.getInstance().getId();
2、Logger 单例类
// Logger类单例
public class Logger {
private FileWriter writer; //FileWriter 本身是对象级别线程安全
private static final Logger instance = new Logger();
private Logger() {
File file = new File("/Users/wangzheng/log.txt");
writer = new FileWriter(file, true); //true表示追加写入
}
public static Logger getInstance() {
return instance;
}
public void log(String message) {
writer.write(mesasge);
}
}
// Logger类的应用示例
public class UserController {
public void login(String username, String password) {
// ...省略业务逻辑代码...
Logger.getInstance().log(username + " logined!");
}
}
3、Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源
4、数据库连接池、多线程的线程池的设计一般也是采用单例模式
5、Spring 中的bean默认 作用域 是singleton
6、Mybatis中有两个地方用到单例模式,ErrorContext和LogFactory,其中ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。
七、小结
1、单例的5种经典创建方式
2、单例的攻击与防御
3、单例的应用场景
评论区欢迎讨论!觉得有用点个赞再走吧。感谢!!!