1. 什么是单例模式
1.1 单例模式的概念
单例模式是一种比较常见的设计模式,在整个系统中,单例类只能有一个实例对象,且需要自行完成示例,并始终对外提供同一实例对象。
因为单例模式只允许创建一个单例类的实例对象,避免了频繁地创建对象,所以可以减少GC 的次数,也比较节省内存资源 ,加快对象访问速度。例如,数据库连接池、应用配置等一般都是单例的。在下面介绍的 VFS 中,也涉及单例模式的使用。单例模式有很多种写法,但是有些写法在特定的场景下,尤其是多线程条件下无法满足实现单一实例对象的要求,从而导致错误。
解决的问题:一个全局使用地方类频繁的创建和销毁。
如何解决:判断系统是有已经有这个单例,如果有则返回,没有则创建。
关键设计:构造函数私有,单例类自己创建唯一的实例,提供public 的访问方法。
单例模式的优点:
只生成一个实例,减少的系统的开销,特别是当一个对象的产生需要比较多的资源时。
单例模式可以在系统设置全局的访问点,优化环共享资源访问,方便管理和维护。
1.2 单例模式的几种形式
几种常见的单例实现方式:
使用说明:不建议使用第 1 懒汉式种和第 2种饿汉方式。
饿汉式,是线程安全的,执行效率高,但是不能延时加载。
懒汉式,线程安全,调用效率略低,但是可以延时加载。
第三种方法同步的开销同样比较大。双重检测锁注意使用了两个同步块,在某些情况下会出现异常,使用volite要好一些。只有在要明确实现 lazy loading 效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。
1.2.1 饿汉式(单例对象立即加载)
要点:
构造器私有化。
静态private的实例。
一个public获得实例的方法。
要点:
构造器私有化。
静态private的实例。
一个public获得实例的方法。
public class SingletonDemo1 {
private static SingletonDemo1 singletonDemo1 = new SingletonDemo1();
private SingletonDemo1() {
}
public static SingletonDemo1 getInstance() {
return singletonDemo1;
}
public static void main(String[] args) {
SingletonDemo1 singletonDemo1 = SingletonDemo1.getInstance();
SingletonDemo1 singletonDemo2 = SingletonDemo1.getInstance();
System.out.println(singletonDemo1 == singletonDemo2 );
}
}
说明:static 变量会在类装载时初始化,此时不会涉及到多线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此可以省略synchronized关键字。
问题:如果只是加载本类,而不调用getInstance(),则会造成资源浪费。
1.2.2 懒汉式
公共要点:
私有构造器
成员变量只声明不初始化,静态私有
public static synchronzied 获得实例。
获得实例中如果已经初始化就直接返沪,否则创建。
特点:
实现了单例对象延迟加载;
不会出现饿汉式的资源浪费问题,资源利用率高了。
但是每次获得实例都要先同步,运行效率低。
synchronized方法实现的单例:
public class SingletonDemo2 {
private static SingletonDemo2 singletonDemo2;
private SingletonDemo2() {
}
public static synchronized SingletonDemo2 getInstance() {
if (singletonDemo2 == null) {
singletonDemo2 = new SingletonDemo2();
}
return singletonDemo2;
}
public static void main(String[] args) {
SingletonDemo2 singletonDemo1 = SingletonDemo2.getInstance();
SingletonDemo2 singletonDemo2 = SingletonDemo2.getInstance();
System.out.println(singletonDemo1 == singletonDemo2);
}
}
这种方式的实现也比较简单,问题是每次获得单例都要进行一次加锁操作,效率低。
1.2.3 双重检测锁方式
将同步内容下放到if内部,提高了执行效率,不用每次获取对象时都进行同步,只有第一次才同步,创建之后就没必要了。
实现方式是使用 volatile 关键字来修饰实例。该关键字的作用是当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存,同时还能防止指令重排序,因为创建个对象至少需要三个指令:
分配内存空间
初始化对象
将对象指向刚分配的内存空间
有些编译器会进行优化,2和3在时间维度上可能反了,这样就无法准确创建预期的实例了。
class SingletonDemo3_2 {
//防止指令重排序,因为new一个对象时需要很多条指令,可能存在重排序的问题。
private volatile static SingletonDemo3_2 instance;
private SingletonDemo3_2() {
}
public static SingletonDemo3_2 getInstance() {
//减少拥塞,因为后续判断都要先加锁
if (instance == null) {
synchronized (SingletonDemo3_2.class) {
//判断获得锁之后是否instance仍然为空
if (instance == null)
instance = new SingletonDemo3_2();
}
}
return instance;
}
}
1.2.4 静态内部类实现懒加载
备线程安全,效率和延时三个好处,原因:
初始化外部类的时候不一定立即初始化静态内部类,只有真正去调getInstance的时候才初始化内部类,从而实现类延时加载。
类的加载是天然线程安全的,因此是线程安全的,也就是JVM帮我们实现了线程安全。
定义为final,所以是只读的,谁都不能修改。
要点:
外部类没有static属性,不会像饿汉式那样立即加载对象。
只有真正调用getInstance(),才会加载静态内部类。加载时是线程安全的。instance是static final类型,保证类内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全。
public class SingletonDemo4 {
private static int count = 0;
private SingletonDemo4() {
synchronized (Singleton.class) {
if(count > 0){
throw new RuntimeException("创建了两个实例");
}
count++;
}
}
public static SingletonDemo4 getInstance() {
return SingletonDemoInstance.instance;
}
private static class SingletonDemoInstance {
private static final SingletonDemo4 instance = new SingletonDemo4();
}
private Object readResolve(){
}
}
不足:可以被通过反射或者序列化破坏。
为什么可以被反射破坏:
因为通过setAccessible(true)指令就可以访问到构造方法。
反射能够破坏的不仅仅是静态内部类方式,双重检查锁等方式都会存在问题。
前者需要在构造方法里加判断,或者抛异常。
序列化,也可以破坏除枚举之外的其他单例模式。
后者是再添加一个方法
1.2.5 枚举方式实现
枚举本身就是单例的,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞! 缺点:无延迟加载。
public enum SingletonDemo5 {
INSTANCE;// 本身就是单例的,但是不能延时加载
public void singleOpertation(){
}
}
1.3 几种破解单例模式的方式
枚举都破解不了
1.3.1 反射可以破解上面几种实现方式
public class Test2 {
public static void main(String[] args) throws Exception {
SingletonDemo6 s1 = SingletonDemo6.getInstance();
SingletonDemo6 s2 = SingletonDemo6.getInstance();
System.out.println(s1);
System.out.println(s2);
Class<SingletonDemo6> clazz = (Class<SingletonDemo6>) Class.forName("pattern.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);
}
}
防范措施,修改私有构造函数,如果不为空,则抛出异常
private SingletonDemo6() {
if(singletonDemo2!=null)
throw new RuntimeException();
}
1.3.2 反序列化破解
FileOutputStream fos = new FileOutputStream("/Users/qingchao1/Desktop/workspace/Java_study/JavaSE/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/qingchao1/Desktop/workspace/Java_study/JavaSE/a.txt"));
SingletonDemo6 s3 = (SingletonDemo6) ois.readObject();
System.out.println(s3);
在单例类中实现一下这个函数:
private Object readResolve() throws ObjectStreamException {
return singletonDemo2;
}
1.4 容器式解决大规模生产单例的问题
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getBean(String className) {
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object object = null;
try {
object = Class.forName(className).newInstance();
ioc.put(className, object);
} catch (Exception e) {
e.printStackTrace();
}
return object;
} else {
return ioc.get(className)
}
}
}
}
2.单例模式在VFS中的应用
VFS 表示虚拟文件系统 ( Virtual File System),它用来查找指定路径下的资源。 VFS 是一个抽象类 , MyBatis 中提供了 JBoss6VFS 和 DefaultVFS 两个 VFS 的实现 ,MyBatis 初始化的流程时,会用到这两个扩展点。
VFS 的核心宇段的含义如下:
public abstract class VFS {
private static final Log log = LogFactory.getLog(VFS.class);
/** Singleton instance holder. */
private static class VFSHolder {
static final VFS INSTANCE = createVFS();
}
}
这个用的是懒汉式方式创建的单例模式,不同的是这里是使用了一个方法创建的,而不是简单的new。看这个方法的代码,会发现是用反射来创建的:
static VFS createVFS() {
...
// Try each implementation class until a valid one is found
VFS vfs = null;
for (int i = 0; vfs == null || !vfs.isValid(); i++) {
Class<? extends VFS> impl = impls.get(i);
try {
vfs = impl.getDeclaredConstructor().newInstance();
if (!vfs.isValid()) {
if (log.isDebugEnabled()) {
log.debug("VFS implementation " + impl.getName() +
" is not valid in this environment.");
}
}
}
.....
return vfs;
}
}