Java设计模式 つ 单例模式
定义:确保一个类只有一个实例,并提供全局访问。
实现:私有化构造器、一个私有静态变量、一个公有静态方法。
一、需要注意的点:
要点 / 名称 | 饿汉式 | 普通懒汉式 | 同步懒汉式 | 双重检查锁懒汉式 | 静态内部类 | 枚举 |
---|---|---|---|---|---|---|
要有懒加载 | 否 | 是 | 是 | 是 | 是 | 否 |
线程安全 | 是 | 否 | 是 | 是 | 是 | 是 |
调用效率高 | 是 | 是 | 否 | 是 | 是 | 是 |
防反射漏洞 | 否 | 否 | 否 | 否 | 否 | 是 |
防反序列化漏洞 | 否 | 否 | 否 | 否 | 否 | 是 |
二、单例模式实现方法
1、饿汉式
实现要点:
- 初始化静态对象;
- 私有化构造器;
- 对外提供静态获取方法。
/**
* 单例模式 - 饿汉式
*/
public class Singleton1{
//类初始化时,立即加载对象!类加载时,是天然线程安全的,但没有延时加载优势
private static Singleton1 instance= new Singleton1();
//私有化构造器
private Singleton1(){}
//方法没有同步,调用效率高
public static Singleton1 getIntance(){
return instance;
}
}
2、懒汉式
实现要点:
- 初始化静态对象为null;
- 私有化构造器;
- 对外提供静态方法获取对象,该方法判断一次对象为空就创建对象。
/**
* 单例模式 - 普通懒汉式
*/
public class Singleton2{
//类初始化时,不初始化这个对象(延时加载,真正用的是否再创建)
private static Singleton2 instance = null;
//私有化构造器
private Singleton2(){}
//方法没有同步,调用效率高,只适用于单线程
public static Singleton2 getInstance(){
if(instance==null){
instance = new Singleton2();
}
return instance;
}
}
3、同步懒汉式
实现要点:
- 初始化静态对象为null;
- 私有化构造器;
- 对外提供静态同步方法获取对象,该方法判断一次对象为空就创建对象。
/**
* 单例模式 - 同步懒汉式
*/
public class Singleton3{
//类初始化时,不初始化这个对象(延时加载,真正用的是否再创建)
private static Singleton3 instance = null;
//私有化构造器
private Singleton3(){}
//方法同步,调用效率低
public static synchronized Singleton3 getInstance(){
if(instance==null){
instance = new Singleton3();
}
return instance;
}
}
4、双重检查锁懒汉式
volatile说明:
instance=new Singleton4();
//这句话创建了一个对象,他可以分解成为如下3行代码:
memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
sInstance = memory; // 3.设置sInstance指向刚分配的内存地址
/*
此时对象还没有被初始化上述伪代码中的2和3之间可能会发生重排序,
造成先指向内存地址,但是没有初始化,第二个线程进来不是null,
但是没有初始化,会发生错误。
解决方案:使用volatile定义变量,重排序在多线程环境中将会被禁止
注意:Java1.4及更早的版本,volatile会失效。
*/
实现要点:
- 初始化
volatile
+静态对象为null; - 私有化构造器;
- 对外提供静态方法取获对象,该方法两次判断对象是否为空,第一次为空获取锁,第二为空创建对象
/**
* 单例模式 - 双重检查锁懒汉式
*/
public class Singleton4{
//类初始化时,不初始化这个对象(延时加载,真正用的是否再创建)
private static volatile Singleton4 instance = null;
//私有化构造器
private Singleton4(){}
public static Singleton4 getInstance(){
//创建对象后不需要获得锁,直接返回对象
if(instance==null){
synchronized(Singleton4.class){
if(instance==null){
instance = new Singleton4();
}
}
}
return instance;
}
}
5、静态内部类式
实现要点:
- 私有静态内部类;
- 静态内部类内部初始化静态对象;
- 私有化构造器,对外提供静态方法获取对象。
/**
* 单例模式 - 静态内部类实现方式
*/
public class Singleton5{
//静态内部类
private static class SingletonClassInstance{
private static final Singleton5 instance = new Singleton5();
}
//私有化构造器
private Singleton5(){}
//线程安全,调用效率高,并且实现了延时加载
public static Singleton5 getInstance(){
return SingletonClassInstance.instance;
}
}
6、枚举式
实现要点:
- 直接定义枚举元素
/**
* 单例模式 - 枚举方式(没有延时加载,天然避免反序列化和反射漏洞)
*/
public enum Singleton6{
INSTANCE;
}
三、什么是反射漏洞
答:可以通过反射的方式直接调用私有构造器创建对象,破坏单例模式唯一性。
反射示例演示:
- 通过Class.forName(“包名.类名”)获得字节码文件;
- 通过getDeclaredContructor(null)获得字节码文件无参构造器;
- 通过setAccessible(ture)暴力访问私有构造器;
- 通过NewInstance()创建多个不同的对象。
//反射创建对象
public class Client {
public static void main(String[] args) throws Exception{
//通过反射的方式直接调用私有构造器
Class<Singleton4> clazz = (Class<Singleton4>) Class.forName("com.Singleton.Singleton4");//获得class字节码文件
Constructor<Singleton4> c = clazz.getDeclaredConstructor(null);//获得无参构造器
c.setAccessible(true);//暴力访问
Singleton4 s1 = c.newInstance();
Singleton4 s2 = c.newInstance();
System.out.println(s1==s2);//false
}
}
四、什么是反序列化漏洞
答:可通过反序列化的方式构造多个不同对象,破坏单例模式的唯一性。
第1步、序列化示例演示:
- 序列化前提对象已经实现Serializable接口;
- 创建文件输出流FileOutputStream(“目标路径”);
- 用对象输出流装饰ObjectOutputStream(2);
- 写出序列化对象writeObject(s);
- 关闭文件流、对象流
第2步、反序列化示例演示:
- 创建文件输入流FileInputStream(“目标路径”);
- 用对象输出流装饰ObjectInputStream(1);
- 读取序列化对象readObject();
- 关闭文件流、对象流。
public class Client {
public static void main(String[] args) throws Exception{
//1、序列化
Singleton4 s1 = new Singleton4();
FileOutputStream fos = new FileOutputStream("C:/a.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
//2、反序列化得到对象
FileInputStream fis = new FileInputStream("C:/a.obj")
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton4 s2 = (Singleton4) ois.readObject();
System.out.println(s1==s2);//false
fis.close();
ois.close();
}
}
五、如何避免反射漏洞
答:私有化构造器中增加判断,如果对象不为空,抛出异常
private Singleton(){
if(instance!=null){
throw new RuntimeException();
}
}
六、如何避免反序列化漏洞
答:类中定义私有readResole()方法防止获取不同对象
private Object readResolve() {
return instance;
}
七、多线程测试5种线程安全单例模式,创建对象的效率(相对)
答:
public class Client {
public static void main(String[] args) throws Exception{
long start = System.currentTimeMillis();
int threadNum=100;
//CountDownLatch:同步辅助类,在完成一组正在其它线程中执行的操作前,它允许一个或多个线程一直等待。
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for (int j = 0; j < threadNum; j++) {
new Thread(new Runnable() {
public void run() {
for (int k = 0; k < 1000000; k++) {
Object o = SingletonDemo1.getInstance();
}
//countDown(),当前线程调用此方法,则计数减一(建议放在finally里执行)
countDownLatch.countDown();
}
}).start();
}
//await(),调用此方法会一直阻塞当前线程,知道计时器的值为0
countDownLatch.await();//main线程阻塞,直到计数器变为0,继续往下执行
long end = System.currentTimeMillis();
System.out.println("饿汉式总耗时:"+(end-start));
}
}
//结果1
饿汉式总耗时:23
懒汉式总耗时:912
双重锁总耗时:51
静态类总耗时:81
枚举式总耗时:53
//结果2
饿汉式总耗时:60
懒汉式总耗时:765
双重锁总耗时:94
静态类总耗时:71
枚举式总耗时:54
八、选用原则
答:
1、单例对象,占用资源少,不要延时加载时
枚举式比饿汉式好。
2、单例对象,占用资源大,需要延时加载
静态内部类式比懒汉式好。