2. 单例模式
1. 核心作用
保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
- 某个类只能有一个实例。
- 他必须自行创建这个实例。
- 必须自行向整个系统提供这个实例。
2. 应用场景
- Windows的Task Manager(任务管理器)就是很典型的单例模式
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- 数据库连接池的设计一般食是采用单例模式,因为数据库连接是一种数据库资源。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- Application 也是单例的典型应用(Servlet编程中会涉及到)
- 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
- 在servlet编程中,每个Servlet也是单例
- 在spring MVC框架/struts1框架中,控制器对象也是单例
3. 单例模式的优点
- 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时?如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
- 单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
- 使用场景:1)不要使用单例模式存取全局变量,因为这违背了单例模式的用意,最好将全局变量放到对应类的静态成员。2)不要将数据库连接做成单例,因为一个系统可能会与数据库有多个连接,并且在有连接池的情况下,应当尽可能的及时释放连接。单例模式由于使用静态成员存储类的实例,所以可能会造成资源无法及时释放。
4. 常见的单例模式的实现方法
- 主要:
- 饿汉式(线程安全,调用效率高。但是,不能延时加载。)
- 懒汉式(线程安全,调用效率不高。但是,可以延时加载。)
- 其他:
- 双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题。不建议使用)
- 静态内部类式(线程安全,调用效率高。但是,可以延时加载)
- 枚拳单例(线程安全,调用效率高,不能延时加载)
5. 代码示例
- 饿汉式
/**
* 测试恶汉式单例模式
*/
public class SingletonDemo1 {
public static void main(String[] args) {
// write your code here
}
// 类初始化时,立即加载这个对象(没有延时加载的优势). 在加载类时候是个天然线程安全的,所以不需要添加同步操作关键字
private static SingletonDemo1 instance = new SingletonDemo1();
private SingletonDemo1() {
}
//方法没有同步代码,调用效率高
public static SingletonDemo1 getInstance() {
return instance;
}
}
- 懒汉式实现
public class SingletonDemo2 {
//类初始化时,不初始化该对象,(延时加载,真正用的时候再创建)
private static SingletonDemo2 s;
//私有化构造器
private SingletonDemo2() {
}
//方法同步,调用效率低
public static synchronized SingletonDemo2 getinstance() {
if (s == null) {
s = new SingletonDemo2();
}
return s;
}
}
- 双重检测锁
这个模式将同步内容下放到if内部,提高了执行的效率不必每次获取对象时都进行同步,只有第一次才 同步创建了以后就没必要了。
package cn.zanezz.singleton;
/**
* @program: GOF->SingletonDemo3
* @description: 双重检查锁
* @author: zhaozhen
* @create: 2019-08-21 20:55
**/
public class SingletonDemo3 {
private volatile static SingletonDemo3 instance = null;
public synchronized static SingletonDemo3 getinstance() {
if(instacne == null){
synchronized(SingletonDemo3.class){
if(instance == null){
instance = new SingletonDemo3();
}
}
}
return instance;
}
private SingletonDemo3() {
}
}
-
静态内部类
//外部类没有static属性,则不会像饿汉式那样立即加载对象。
//只有真正调用getinstance(),才会加载静态内部类。加载类时是线程安全的。instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性.
//兼备了并发高效调用和延迟加载的优势!
/**
* @program: GOF->SingletonDemo4
* @description: 静态内部类
* @author: zhaozhen
* @create: 2019-08-21 21:35
*
**/
public class SingletonDemo4 {
private static class SingletonClassInstance{
private static final SingletonDemo4 instance = new SingletonDemo4();
}
public static SingletonDemo4 getInstance(){
return SingletonClassInstance.instance;
}
private SingletonDemo4() {
}
}
枚举方式
优点:
- 实现简单
- 枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
- 无延迟加载
/**
* @program: GOF->SingletonDemo5
* @description: 枚举方式实现
* @author: zhaozhen
* @create: 2019-08-21 21:49
**/
public enum SingletonDemo5 {
/**
* 定义一个枚举元素,代表singleton的一个实例 (没有懒加载的优势)
* 这个枚举元素,本身就是个单例模式
*/
INSTANCE;
/**
* 单例可以有自己的操作
*/
public void singletonOperation() {
}
}
6. 常见的5中单例模式的对比
常见的五种单例模式实现方式
主要:
饿汉式(线程安全,调用效率高。但是,不能延时加载。)
懒汉式(线程安全,调用效率不高。但是,可以延时加载。
其他:
双重检测锁式(由于JVM底层内部模型原区,偶尔会出问题。不建议使用)
静态内部类式(线程安全,调用效率高。但是,可以延时加载)
枚举式(线程安全,调用效率高,不能延时加载,并且可以天然的防止反射和反序列化漏洞)
如何选用?
单例对象占用资源少,不需要延时加载:
枚举式好于饿汉式
单例对象占用资源大,需要延时加载:
静态内部类式好于懒汉式
7. 单例模式存在的问题
使用反射和反序列化可以破坏单例模式的特性,实例如下
public class Client2 {
public static void main(String[] args) throws Exception {
SingletonDemo6 s1 = SingletonDemo6.getInstance();
System.out.println(s1);
SingletonDemo6 s2 = SingletonDemo6.getInstance();
System.out.println(s2);
//通过反射的方式直接调用私有构造器
Class<SingletonDemo6> clazz = (Class<SingletonDemo6>) Class.forName("cn.zanezz.singleton.SingletonDemo6");
Constructor<SingletonDemo6> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
SingletonDemo6 s3 = c.newInstance();
SingletonDemo6 s4 = c.newInstance();
System.out.println(s3);
System.out.println(s4);
//通过序列化的方法来破解单例模式,
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"));
SingletonDemo6 s5 = (SingletonDemo6) ois.readObject();
System.out.println(s5);
}
}
解决方式
package cn.zanezz.singleton;
import javax.management.RuntimeErrorException;
import java.io.ObjectStreamException;
import java.io.Serializable;
/**
* @program: GOF->SingletonDemo5
* @description: 测试懒汉式单例模式(如何防止反射和序列化漏洞)
* @author: zhaozhen
* @create: 2019-08-21 21:49
**/
public class SingletonDemo6 implements Serializable {
private SingletonDemo6() {
//防止通过反射生成新的对象,破坏单例模式的特性
if (singletonDemo6 != null) {
throw new RuntimeException();
}
}
private static SingletonDemo6 singletonDemo6;
public static synchronized SingletonDemo6 getInstance() {
if (singletonDemo6 == null) {
singletonDemo6 = new SingletonDemo6();
}
return singletonDemo6;
}
//反序列化时, 如果我定义了readResolve()则直接返回此方法指定的对象,而不需要单独创建新的对象
private Object readResolve() throws ObjectStreamException {
return singletonDemo6;
}
}
8. 性能测试
/**
* 测试多线程环境下五种创建单例模式的效率
*/
public class Client3 {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
int count = 10;
final CountDownLatch countDownLatch = new CountDownLatch(count);
for (int j = 0; j < 10; j++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
Object o = SingletonDemo2.getinstance();
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await(); // main线程阻塞,直到计数器变为0,才会往下执行
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
单例名称 | 耗费时间 |
---|---|
饿汉式 | 17 ms |
懒汉式 | 65 ms |
双重检查锁 | 19 ms |
静态内部类 | 22 ms |
枚举方式 | 9 ms |