定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例再众多的设计模式中应该算是运用最频繁的了,通过定义我们知道,单例就是在整个系统中单例类只有一个对象。单例模式的写法很多,下面来学习下他们。
懒汉式
public class LazySingleton {
private static LazySingleton instance;
//1
private LazySingleton() {};
public static LazySingleton getInstance() {
if(instance==null) {
instance=new LazySingleton();
}
return instance;
}
}
懒汉顾名思义就是懒,在我们需要对象时它才创建,不需要就不创建。在注释1处通过将构造方法私有化防止外界调用new关键字创建LazySingleton对象,只给外界提供一个getInstance方法获取。这种方式编写的单例适合单线程,在多线程情况下就无法保证只创建一个对象了,比如有线程1和线程2,线程1进入if判断了但是还没创建对象此时instance依旧为null,这时线程2如果也进来了就会导致创建两个LazySingleton对象。
DCL(双重检查锁定)
通过DCL方式编写的单例也是延迟创建的。
public class DCLSingleton {
private static DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
//1
if(instance==null) {
synchronized (DCLSingleton.class) {
//2
if(instance==null) {
instance=new DCLSingleton();
}
}
}
return instance;
}
}
这种方式编写的单例可能是我们最常遇见的,通过注释1的判断,如果有就直接返回,没有的话当前线程就先锁定单例类然后再创建。注释1 的判断解决了不必要的锁操作,如果没有注释1的判断每次获取都先锁定当前类然后再释放锁,性能不好。锁定当前类和注释2的判断很好的解决了多线程获取单例创建多个对象的情况,但是这种单例还是有bug存在(DCL失效),不过不易发现。
在分析DCL失效之前需要大致了解下对象的创建过程,大致上可以分为3步骤。
a、给对象分配内存空间。
b、初始化这个对象(对对象所在内存空间进行初始化)。
c、让对象的引用指向这块内存空间。
大致就是这三步,但是由于重排序的作用(即时编译器的重排序、处理器的乱序处理、内存系统重排序),步骤b、c的顺序可能不一致,有可能是a、b、c也有可能是a、c、b,如果是a、c、b就可能导致DCL失效。想象一下,当前有两个线程来调用getInstance获取单例对象,线程0先执行创建对象,给对象分配内存,让对象引用指向这块内存(注意此时instance!=null了)但还没初始化这个对象,这时线程1进来了再注释1处判断instance是否等于null,肯定不等于了,线程1拿到这个还没初始化的对象去使用肯定会有问题。
虽然有这个问题存在,但可以通过volatile关键字解决,因为不管线程间怎么切换都会遵守:**volatile字段的写操作happens-before之后(时钟顺序先后)对同一字段的读操作。**对于volatile字段的访问会被插入内存屏障,内存屏障会限制即时编译器的重排序操作。
public class DCLSingleton {
//在这加了volatile关键字
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if(instance==null) {
synchronized (DCLSingleton.class) {
if(instance==null) {
instance=new DCLSingleton();
}
}
}
return instance;
}
}
现在看起来一切都很完美了,不过这种单例还是不安全的,它可以被反射、序列号反序列号破坏。
通过反射破坏单例
public class Test {
public static void main(String[] args) throws Exception{
//通过getInstance
DCLSingleton s1=DCLSingleton.getInstance();
DCLSingleton s2=DCLSingleton.getInstance();
//通过反射破坏单例
Class cls=DCLSingleton.class;
Constructor c=cls.getDeclaredConstructor(null);
c.setAccessible(true);
DCLSingleton s3=(DCLSingleton)c.newInstance(null);
System.out.println("s1=="+s1); //s1==Singleton.DCLSingleton@15db9742
System.out.println("s2=="+s2); //s2==Singleton.DCLSingleton@15db9742
System.out.println("s3=="+s3); //s3==Singleton.DCLSingleton@6d06d69c
}
}
很明显通过DCLSingleton.getInstance()获取的是同一个对象,但是通过反射竟然可以很暴力的创建对象?~~~
序列化反序列化破坏单例
首先单例类需要实现Serializable接口才能进行序列化。
public class Test {
public static void main(String[] args) throws Exception{
DCLSingleton s1=DCLSingleton.getInstance();
//写入
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(new File("mao.txt")));
oos.writeObject(s1);
//读出
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(new File("mao.txt")));
DCLSingleton s2=(DCLSingleton) ois.readObject();
System.out.println("s1==="+s1);//s1===Singleton.DCLSingleton@5c647e05
System.out.println("s2==="+s2);//s2===Singleton.DCLSingleton@4c873330
}
}
可以发现s1,s2不是同一个对象,序列化反序列化确实破坏了单例。虽然序列化反序列话可以破坏单例,但还是有防护方法的。只需要再单例类中加一个方法就好了。
public class DCLSingleton implements Serializable{
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if(instance==null) {
synchronized (DCLSingleton.class) {
if(instance==null) {
instance=new DCLSingleton();
}
}
}
return instance;
}
//增加readResolve方法
private Object readResolve() {
return instance;
}
}
现在再对这个单例类做序列化操作,反序列化出来的就是同一个对象。那么我们是否可以对反射进行防护呢?答案是可以的,后面再说。
通过静态内部类编写单例
public class StaticInnerSingleton implements Serializable{
private StaticInnerSingleton() {};
private static class Inner{
private static StaticInnerSingleton instance=new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance() {
return Inner.instance;
}
}
通过静态内部类的方式编写单例的好处是:首先对象是延迟初始化的,我们用的时候再创建。其次,我们不需要加任何的同步也是线程安全的,因为类的加载过程就是线程安全的。不过静态内部类的方式编写单例还是不能天然的解决反射和序列化反序列化破坏单例的情况。
上面的单例其实都是延迟初始化的,下面再来看非延迟初始化的单例。
饿汉式
public class HungerSingleton {
private HungerSingleton() {};
private static final HungerSingleton instance=new HungerSingleton();
public static HungerSingleton getInstance() {
return instance;
}
}
饿汉顾名思义,不管我们需不需要在类加载过程就会把对象创建出来了,这种方式的好处是不用额外的同步手段。缺点是即使我们不用它还是会创建单例对象,此外,它也不能天然的防止反射、序列化反序列化破坏单例。现在来看看如何防止反射破环单例。
public class HungerSingleton {
//1
private HungerSingleton() {
if(instance!=null) {
throw new RuntimeException("不能反射获取对象");
}
};
private static final HungerSingleton instance=new HungerSingleton();
public static HungerSingleton getInstance() {
return instance;
}
}
可以发现只在私有构造器加了一个判断。这样当反射获取对象就会抛异常。这种防止反射的方式对于饿汉式很好用,不过对于懒汉式就无法保证了,因为饿汉肯定先在类加载时创建对象,这样再通过反射访问构造器instance肯定就不为空。不过懒汉就不行了,懒汉是延迟初始化的,在类加载阶段没有创建对象,如果先调用了反射此时instance是null因此可以创建对象,再调用getInstance方法获取对象,就会产生两个单例对象。
通过枚举创建单例
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance() {
return INSTANCE;
}
public void doSomeThings() {
System.out.println("doSomeThings");
}
}
通过枚举类来创建单例很简单,并且它可以天然的防止反射、序列化反序列化对单例的破坏。
单例的写法有很多,很难说谁好谁坏,需要根据我们的需求来选择最合适的。

124

被折叠的 条评论
为什么被折叠?



