标签:
单例模式
最简单但是也挺困难的。
要保证在一个JVM中只能存在一个实例,要考虑到如下的情况:
public class SingletonTester {
public static <T> void checkClassNewInstance(Class<T> c){
try {
T t1 = c.newInstance();
T t2 = c.newInstance();
if(t1 != t2){
System.out.println("Class.newInstance校验失败,可以创建两个实例");
}else{
System.out.println("Class.newInstance校验通过");
}
} catch (Exception e) {
System.out.println("不能用Class.newInstance创建,因此Class.newInstance校验通过");
}
}
public static <T> void checkContructorInstance(Class<T> c){
try {
Constructor<T> ctt = c.getDeclaredConstructor();
ctt.setAccessible(true);
T t1 = ctt.newInstance();
T t2 = ctt.newInstance();
if(t1 != t2){
System.out.println("ContructorInstance校验失败,可以创建两个实例");
}else{
System.out.println("ContructorInstance校验通过");
}
} catch (Exception e) {
System.out.println("不能用反射方式创建,因此ContructorInstance校验通过");
}
}
public static <T> void testSerializable(T t1){
File objectF = new File("/object");
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream(objectF));
out.writeObject(t1);
out.flush();
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(objectF));
T t2 = (T) in.readObject();
in.close();
if(t1 != t2){
System.out.println("Serializable校验失败,可以创建两个实例");
}else{
System.out.println("Serializable校验通过");
}
} catch (Exception e) {
System.out.println("不能用反序列化方式创建,因此Serializable校验通过");
}
}
public static void main(String[] args) {
checkClassNewInstance(Singleton3.class);
checkContructorInstance(Singleton3.class);
testSerializable(Singleton3.getInstance());
}
}
这个工具验证了Class.newInstance攻击,反射攻击,反序列化攻击,能够屏蔽着三种攻击的才是好的单例。
public class Singleton1{
private Singleton1() {
}
private static Singleton1 instance;
public static Singleton1 getInstance(){
if(instance == null){
instance = new Singleton1();
}
return instance;
}
}
最普通懒汉模式的单例, 私有构造器,静态方法获取实例,获取的时候先判空。
测试结果:
不能用Class.newInstance创建,因此Class.newInstance校验通过
ContructorInstance校验失败,可以创建两个实例
不能用反序列化方式创建,因此Serializable校验通过
这个类因为不能被序列化,因此不会受到反序列化攻击
因为私有构造器避免了Class.newInstance
但是会被反射攻击
另外其不是线程安全的
public class Singleton2 {
private static Singleton2 sington = new Singleton2();
private Singleton2(){};
public static Singleton2 getInstance(){
return sington;
}
}
来个典型的饿汉模式的
测试结果:
不能用Class.newInstance创建,因此Class.newInstance校验通过
ContructorInstance校验失败,可以创建两个实例
不能用反序列化方式创建,因此Serializable校验通过
同样不会有反序列化及Class.newInstance的问题。
并且没有并发的问题。
不过其会在不同的时候也初始化一个实例出来。个人感觉实际上影响不大
上面的都会有反射攻击的问题。来解决它。
public class Singleton3 {
private static Singleton3 sington = new Singleton3();
private static int COUNT = 0;
private Singleton3(){
if(++COUNT > 1){
throw new RuntimeException("can not be construt more than once");
}
};
public static Singleton3 getInstance(){
return sington;
}
}
测试结果:
不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
不能用反序列化方式创建,因此Serializable校验通过
通过加入计数器来解决,这样虽然解决了反射攻击,但是却不是线程安全的,另外引入了新的变量也不优雅。下面换个方式:
public abstract class Singleton4 {
private static class SingletonHolder{
private static final Singleton4 INSTANCE = new Singleton4() {
};
}
private Singleton4(){};
public static Singleton4 getInstance(){
return SingletonHolder.INSTANCE;
}
}
这个推荐使用
下面说下不用内部类的懒汉模式
public class Singleton5 {
private static Singleton5 sington = null;
private Singleton5(){};
public static Singleton5 getInstance(){
if(sington == null){ // 1
synchronized (Singleton5.class) {
if(sington == null){ // 2
sington = new Singleton5();
}
}
}
return sington;
}
}
如果没有 //1 的检查,那么所有的getInstance()都会进入锁争夺,会影响性能,因此加入了检查。
此外其会被反射攻击
上面的会有线程安全问题,是由于JVM的重排序机制引起的:
重排序:
JVM在编译的时候会保证单线程模式下的结果是正确的,但是其中代码的顺序可能会进行重排序,或者乱序,主要是为了更好的利用多cpu资源(乱序), 以及更好的利用寄存器,。
比如1 a = 1; b = 2; a=3;三个语句,如果b执行的时候可能会占用a的寄存器位置,JVM可能会把a=3语句提到b=2前面,减少寄存器置换次数。
比如上面的 instance = new Singleton5()这部分代码的伪字节码为:
1. memory = allocate() // 分配内存
2. init(memory) // 初始化对象
3. instance = memory // 实例指向刚才初始化的内存地址。
4. 第一次访问instance
在JVM的时候有可能2.3的位置进行了重新排序,因为JVM只保证构造器执行完之后的结果是正确的,但是执行顺序可能会有变化。 这个时候并发调用getInstance的时候就有可能出现如下的情况:
时间 | 线程A | 线程B |
---|---|---|
t1 | A1:分配对象的内存空间 | |
t2 | A3:设置instance指向内存空间 | |
t3 | B://1 处判断instance是否为空 | |
t4 | B:由于instance不为null,线程B将返回instance引用的对象 | |
t5 | B:instance没有经过初始化,可能会有未知问题 | |
t6 | A2:初始化对象 | |
t7 | A:这是对象才是被初始化的 |
为了解决这个问题,我们可以从两个方向考虑:制止重排序,或者使重排序对其他线程不可见。
制止重排序的方式单例:
使用JDK1.5之后提供的volatile关键字。这个关键字的意义在于保证变量的可见性。保证变量的改变肯定会回写主内存,并且关闭java -server模式下的一些优化,比如重排序:
public abstract class Singleton6 {
private static volatile Singleton6 sington = null;
private Singleton6(){};
public static Singleton6 getInstance(){
if(sington == null){ // 1
synchronized (Singleton6.class) {
if(sington == null){ // 2
sington = new Singleton6(){};;
}
}
}
return sington;
}
}
还可以,但是代码有些长,不如Singleton4
使重排序对其他线程不可见的单例:
public abstract class Singleton7 {
private static Singleton7 sington = null;
private Singleton7(){};
public static Singleton7 getInstance(){
if(sington == null){ // 1
synchronized (Singleton7.class) {
if(sington == null){ // 2
Singleton7 temp = new Singleton7(){};
sington = temp;
}
}
}
return sington;
}
}
另外单例4页是这样的,重排序对其他的线程是不可见的
如果有必要序列化,那么就需要实现Serializable接口,下面说下这种情况如何解决反序列化攻击的问题
public abstract class Singleton8 implements Serializable{
private static class SingletonHolder{
private static final Singleton8 INSTANCE = new Singleton8() {
};
}
private Singleton8(){};
public static Singleton8 getInstance(){
return SingletonHolder.INSTANCE;
}
public Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
测试结果:
不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
Serializable校验通过
这个主要在于方法readResolve, 其返回结果会用来代替反序列化的结果
枚举单例,effectiveJava中推荐的
最后一个了。就是使用枚举单例了。可以看一下,是极好用的
public enum SingleEnum {
INSTANCE;
}
测试结果:
不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
Serializable校验通过
它也成功的避免了各种可能存在的问题:
public abstract class Enum{
private Enum{}
private static Enum INSTANCE = null;
static{
INSTANCE = new Enum(){};
}
}
好了,综上,尽量用枚举单例,或者是Holder单例吧
标签:
原文地址:http://blog.csdn.net/three_man/article/details/45174059