Kele0 2020-05-30
一、什么是单例模式
单例模式(singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。
二、单例模式的使用场景
应用程序日志
应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
Web应用的配置文件
Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
数据库连接池
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
多线程的线程池
多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
总结:
单例模式应用的场景一般发现在以下条件下:
三、单例模式的写法
1、饿汉式
public final class Singleton { private Singleton(){} private static final Singleton INSTANCE = new Singleton(); public static Singleton getInstance(){ return INSTANCE; } }
饿汉式的特点是类在加载完成时就完成了实例化,避免了多线程的同步问题。但是缺点是:如果该类的对象一直没有被使用,就会导致内存的浪费。
2、懒汉式
public final class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
这种写法是双重检查的懒汉式,为什么要做两次 instance 是否为空的判断呢?这是为了线程安全来考虑的,如果 A、B 两个线程同时运行到第一个判断,都通过第一个 if 判断进入if 代码块,然后 A 线程抢到锁,创建了一个实例,A 线程释放锁。然后 B 线程再抢到锁,如果没有第二步判断的话,B 线程也会创建一个实例,这时候就创建了两个实例,不符合单例模式的定义了。
还有个问题:既然 synchronized 已经保证了线程安全,为什么在定义 instance 时还要加上 volatile 关键字?其实这是起到一个优化作用,synchronized 代码块只有执行完才会同步到主内存,那么比如说 instance 刚创建完成(不为空),但还没有跳出 synchronized 代码块,此时又有10000 个线程调用 getInstance() 方法,那么如果没有 volatile 关键字,此时 instance 在主内存中仍然为空,这一万个线程仍然能通过第一次判断,进入 synchronized 代码块前进行等待,正是有了volatile关键字,一旦 instance 发生改变,那么便会同步到主内存,即使没有出 synchronized 代码块,instance 仍然同步到了主内存,新加入的线程调用 getInstance() 方法时都通过不了第一个判断,也就避免了这些新加入的线程去竞争锁。
3、静态内部类
public final class Singleton { private Singleton() {} private static class ClassHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return ClassHolder.INSTANCE; } }
这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被加载时并不会立即实例化,而是在需要实例化时,调用 getInstance() 方法,才会加载 ClassHolder这个内部类,从而完成对象的实例化。
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了 ClassHolder 中的对象只会被实例化一次,并且这个过程也是线程安全的。
4、枚举
public enum Singleton { INSTANCE; }
这种写法在《Effective JAVA》中大为推崇,它可以解决两个问题: