HeadFirst中对单例模式的定义:单例模式确保一个类只有一个实例,并只提供一个全局访问点。
单例模式的应用:任务管理器、回收站、项目的配置文件、日志文件等等
单例模式的特点:单例模式只有一个实例,减少了系统的开销,当一个对象的产生需要很多资源时,就可以通过在启动时来创建一个实例永久的驻存。
可以在全局设置访问点,优化资源的访问。
单例模式的常见实现方式:
饿汉式:线程安全,效率高,不能延时加载。
懒汉式:线程安全,效率低,可以延时加载。
双重检测锁:线程安全,效率高,可以延时加载,但是只有在java1.5之后才支持,并且由于JVM底层模型的原因容易出问题。
静态内部类:线程安全,效率高,可以延时加载。
枚举:线程安全,效率高,不能延时加载,可以天然的防止反射和反序列化漏洞。
一.饿汉式
1.静态初始化是天然的线程安全的。
2.效率比较高
3.一开始就创建,没有延时加载,如果一直没有用到这个单例对象的话就浪费了资源。
package com.wxisme.singleton;
/**
* 饿汉式单例模式
* @author wxisme
*
*/
public class SingletonOne {
//静态初始化时就new出单例对象
private static final SingletonOne instance = new SingletonOne();
//私有构造器
private SingletonOne() {
}
//返回单例对象
public static SingletonOne getInstance() {
return instance;
}
}
二.懒汉式
1.线程安全,但是效率较低。
2.延时加载,不会造成资源的浪费。
package com.wxisme.singleton;
import java.io.Serializable;
/**
* 懒汉式实现单例模式
* @author wxisme
*
*/
public class SingletonTow implements Serializable {
//在获取的时候创建此处不能加final
private static SingletonTow instance;
private SingletonTow() {
//防止反射破解
/*
if(instance != null) {
throw new RuntimeException();
}
*/
}
//必须要手动加锁,达到线程安全的目的。
public synchronized static SingletonTow getInstance() {
if(instance == null) {
instance = new SingletonTow();
}
return instance;
}
//防止反序列化破解
/*
private Object readResolve() {
return instance;
}
*/
//防止反射破解
/*
private static Class getClass(String classname)
throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = SingletonTow.class.getClassLoader();
return (classLoader.loadClass(classname));
}
*/
}
三.双重检验锁
1.对懒汉式进行改进,只需要在第一次调用getInstance()方法的时候枷锁,提高了效率。
2.由于JVM底层模型问题,这种方式偶尔会出问题。在JDK1.5之后才能支持。
volatile 用来确保线程安全。
package com.wxisme.singleton;
/**
* 双重检验锁实现单例模式
* @author wxisme
*
*/
public class SingletonThree {
//volatile指令关键字确保实例是线程安全的
private volatile static SingletonThree instance;
private SingletonThree() {
}
//双重检验锁实现 线程安全&延时加载
public static SingletonThree getInstance() {
if(instance == null) {
synchronized (SingletonThree.class) {
if(instance == null) {
instance = new SingletonThree();
}
}
}
return instance;
}
}
四.静态内部类
1.静态初始化,天然的线程安全,效率高。
2.实现延时加载
3.但是能用反射机制和序列化破解
package com.wxisme.singleton;
/**
* 静态内部类实现单例模式
* @author wxisme
*
*/
public class SingletonFour {
//静态内部类
private static class Inner {
private static final SingletonFour instance = new SingletonFour();
}
private SingletonFour() {}
//只有显示调用getInstance方法时才会加载内部类
public static final SingletonFour getInstance() {
return Inner.instance;
}
}
五.枚举
1.天然的线程安全,效率高
2.代码简洁。
3.防止反射和反序列化破解
package com.wxisme.singleton;
/**
* 枚举实现单例模式
* @author wxisme
*
*/
public enum SingletonFive {
INSTANCE;
public void getInstance() {
}
}
存在的问题:以上方法中除了枚举的方式之外,都可以通过反射和反序列化的方式来破解。
来破解一下。
package com.wxisme.singleton;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
/**
* 反射和反序列化破解单例模式(除枚举之外的都可以破解)
* @author wxisme
*
*/
@SuppressWarnings("all")
public class SingletonBreak {
/*
* 通过反射破解,以懒汉式为例
* 应对策略:在私有构造器中手动抛出异常
*/
public static void test1() throws Exception {
SingletonTow st1 = SingletonTow.getInstance();
SingletonTow st2 = SingletonTow.getInstance();
System.out.println(st1==st2);
//反射破解
Class<SingletonTow> clazz = (Class<SingletonTow>) Class.forName("com.wxisme.singleton.SingletonTow");
//获取构造器
Constructor<SingletonTow> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);//跳过权限的检查,可以访问私有构造器
SingletonTow st3 = c.newInstance();
SingletonTow st4 = c.newInstance();
System.out.println(st3==st4);
}
/*
* 反序列化破解 以懒汉式为例 (被反序列化的类必须实现Serializable接口)
* 应对策略:在类中定义一个readResolve()方法,当反序列化时直接返回已经存在的对象
*/
public static void test2() throws IOException, ClassNotFoundException {
SingletonTow st1 = SingletonTow.getInstance();
SingletonTow st2 = SingletonTow.getInstance();
System.out.println(st1==st2);
//序列化st1对象
FileOutputStream fos = new FileOutputStream("e:/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(st1);
oos.close();
fos.close();
//反序列化创建对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:/a.txt"));
SingletonTow st3 = (SingletonTow) ois.readObject();
ois.close();
System.out.println(st1==st3);
}
public static void main(String[] args) throws Exception {
test1();
System.out.println("---------------");
test2();
}
}
不过这种破解可以防止,但是比较繁琐。
防止反射破解的方法:
在私有构造器中手动抛出异常
private SingletonTow() {
//防止反射破解
if(instance != null) {
throw new RuntimeException();
}
}
添加一个getClass()方法
private static Class getClass(String classname)
throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = SingletonTow.class.getClassLoader();
return (classLoader.loadClass(classname));
防止反序列化破解的方法:
添加一个readRsolve()方法
private Object readResolve() {
return instance;
}
总结:通过以上可以得出结论:如果需要延时加载,静态内部类好于懒汉式,不需要延时加载则枚举好于饿汉式。双重检验锁是对饿汉式的优化但是不推荐使用。如果没有特别的安全要求静态内部类式是最好的,如果需要还可以防止破解。懒汉式也不错。
PS.在多线程环境下测试每种方式的执行效率。(感谢高淇老师的视频:))
必须在除main线程执行其他所有的线程执行完之后才能计时,用到了CountDownLatch类来控制。
Demo:
package com.wxisme.singleton;
import java.util.concurrent.CountDownLatch;
/**
* 在多线程环境下测试单例模式的效率
* @author wxisme
*
*/
public class TestEfficiency {
public static void main(String[] args) {
long start = System.currentTimeMillis();
int thread = 0;
//内部类方法生命周期和全局变量不一致,需要加final
final CountDownLatch count = new CountDownLatch(thread);
for(int i=0; i<100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for(int j=0; j<10000; j++) {
Object o = SingletonOne.getInstance();
}
count.countDown();//一个线程执行完计数器减一。
}
}).start();
}
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}//阻塞main线程,直到所有的线程执行完,线程计数器减为零。
long end = System.currentTimeMillis();
}
}