单例模式
核心作用
保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
常见应用场景
1、windows系统的任务管理器和回收站就是很典型的单例模式;
2、项目中读取配置文件的类,一般也只有一个对象,没必要每次使用配置文件数据,每次new一个对象去读取;
3、网站的计数器,一般也是采用单例模式实现,否则难以同步;
4、应用程序的日志应用,一般都采用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;
5、数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源;
6、Servlet编程中的Application、每个Servlet也是单例模式的典型应用
7、在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理;
8、在SpringMVC中,控制器对象也是单例。
优点
由于单例模式只生成一个实例,减少了系统的开销,当一个对象的产生需要比较多的资源时,比如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
常见的五种单例模式实现方式
主要
- 饿汉式(线程安全,调用效率高。但是,不能延时加载。)
- 懒汉式(线程安全,调用效率不高,资源利用率高了。但是,可以延时加载。)
其他
- 双重检测锁式(由于JVM底层内部模式原因,偶尔会出问题,不建议使用)
- 静态内部类式(线程安全,调用效率高,可以延时加载)
- 枚举单例(线程安全,调用效率高,不能延时加载,可以避免反射和反序列化的漏洞)
选用方式
单例对象 占用资源少,不需要延时加载
枚举式 好于 饿汉式
单例对象 占用资源大 需要延时加载
静态内部类式 好于 懒汉式
案例
1、饿汉式
/**
* 饿汉式单例模式(单例对象立即加载)
* @author huangyzh
* @create 2020-05-13 0:07
*/
public class SingletonDemo01 {
//类初始化时,立即加载这个对象(没有延时加载的优势)
//由于加载类时,天然的是线程安全的
private static SingletonDemo01 instance = new SingletonDemo01();
/**
* 私有构造器
*/
private SingletonDemo01(){}
/**
* 方法没有同步,调用效率高
* @return
*/
public static SingletonDemo01 getInstance(){
return instance;
}
}
饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会设计多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此可以省略synchronized关键字
问题:r如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!
2、懒汉式
/**
* 懒汉式单例模式(单例对象延迟加载)
* @author huangyzh
* @create 2020-05-13 0:15
*/
public class SingletonDemo02 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo02 instance;
/**
* 私有构造器
*/
private SingletonDemo02(){}
//方法同步,调用效率不高,资源利用率高
public static synchronized SingletonDemo02 getInstance(){
if(instance == null){
instance = new SingletonDemo02();
}
return instance;
}
}
要点:lazy load! 延迟加载! 懒加载! 真正用的时候才加载!
问题:资源利用率高了。但是,每次调用getInstance()方法都要同步,并发效率较低。
3、双重检测锁式
/**
* 双重检测锁式(不建议使用,了解即可)
* @author huangyzh
* @create 2020-05-13 0:19
*/
public class SingletonDemo03 {
private static SingletonDemo03 instance;
/**
* 私有构造器
*/
private SingletonDemo03(){}
public static SingletonDemo03 getInstance(){
if(instance == null){
SingletonDemo03 sd;
synchronized (SingletonDemo03.class){
sd = instance;
if(sd == null){
synchronized (SingletonDemo03.class){
if(sd == null){
sd = new SingletonDemo03();
}
}
instance = sd;
}
}
}
return instance;
}
}
这个模式将同步内容下放到if内部,提供了执行的效率,不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。
问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题,不建议使用。
4、静态内部类式
/**
* 静态内部类式(线程安全,调用效率高,并且实现了延时加载)
* @author huangyzh
* @create 2020-05-13 0:25
*/
public class SingletonDemo04 {
private static class SingletonClassInstance{
private static final SingletonDemo04 instance = new SingletonDemo04();
}
/**
* 私有构造器
*/
private SingletonDemo04(){}
public static SingletonDemo04 getInstance(){
return SingletonClassInstance.instance;
}
}
要点:
1、外部类没有static属性,则不会像饿汉式那样立即加载对象。
2、只要真正调用getInstance()才会加载静态内部类。加载类时是线程安全的。instace是static final类型,保证了内存中只要这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。
3、兼备了并发高效调用和延迟加载的优势!
破解单例问题
1)反射方式
反射可以破解上面几种实现方式:
可以在构造器中收到抛出异常控制
测试代码
/**
* 测试反射破解单例
* @author huangyzh
* @create 2020-05-13 1:25
*/
public class SigletonTest01 {
public static void main(String[] args) throws Exception {
SingletonDemo02 s1 = SingletonDemo02.getInstance();
SingletonDemo02 s2 = SingletonDemo02.getInstance();
System.out.println(s1);//edu.yunyu.singleton.SingletonDemo@1b6d3586
System.out.println(s2);//edu.yunyu.singleton.SingletonDemo@1b6d3586
System.out.println(s1 == s2);//true
//通过反射的方式直接调用私有构造器
Class<SingletonDemo02> clazz = (Class<SingletonDemo02>) Class.forName("edu.yunyu.singleton.SingletonDemo02");
Constructor<SingletonDemo02> constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);//跳过权限检查
SingletonDemo02 s3 = constructor.newInstance();
SingletonDemo02 s4 = constructor.newInstance();
System.out.println(s3);//edu.yunyu.singleton.SingletonDemo@4554617c
System.out.println(s4);//edu.yunyu.singleton.SingletonDemo@74a14482
System.out.println(s3 == s4);//false
}
}
阻止反射破解
/**
* 懒汉式单例模式(单例对象延迟加载)
* @author huangyzh
* @create 2020-05-13 0:15
*/
public class SingletonDemo06 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo06 instance;
/**
* 私有构造器,阻止反射破解单例
*/
private SingletonDemo06(){
if(instance != null){
throw new RuntimeException();
}
}
//方法同步,调用效率不高,资源利用率高
public static synchronized SingletonDemo06 getInstance(){
if(instance == null){
instance = new SingletonDemo06();
}
return instance;
}
}
2)反序列化方式
反序列化可以破解上面几种实现方式:
反序列化时,如果对象所在类定义了readResolve(),(实际是一种回调),定义返回哪个对象
测试代码
/**
* 测试反序列化破解单例
* @author huangyzh
* @create 2020-05-13 1:25
*/
public class SigletonTest02 {
public static void main(String[] args) throws Exception {
SingletonDemo02 s1 = SingletonDemo02.getInstance();
SingletonDemo02 s2 = SingletonDemo02.getInstance();
System.out.println(s1);//edu.yunyu.singleton.SingletonDemo@1b6d3586
System.out.println(s2);//edu.yunyu.singleton.SingletonDemo@1b6d3586
System.out.println(s1 == s2);//true
//通过反序列化的方式构造多个对象
FileOutputStream fos = new FileOutputStream("D:/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/a.txt"));
SingletonDemo02 s3 = (SingletonDemo02)ois.readObject();
System.out.println(s3);//edu.yunyu.singleton.SingletonDemo@6d03e736
}
}
阻止反序列化破解
/**
* 懒汉式单例模式(单例对象延迟加载)
* 阻止反序列化
* @author huangyzh
* @create 2020-05-13 0:15
*/
public class SingletonDemo07 implements Serializable {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo07 instance;
/**
* 私有构造器,阻止反射破解单例
*/
private SingletonDemo07(){
if(instance != null){
throw new RuntimeException();
}
}
//方法同步,调用效率不高,资源利用率高
public static synchronized SingletonDemo07 getInstance(){
if(instance == null){
instance = new SingletonDemo07();
}
return instance;
}
//反序列化时,如何定义了readResolve()则直接返回此方法指定对象,而不需要单独再创建新对象
private Object readResolve(){
return instance;
}
}
5、枚举方式
package edu.yunyu.singleton;
/**
* 枚举单例(避免反射和反序列化的漏洞,无延时加载)
* @author huangyzh
* @create 2020-05-13 0:35
*/
public enum SingletonDemo05 {
//这个枚举元素,本身就是单例对象
INSTANCE;
public void singletonOperation(){
//添加操作
}
}
优点:
实现简单
枚举本身就是单例模式,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞
缺点:
无延迟加载
多线程环境下五种单例模式的效率测试
/**
* 测试多线程环境下五种创建单例模式的效率
* @author huangyzh
* @create 2020-05-13 1:25
*/
public class SigletonTest03 {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
int threadNum =10;
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
Object o1 = SingletonDemo01.getInstance();
// Object o2 = SingletonDemo02.getInstance();
// Object o3 = SingletonDemo03.getInstance();
// Object o4 = SingletonDemo04.getInstance();
// Object o5 = SingletonDemo05.INSTANCE;
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();//main线程阻塞,直到计算器变为0,才会继续往下执行
long end = System.currentTimeMillis();
System.out.println("总耗时:"+(end-start));
}
}
模式 | 毫秒数 |
---|---|
饿汉式 | 22ms |
懒汉式 | 636ms |
静态内部类式 | 28ms |
枚举式 | 32ms |
双重检测锁式 | 65ms |
关注相对值即可,在不同的环境下不同的程序测试的结果可能完全不一样。