单例模式
单例模式(Singleton Pattern)
属于创建型模式中的一种常用的软件设计模式,Java中单例模式的定义为:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
也就是说某一个类负责创建自己的对象,并且保证创建这一次,这个对象成为了系统中唯一实例,该类还要提供一个访问该实例的全局访问点。
常见的应用
-
数据库的连接池
-
Windows的任务管理器(Task Manager)
-
在Servlet编程中的每一个servlet
-
在Spring框架中的每个Bean默认都是单例的(便于Spring容器管理)
-
一般来讲在项目中读取配置文件的类也是单例的
-
操作系统的文件系统也是单例的
优点
由于单例模式只生成一个实例,减少了系统性能的开销,当一个对象的创建需要比较多的资源是,比如读取配置等,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
常见的五种单例模式实现方式
主要:
-
饿汉式(线程安全,调用效率高,不能延时加载)
-
懒汉式(线程安全,调用效率不高,能延时加载)
扩展:
-
双重检测锁式(由于JVM底层内部模型的原因,偶尔会出问题,不建议使用)
-
静态内部类式(线程安全,调用效率高,能延时加载)
-
枚举单例式(线程安全,调用效率高,不能延时加载,并且可以天然的防止反射和反序列化漏洞)
1.饿汉式
static变量在类装载时初始化,此时不会涉及多个线程对象访问该对象的问题,虚拟机只装载一次该类,肯定不会发生并发访问的问题,因此可以省略synchronized关键字,但是会出现一个问题,就是该初始化的实例存在永远没有调用的情况,这种情况就会造成资源浪费。以空间换时间,不存在线程安全问题。
public class SingletonDemo1 {
//类初始化时,立即加载这个对象(不管后面是否会用,没有延时加载的优势)。加载类时,天然时线程安全的
private static SingletonDemo1 instance = new SingletonDemo1();
//私有化构造器
private SingletonDemo1(){
}
//方法不加synchronized没有同步,调用效率高
public static SingletonDemo1 getInstance() {
return instance;
}
}
2.懒汉式
顾名思义,比较懒,在类初始化的时候,不会初始化该实例,等到真正用的时候才会去检查有没有实例,如果有则返回,没有则创建。以时间换空间,在多线程环境下存在风险。
public class SingletonDemo2 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo2 instance;
//私有化构造器
private SingletonDemo2(){
}
//方法加synchronized,同步,调用效率低
public static synchronized SingletonDemo2 getInstance() {
if(instance==null) {
instance = new SingletonDemo2();
}
return instance;
}
}
3.双重检测锁式
这个模式是先不同步,进入getInstance()方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检测。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检测。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。由于编译器优化原因和JVM底层内部模型原因可能出现指令重排,偶尔会出现问题,不建议使用。
public class SingletonDemo3 {
private static SingletonDemo3 instance;
private SingletonDemo3() {
}
public static SingletonDemo3 getInstance(){
if(instance==null) {
SingletonDemo3 sc;
//第一重检测锁
synchronized (SingletonDemo3.class) {
sc=instance;
if (sc==null) {
//第二重检测锁
synchronized (SingletonDemo3.class) {
if (sc==null) {
sc=new SingletonDemo3();
}
}
instance = sc;
}
}
}
return instance;
}
}
4.静态内部类式
外部类没有static属性,加载时不会像饿汉式那样立即加载对象,故不占内存。只有真正调用getInstance()方法,才会加载静态内部类,instance的类型为static final类型,保证了内存中只有这一个实例存在,而且只能被赋值一次,保证了线程的安全,同时具有并发高效调用和延迟加载的双重优势。
public class SingletonDemo4 {
//静态内部类
private static class SingletonClassInstance{
private static final SingletonDemo4 instance = new SingletonDemo4();
}
private SingletonDemo4() {
}
//方法没有同步,调用效率高
public static SingletonDemo4 getInstance() {
return SingletonClassInstance.instance;
}
}
5.枚举单例式
枚举本身就是单例模式,由JVM从根本上提供保障!避免通过反射和反序列化的漏洞,天然的线程安全。
public enum SingletonDemo5 {
//这个枚举元素,本身就是单例对象
INSTANCE;
//添加自己的操作
public void singletonOperation() {
}
}
多线程环境下效率测试
相对值:(在不同的环境下值不一样)
方式 | 耗费时间 |
---|---|
饿汉式 | 20ms |
懒汉式 | 178ms |
双重检测锁式 | 23ms |
静态内部类式 | 57ms |
枚举单例式 | 18ms |
public class ClientSingleton {
public static void main(String[] args) throws InterruptedException {
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 j = 0; j < 1000000; j++) {
Object object = SingletonDemo4.getInstance();
//Object object = SingletonDemo5.INSTANCE;
}
//当前线程调用此方法,计数减一
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();//main线程阻塞,直到计数器变为0,才会继续往下执行
long end = System.currentTimeMillis();
System.out.println("总耗时:"+(end-start));
}
}
单例模式的问题
通过反射或者反序列化可以破解上面除枚举单例式的其他几种实现方式。
反射
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。
public class ClientSingleton2{
public static void main(String[] args) throws Exception {
SingletonDemo6 s1 = SingletonDemo6.getInstance();
SingletonDemo6 s2 = SingletonDemo6.getInstance();
System.out.println("对象s1:"+s1);
System.out.println("对象s1:"+s2);
//通过反射的方式直接调用私有构造器
Class<SingletonDemo6> clazz = (Class<SingletonDemo6>) Class.forName("com.java.design_patterns.SingletonDemo6");
Constructor<SingletonDemo6> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
SingletonDemo6 s3 = c.newInstance();
System.out.println("反射后的对象s3:"+s3);
}
}
输出
反序列化
要知道什么是反序列化,我们的先了解什么是序列化。
- 序列化:把对象转换为字节序列存储于磁盘或者进行网络传输的过程称为对象的序列化。
- 反序列化:把磁盘或网络节点上的字节序列恢复到对象的过程称为对象的反序列化。
public class ClientSingleton2{
public static void main(String[] args) throws Exception {
SingletonDemo6 s1 = SingletonDemo6.getInstance();
SingletonDemo6 s2 = SingletonDemo6.getInstance();
System.out.println("对象s1:"+s1);
System.out.println("对象s1:"+s2);
//通过反序列化的方式构造多个对象
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 s3 = (SingletonDemo6) ois.readObject();
System.out.println("反序列化后的对象s3:"+s3);
}
}
输出
破解反射与反序列化
public class SingletonDemo6 implements Serializable {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo6 instance;
//私有化构造器
private SingletonDemo6(){
//破解反射
if (instance!=null) {
throw new RuntimeException();
}
}
//方法加synchronized,同步,调用效率低
public static synchronized SingletonDemo6 getInstance() {
if(instance==null) {
instance = new SingletonDemo6();
}
return instance;
}
//反序列化时,如果定义了readResolve()则直接反回此方法指定的对象,而不需要单独创建新对象
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
如何选用
-
占用资源少,不需要延时加载:选用饿汉式或者枚举式,一般情况下枚举式好于饿汉式
-
占用资源大,需要延时加载:选用懒汉式或者静态内部类式,一般情况下静态内部类好于懒汉式
-
双重检测锁式由于编译器优化原因和JVM底层内部模型原因可能出现指令重排,偶尔会出现问题,不建议使用。