Java单例模式
1. 实现单例模式
-
饿汉模式和懒汉模式
单例模式根据实例化时机分为饿汉模式和懒汉模式。
饿汉模式,是指不等到单例真正使用时在去创建,而是在类加载或者系统初始化就创建好。
懒汉模式中单例要等到第一次使用时才创建。 -
饿汉模式
最简单的实现:class Singleton{ private static Singleton instance = new Singleton(); private Singleton(){}; public static Singleton getInstance(){return instance;} }
上面是一种线程安全的实现方式,因为instance是类静态成员,会在类加载并初始化时创建,因此可以保证即便是不同线程也会获得同一份实例(这句话在有些情况下并不正确,比如通过序列化,反射的方式还是能够创建多个实例出来)。
-
懒汉模式
相对于1中在加载的时候就创建,另一种则是在首次使用时创建,比如下面这种方式:
class Singleton{ priavte static Singleton instance = null; private Singleton(){}; public static Singleton getInstance(){ if(null == instance){ instance = new Singleton(); } return instance; } }
上面的这种形式,在首次调用getInstance时才会创建单例,但是它有一个问题就是,在多线程的情况下有可能会创建出多个实例化对象出来:比如线程1和线程2同时判断null == instance为true,结果进入下一步两个线程就创建两个instance出来。当然这种方式通过加锁或则使用synchronize关键字的方式就可以避免了。这里不展示对整个getInstance方法加锁的实现,而是展示另一种方式:
3.1 两次判断,代码如下:
class Singleton{ priavte static volatile Singleton instance = null; private Singleton(){}; public static Singleton getInstance(){ if(null == instance){ synchronize(Singleton.class){ if(null == instance){ instance = new Singleton(); } } } return instance; } }
比起对整个getInstance方法加锁,两次判断的方式可以避免一些不必要的加锁开销。
同时volatile关键字十分必要,多核环境下,多线程分布在多个核上,每个核心拥有各自的cache,读取数据总会尝试从cache读取。那就意味着
instance = new Singleton();
可能不会立即被运行在其他核心上的线程所知,导致即便instance更新后,其他线程cache中instance依然是null。volatile关键字保存每次更新都会更新到内存,同时保存其他核心上该缓存项失效,需要从内存读取。3.2 内部类实现延迟加载
上面两次判断的方法依然是通过加锁的方式来保证多线程情况下的创建单一实例,回顾1的实现中,保证只有一个实例是通过jvm只初始化一次static类成员这一机制实现的,但是1中在Singleton类加载的时候就会实例化静态成员instance,这可不是我们想要的首次使用创建这一目的。为了达到这一目的,我们可以借助内部类的方式实现,下面是代码实现:class Singleton{ private Singleton(){}; private static class SingletonHolder{ priavte static Singleton instance = new Singleton(); } public static Singleton getInstance(){return SingletonHolder.instance;} }
jvm加载Singleton时并不会加载其SingletonHolder,因此instance就不会被早早的创建,直到调用getInstance方法时才回加载SingletonHolder,而instance是其静态成员,jvm保证了它只此一份。
附:关于类的加载时机
「深入理解java虚拟机」一书中有介绍过类什么时候被初始化:
- 创建类的实例时
- 使用Class.forName时
- 访问类的静态成员
- 调用类的静态方法
- 子类初始化时,父类也会初始化
2.实现单例模式的问题
在java中创建一个对象,我们可以通过:new,clone,序列化,反射。上面单例模式的实现我们通过将构造函数私有化使得不能通过new来创建对象,但是其他的手段依然可以,下面举例说明:
-
反射
通过反射我们可以访问类的私有构造函授,测试代码如下(单例代码见上面1):public class TestSingleton { public static void main(String args[]){ try { Constructor cons = Singleton.class.getDeclaredConstructor(); cons.setAccessible(true); Singleton instance1 = Singleton.getInstance(); Singleton instance2 = (Singleton)cons.newInstance(); System.out.println("instance1 == instance2 ?"+(instance1 == instance2)); } catch (Exception e) { e.printStackTrace(); } } }
打印的结果如下:
instance1 == instance2 ? false
instance1和instance2是不同对象,因此这就破坏了单例模式,网上提供解决反射带来的问题也十分简单,只需要修改构造函数,使得它第二次以及更多次的调用抛出异常,修改构造函数如下:private static boolean flag = false; public Singleton(){ if(false == flag){ flag = true; }else{ throw new Exception(...); } }
不过java的反射有点没节操,你还是可以修改flag值,我的天。
在《effective java》里提供一种解决之道,可以无视反射,那就是通过枚举来实现。像下面这样:public enum Singleton3 { INSTANCE; public void applaud(){ System.out.println("haha, go home,reflection!"); } }
没有构造函数了。。。(跟jvm初始化枚举变量的方式有关系,当你再试图通过反射获取构造函数会抛出异常),所以再尝试通过反射获得构造函数,就会抛异常。
-
序列化的影响
不考虑枚举实现单例模式,如果Singleton实现了Serializable接口,那么如果我们将Singleton序列到一个对象中去,在反序列化出来,就会导致不同的实例,请看下面代码:public class TestSingleton2 { public static void main(String []args){ try { Singleton instance = Singleton.getInstance(); //将instance序列化到文件singleton中. FileOutputStream fos = new FileOutputStream("singleton"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance); //从文件singleton中读出对象 FileInputStream fis = new FileInputStream("singleton"); ObjectInputStream ois = new ObjectInputStream(fis); Singleton instance1 = (Singleton)ois.readObject(); System.out.println("instance == instance1 ? " + (instance == instance1)); } catch (Exception e) { e.printStackTrace(); } } }
结果显示instance和instance1为两个实例。
序列化前后产生不同对象,解决方法也很简单,jvm在反序列化时,如果该类实现的下面方法:
private Object readResolve() throw IOException
那么就会调用这个方法返回对象,以替换流中对象。因此可以在这个方法里返回Singleton的instance成员,如下:
private Object readResolve() throws ObjectStreamException{
return instance;
}