单例模式
什么是单例模式?
单例模式算是设计模式入门的最简单的一个模式, 由于 Java
语言的特性(指令重排序), 导致同时也是最难的一个模式。
所幸, 先驱者Joshua Bloch
在Google I/O 2008
上的新书Effective Java
介绍了单例模式的最佳实践。这本神书我还没来的及看, 但在国外好像备受推崇, 所以有机会还是看看。
一个android
应用程序的一个单例模式的类只能有且只有一个实例对象。
通俗的讲,就是构造方法私有化,并在类内创建唯一一个私有的类实例,提供一个用于获取唯一实例的公有方法。
1 | public class A { |
单例模式分为以下两种类型
- 饿汉模式:类加载的时候便进行创建(加载类时较慢,运行时获取对象较快,线程安全)
- 懒汉模式:要使用的时候才进行创建(加载类时较快,运行时获取对象较慢,线程不安全)
饿汉模式
在Java 5 之前的最佳实践
直接在声明的时候初始化, 当类加载时, instance = new A()
就会执行.
1 | public class A { |
Java 5 之后的最佳实践
enum
作为枚举关键字, 很难想像到它和 单例模式
联系到一起。
但是换种思路, 枚举类只有一个元素, 不就是单例模式了吗?
枚举类还可以添加自定义的方法。完全可以当成一个类来使用。
1 | public enum A { |
懒汉模式
要使用的时候才进行创建
静态内部类 (在Java 5 之前的最佳实践)
因为Holder
是静态内部类, 只有getInstance()
方法访问到Holder
, 才会初始化静态内部类的static
的变量.
1 | public class A { |
双重锁+volatile (在Java 5 之后的最佳实践)
双重锁的优化
上面提到,懒汉式是线程不安全的,对于多线程比较陌生的可能不太理解,先看代码
1 | public class A { |
当我们获取调用getInstance()
方法获取A
的实例对象时,如果有多个线程调用getInstance()方法
,就会出现线程不安全,怎么解释呢?
假设有两个线程同时调用了getInstance()
方法.
- 当线程1执行到
if(instance == null)
时, 判断为true
, 进入if
内, 此时线程1时间片结束,CPU
切换到线程2. - 此时线程2执行到
if(instance == null)
, 因为线程1还未来得及执行instance = A()
, 所以if
判断为true
, 进入方法体, 此时线程2时间片结束,CPU
切换到线程1. - 此时线程1执行
instance = A()
,instance
的hashcode
为850
, 然后return
, 执行结束, 切换回线程2. - 此时线程2在
if
内, 执行instance = A()
,instance
的hashcode
为851
, 然后return
.
看到了吗? 这样就创建了两个A
的实例. 说好的单例模式呢?
那么改一下, 加上synchronized
同步一下.
1 | public class A{ |
假设有两个线程同时调用了getInstance()
方法.
- 当线程1执行到
if(instance == null)
时, 判断为true
, 进入if
内, 此时线程1时间片结束,CPU
切换到线程2. - 此时线程2执行到
if(instance == null)
, 因为线程1还未来得及执行instance = A()
, 所以if
判断为true
, 进入方法体, 此时线程2时间片结束,CPU
切换到线程1. - 线程1拿到
A.class
锁, 进入synchronized
, 此时就算切换回线程2, 线程2也会因为拿不到A.class
锁对象, 阻塞在同步代码块外面. - 线程1继续执行
if
, 执行instance = A()
,instance
的hashcode
为850
, 然后return
, 执行结束, 切换回线程2. - 此时线程2拿到
A.class
锁, 进入synchronized
,if
判断为false
, 直接return
.
为什么不把第一个
if
去掉, 直接留个synchronized + if
呢?
因为synchronized
耗时长, 消耗性能, 双重锁只要保证第一次并发不产生多个对象即可.
你以为这样就线程安全了吗! Java
还有个指令重排序的大招等着你呢.
volatile 优化
volatile
关键字用来解决指令重排序在多线程下的问题, 它有两个功能.
- 保证内存可见性
- 防止指令重排序
我们先看内存可见性(其实这已经被synchronized
解决了)Java
内存模型规定, 变量存储在主存中, 每个线程拥有该变量的一个拷贝副本在自己的工作内存中, 线程修改变量是修改自己工作内存中的变量, 而修改完毕后, 会将自己工作内存中的修改后的值回写到主存中.
1 | public class A { |
假设有两个线程同时调用了getInstance()
方法.
- 当线程1执行到
if(instance == null)
时, 判断为true
, 进入if
内, 拿到A.class
锁, 进入synchronized
, 继续执行if
, 执行instance = A()
,instance
的hashcode
为850
, 此时instance
没有回写到主存, 切换回线程2. - 线程2执行到第一个
if
, 因为线程1的值没有回写到主存, 所以还是会进入if
内, 但是被synchronized
阻塞了. - 线程1将变量回写到主存, 并
return
. - 此时线程2拿到
A.class
锁, 进入synchronized
,if
判断为false
, 直接return
.
所以就算没有使用volatile
保证内存可见性, 也不会导致出错.
其实synchronized
就有保证内存可见性的功能.在 java.util.concurrent.locks.Lock 接口的Javadoc中有这样一段话:
All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock
再来看指令重排序instance = A();
这条命令并不是原子性的, 它包含3个操作.
- 在堆内存申请
A
实例的内存空间. - 初始化
A
实例. - 将
instance
变量指向内存中的A
实例的内存空间(执行完这步instance
就为非null
了)
由于JVM
的指令重排序, 步骤123
可能会变成132
, 多线程下会导致出错.
假设有两个线程同时调用了getInstance()
方法. 指令顺序为132
.
- 当线程1执行到
if(instance == null)
时, 判断为true
, 进入if
内, 拿到A.class
锁, 进入synchronized
, 继续执行if
, 执行instance = A()
. - 先在堆内存申请
A
实例的内存空间, 由于指令重排序, 将instance
变量指向没有初始化的, 但是已经申请了的内存空间. 此时线程1时间片结束, CPU切换到线程2. - 线程2执行到第一个
if
, 因为instance
已经指向没有初始化的内存空间, 所以直接return
. 这时调用这个单例, 因为还未初始化, 所以会导致错误出现.
而volatile
可以防止指令重排序, 让指令严格按照123
的顺序执行.
但是, 在Java 5
之前, 因为Java
内存模型的缺陷, volatile
不能解决指令重排序的问题.
所以最佳实践就是
1 | public class A { |
破坏单例模式的攻击
克隆 clone 攻击
要用clone
攻击单例模式只需要两步.
A
类实现Cloneable
接口, 虽然里面啥也没有, 主要时为了解决CloneNotSupportedException
.- 重写
Object
中的protected native Object clone()
方法, 不然外部访问不了.
然后运行main
方法,a1==a2
得到false
.要解决1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class A implements Cloneable {
private volatile static A instance;
private A() {}
public static A getInstance() {
if(instance == null) {
synchronized (A.class) {
if(instance == null) {
instance = new A();
}
}
}
return instance;
}
public Object clone() throws CloneNotSupportedException {
return (A) super.clone();
}
}
public class Main {
public static void main(String[] args) throws Exception {
A a1 = A.getInstance();
A a2 = (A) a1.clone();
System.out.println(a1 == a2); // false
}
}clone
攻击也很简单, 既然是clone
方法的问题, 那我们就直接在clone
方法改动即可.1
2
3
4
5public class A implements Cloneable {
public Object clone() throws CloneNotSupportedException {
return getInstance();
}
}
反射攻击
用constructor.setAccessible(true);
破解私有构造函数.
1 | public class A { |
那我们只能在构造函数体做文章了.
判断如果被初始化过了, 再次初始化则抛出异常即可.
1 | public class A { |
序列化攻击
只要对A
类实现Serializable
接口, 即可进行对象序列化.
1 | public class A implements Serializable { |
要化解序列化攻击, 很简单, 我们先看ois.readObject()
这个方法.
最终调用的是readOrdinaryObject
方法.
1 | public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants { |
我们可以看到desc.isInstantiable()
返回true
之后, 反序列化会通过反射创建一个新的对象.
看下isInstantiable()
的文档.
Returns true if represented class is serializable/externalizable and can be instantiated by the serialization runtime–i.e., if it is externalizable and defines a public no-arg constructor, or if it is non-externalizable and its first non-serializable superclass defines an accessible no-arg constructor. Otherwise, returns false.
也就是说, 满足以下两种情况任意一种, 则返回true
- 类实现了
Externalizable
接口, 并定义了一个无参构造器 - 类没有实现
Externalizable
接口, 它的第一个非Serializable
父类(如Object
)定义了一个无参构造器.
我们继续往下看, 根据方法名, 如果A
类实现了readResolve
方法, 就会调用readResolve
方法, 并返回出去.
1 | public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants { |
所以, 要化解序列化攻击, 只需要写一个readResolve
方法.
1 | public class A implements Serializable { |
究极无敌完全进化完美精华牛逼上天单例模式懒汉版
1 | public class A implements Serializable, Cloneable { |