设计模式
单例
一个类只允许有一个对象,建立一个全局的访问点,提供出去供大家使用。
好处:可以让两个对象在完全没有关系的前提下,实现值的传递,降低了耦合性,提高了内聚性。
懒汉
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static Singleton instance;
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉2
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static Singleton instance;
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例,加synchronized关键字实现同步
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
线程安全的懒汉升级版(常用方式)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
双校验原理
这里会发现一个不同寻常的地方:private static volatile Singleton instance;
-
为什么有volatile。
这是因为虚拟机在实现instance = new Singleton();的时候,总共做了一下三个步骤:
- 给instance分配内存空间
- 调用Singleton构造函数来初始化成员变量
- 将instance指向分配的内存空间
然而这三步中的后两步因为JVM的自动优化执行顺序可能是1-2-3也可能是1-3-2。这在单线程下是没有问题的。但是在多线程下,如果第一个线程执行的是1-3-2。当执行到3的时候线程被第二个线程抢走了。那么因为instance已经指向一个内存空间了,非null成立,就直接返回instance了。这样调用的时候就出现异常了(未初始化)。而用了volatile可以避免这种情况。volatile不能保证禁止指令重排,但是可以确保变量赋值过程全部结束才会被正常调用。解决了不满足原子性所带来的问题。
饿汉
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static、final修饰
private static final Singleton instance = new Singleton();
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例
public static Singleton getInstance() {
return instance;
}
}
优质解法——静态内部类《剑指offer》
知识点:内部类是延时加载的,也就是说只会在第一次使用时加载(调用getInstance()的时候加载)。
public class Singleton {
private static class SingletonInner{
private static Singleton singleton=new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonInner.singleton;
}
}
似乎静态内部类已经是最优解了。延迟加载达到了懒汉的空间利用率,还是线程安全的。但是,我们依旧可以使其生成两个实例对象。比如反射攻击或反序列化攻击。
攻击测试
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射攻击
//1.直接通过getInstance()获取对象
Singleton singleton=Singleton.getInstance();
//2.通过反射获取单例类的构造器
Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
//3.设置私有的构造器可使用
constructor.setAccessible(true);
//4.通过构造器new一个实例对象
Singleton newSingleton=constructor.newInstance();
//5.判断两次的实例对象是否相等
System.out.println(singleton==newSingleton);//false
//序列化攻击
//1.直接获取实例对象
Singleton singleton1 = Singleton.getInstance();
//2.序列化写出对象
ObjectOutputStream oop
= new ObjectOutputStream(new FileOutputStream("test.txt"));
oop.writeObject(Singleton.getInstance());
//3.反序列化读回对象
ObjectInputStream objectInputStream
= new ObjectInputStream(new FileInputStream("test.txt"));
Singleton object = (Singleton) objectInputStream.readObject();
//4.判断两次的实例对象是否相等
System.out.println(object == singleton1);//false
oop.close();
objectInputStream.close();
}
以上两种攻击,虽然能破坏静态内部类的单例,但是平时也很少见。所以平时这个方法应该是极其优秀的方法。
最优解——枚举单例《Effective Java》
public enum EnumSingle{
INSTANCE;
public void doSomething(){
System.out.println("do something here");
}
//测试
public static void main(String[] args) {
//获取对象实例并使用
EnumSingle.INSTANCE.doSomething();
}
}
enum保证这个INSTANCE是final static(可以反编译查看一下),同时可以知道虚拟机会保证一个类的*()* 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。
而序列化的时候,枚举只会把这个INSTANCE输出,反序列化的时候再通过这个INSTANCE查找对应的枚举类,所以保证了自由序列化。