文章概要
1、什么是单例
2、为什么需要单例
3、单例的优点和缺点
4、单例的写法和比较
5、序列化破坏单例
6、反射破坏单例
7、不使用synchronized和lock,如何实现一个线程安全的单例?
8、JDK中的单例
9、引用
1、什么是单例
单例是23中设计模式之一,保证一个类只有一个实例,并且对外提供统一的访问入口。是创建型模式。
2、为什么需要单例
我们都知道,类的对象是由类的构造函数创建的,若构造函数是public的,则外部可以通过构造函数随意创建对象,若想限制类对象的创建,可以将构造函数改为private,至少也要protected。但是要保证类的可用性,就需要对外提供一个方法用于访问该类。
3、单例的优点和缺点
优点:在内存中一个类只有一个实例,避免对象的不断创建和销毁,减少内存开销
缺点:在编写单例代码时需要解决线程安全问题和序列化问题
4、单例的写法和比较
单例的写法分为饿汉式、懒汉式、静态内部类、枚举、
①饿汉式
public class Singleton {
//实例化
private static Singleton instance = new Singleton();
//私有化的构造函数
private Singleton(){}
//对外提供一个统一获取实例的静态方法
public static Singleton getInstance(){
return instance;
}
}
线程安全:安全。因为instance对象是static修饰的,在类第一次被加载时,instance就已经被初始化完成,所以线程安全。
缺点:类第一次被加载时就实例化,若实例用不到,会造成资源的浪费。若类被多次加载,会产生多个实例化对象。
②静态内部类
public class Singleton {
//私有化的构造函数
private Singleton(){}
//静态内部类实例化
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
//对外提供统一方法
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
静态内部类的方式解决了饿汉式资源浪费的问题,当Singleton类第一次被加载时,instance不一定初始化,只有显示调用getInstance()方法时,才会装载SingletonHolder类,进而instance被初始化,这种延迟加载的方式解决了饿汉式浪费资源的问题。
线程安全:安全。与饿汉式一样用static修饰,利用classLoader机制的线程安全性保证此种单例的线程安全。
③懒汉式
public class Singleton {
private static Singleton instance;
//私有化的构造函数
private Singleton(){}
//对外提供统一方法
public static Singleton getInstance(){
if (null == instance){
instance = new Singleton();
}
return instance;
}
}
懒汉式是在对象第一次被使用时初始化instance。
线程安全:不安全。若线程A和线程B同时请求调用getInstance()方法,当A进入if判断,但是没有执行new操作时,B也进行if判断,此时inatance为null,这种情况线程A和线程B会分别new不同的Singleton对象。
④懒汉式改进版一
public class Singleton {
private static Singleton instance;
//私有化的构造函数
private Singleton(){}
//对外提供统一方法
public static synchronized Singleton getInstance(){
if (null == instance){
instance = new Singleton();
}
return instance;
}
}
为了解决懒汉式的线程安全问题,用synchronized修饰方法。
线程安全:安全。
缺点:由于getInstance方法被整个锁住,所以多线程环境下,此方法是串行执行的,执行效率低。
⑤懒汉式改进版二
public class DoubleCheckLazySingleton {
private static DoubleCheckLazySingleton instance;
private DoubleCheckLazySingleton(){};
public static DoubleCheckLazySingleton getInstance(){
if (instance == null){
synchronized (DoubleCheckLazySingleton.class){
if (instance == null){
instance = new DoubleCheckLazySingleton();
}
}
}
return instance;
}
}
双重校验锁懒汉式单例,将synchronized锁范围缩小至初始化代码,从而提高执行效率,采用两次空值判断。
线程安全:不安全。
缺点:JVM可以执行重排序,指令的执行顺序:(1)分配内存给instance对象。(2)初始化instance对象。(3)设置instance对象指向分配内存地址。正常我们认为的执行顺序是(1)(2)(3),由于指令重排序机制,最后执行的顺序可能是(1)(3)(2),此时线程A执行(3),但没有执行(2),线程B进来之后判断instance对象非空(正在执行(2)或者未执行(2)),程序会产生错误。
那么如何解决上面的问题呢?volatile解决线程间的可见性和指令重排序问题。
public class DoubleCheckLazySingleton {
private volatile static DoubleCheckLazySingleton instance;
private DoubleCheckLazySingleton(){};
public static DoubleCheckLazySingleton getInstance(){
if (instance == null){
synchronized (DoubleCheckLazySingleton.class){
if (instance == null){
instance = new DoubleCheckLazySingleton();
}
}
}
return instance;
}
}
将instance变量用volatile修饰之后可解决以上问题。
但是它只是看上去完美无缺,其实还是有问题的。
缺点:序列化问题(后续展开说明)
⑥枚举单例
public enum EnumSingleton {
INSTANCE;
private EnumSingleton(){};
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
这种方式是Effective Java
作者Josh Bloch
提倡的方式。枚举类型单例,即解决了线程安全问题,有解决了序列化问题。
优点:
线程安全
枚举在底层做了线程安全的操作。
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(com/xxx/EnumSingleton, name);
}
private EnumSingleton(String s, int i)
{
super(s, i);
}
public static EnumSingleton getInstance()
{
return INSTANCE;
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
以上代码是枚举类反编译之后的代码,我们发现变成了继承Enum类的final class类,变量instance用static修饰,instance的初始化在static块中,其实就是上面我们讲到的饿汉式单例。所以是线程安全的。
5、序列化破坏单例:
其他的单例模式都会有序列化问题,我们来以双重校验锁单例对象的序列化和反序列化做个测试:
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testFile"));
//写入双重校验锁单例对象
DoubleCheckLazySingleton s1 = DoubleCheckLazySingleton.getInstance();
oos.writeObject(s1);
oos.flush();
oos.close();
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testFile"));
DoubleCheckLazySingleton s2 = (DoubleCheckLazySingleton) ois.readObject();
ois.close();
System.out.println("s1:"+s1);
System.out.println("s2:"+s2);
System.out.println("s1==s2:"+ (s1==s2));
}
}
输出结果:
s1:com.zhangjq.DoubleCheckLazySingleton@14ae5a5
s2:com.zhangjq.DoubleCheckLazySingleton@6d03e736
s1==s2:false
从结果可以看出序列化和反序列化不是同一个对象,那么是什么原因造成的呢?我们来看源码分析:
ObjectInputStream.readObject()
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
在这里调用了readObject0(),在这个方法中可以看到
private Object readObject0(boolean unshared) throws IOException {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
...
}
我们继续进入readOrdinaryObject
方法
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
·········
return obj;
}
可以看到这样一段代码obj = desc.isInstantiable() ? desc.newInstance() : null;
进入isInstantiable
方法,发现就是一个判断是否存在无参构造函数的语句
/** serialization-appropriate constructor, or null if none */
private Constructor<?> cons;
·········
/**
* 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.
*/
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
综合以上所述:我们发现只要序列化的类存在无参构造函数就调用desc.newInstance(),构造一个新的对象返回。
解决方案:
只需要在序列化的类中添加readResolve()
方法即可解决序列化问题
public class DoubleCheckLazySingleton implements Serializable{
private volatile static DoubleCheckLazySingleton instance;
private DoubleCheckLazySingleton(){};
public static DoubleCheckLazySingleton getInstance(){
if (instance == null){
synchronized (DoubleCheckLazySingleton.class){
if (instance == null){
instance = new DoubleCheckLazySingleton();
}
}
}
return instance;
}
private Object readResolve(){
return instance;
}
}
再次运行序列化和反序列化操作,结果如下:
s1:com.zhangjq.DoubleCheckLazySingleton@14ae5a5
s2:com.zhangjq.DoubleCheckLazySingleton@14ae5a5
s1==s2:true
为什么添加readResolve()
方法就可以解决序列化问题呢?我们来看源码:
在readOrdinaryObject()
方法中,判断是否存在无参构造函数之后,又判断了是否存在readResolve()
的方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
hasReadResolveMethod
:
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
若存在则执行hasReadResolveMethod
方法,结果覆盖上面new 出来的对象,返回。
那么readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法
ObjectStreamClass()方法中给 readResolveMethod 进行赋值,来看代码:
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。
通过 JDK 源码分析我们可以看出,虽然,增加 readResolve()方法返回实例,解决了单
例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两
次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就
意味着内存分配开销也就随之增大。
枚举单例序列化
对上述测试代码做修改
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testFile"));
//写入双重校验锁单例对象
// DoubleCheckLazySingleton s1 = DoubleCheckLazySingleton.getInstance();
EnumSingleton s1 = EnumSingleton.getInstance();
oos.writeObject(s1);
oos.flush();
oos.close();
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testFile"));
// DoubleCheckLazySingleton s2 = (DoubleCheckLazySingleton) ois.readObject();
EnumSingleton s2 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println("s1:"+s1);
System.out.println("s2:"+s2);
System.out.println("s1==s2:"+ (s1==s2));
}
}
运行结果:
s1:INSTANCE
s2:INSTANCE
s1==s2:true
没什么任何附加的操作,就可以保证序列化问题,我们来看源码分析:
继续分析ObjectInputStream.readObject()
,在readObject0
方法中
case TC_ENUM:
return checkResolve(readEnum(unshared));
我们看到在 readObject0()中调用了 readEnum()方法,来看 readEnum()中代码实现:
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
可以看出枚举对象反序列化时是通过Enum.valueOf()方法根据名字查找枚举对象 。不存在new新对象的情况,所以枚举单例的反序列化问题是安全的。
6、反射破坏单例
在上述所有的单例模式中,构造方法都是private权限的,很多人以为只要设置成private就是安全的了,其实不然,利用反射机制可以强制访问private的构造方法来创建对象。我们来看一个例子:
public class ReflectSingletonTest {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//以双重校验锁单例为例
Class<?> clazz = DoubleCheckLazySingleton.class;
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
//设置强制访问
declaredConstructor.setAccessible(true);
//初始化
Object o1 = declaredConstructor.newInstance();
Object o2 = declaredConstructor.newInstance();
System.out.println("o1:"+o1);
System.out.println("o2:"+o2);
System.out.println("o1==o2:"+ (o1==o2));
}
}
运行结果:
o1:com.zhangjq.DoubleCheckLazySingleton@1b6d3586
o2:com.zhangjq.DoubleCheckLazySingleton@4554617c
o1==o2:false
解决方案:
针对这种情况,我们可以在构造函数里面加一些判断,来解决反射破坏单例的问题:
public class DoubleCheckLazySingleton implements Serializable{
private volatile static DoubleCheckLazySingleton instance;
private volatile static boolean initialized = false;
private DoubleCheckLazySingleton(){
if(!initialized){
initialized = !initialized;
}else{
throw new RuntimeException("不允许重复创建DoubleCheckLazySingleton对象!");
}
}
public static DoubleCheckLazySingleton getInstance(){
if (instance == null){
synchronized (DoubleCheckLazySingleton.class){
if (instance == null){
instance = new DoubleCheckLazySingleton();
}
}
}
return instance;
}
private Object readResolve(){
return instance;
}
}
再次运行结果如下:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.zhangjq.ReflectSingletonTest.main(ReflectSingletonTest.java:25)
Caused by: java.lang.RuntimeException: 不允许创建多个实例
at com.zhangjq.LazyDoubleCheckSingleton.<init>(LazyDoubleCheckSingleton.java:13)
... 5 more
枚举单例反射问题
那么枚举类型的单例是否存在反射问题呢?
public class ReflectSingletonTest {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//以双重校验锁单例为例
Class<?> clazz = EnumSingleton.class;
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(null);
//设置强制访问
declaredConstructor.setAccessible(true);
//初始化
Object o1 = declaredConstructor.newInstance();
Object o2 = declaredConstructor.newInstance();
System.out.println("o1:"+o1);
System.out.println("o2:"+o2);
System.out.println("o1==o2:"+ (o1==o2));
}
}
输出结果:
Exception in thread "main" java.lang.NoSuchMethodException: com.zhangjq.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.zhangjq.ReflectSingletonTest.main(ReflectSingletonTest.java:13)
异常的意思是没有找到EnumSingleton无参的构造函数。
查看Enum类的源码发现只有一个带参数的构造函数
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
我们将代码修改一下,再次运行:
public class ReflectSingletonTest {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//以双重校验锁单例为例
Class<?> clazz = EnumSingleton.class;
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(String.class,int.class);
//设置强制访问
declaredConstructor.setAccessible(true);
//初始化
Object o1 = declaredConstructor.newInstance("zhang",10);
Object o2 = declaredConstructor.newInstance("wang",11);
System.out.println("o1:"+o1);
System.out.println("o2:"+o2);
System.out.println("o1==o2:"+ (o1==o2));
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.zhangjq.ReflectSingletonTest.main(ReflectSingletonTest.java:19)
报错信息很明显:不能用反射创建枚举类对象。
我们查看newInstance()
方法的源码
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
可以看出当修饰符是枚举类型时,抛出异常,也就是说jdk控制反射不能创建枚举类对象。所以枚举类的反射破坏问题是不存在的。
7、不使用synchronized和lock,如何实现一个线程安全的单例?
以上我们讲的所有单例的线程安全都显示或者隐示的用到了ClassLoader的线程安全机制。
饿汉式:用static修饰,类第一次被加载时实例化,ClassLoader的类加载是synchronized修饰的。
懒汉式:显示的用synchronized修改。
枚举:编译器编译之后枚举中的所有变量都用static final 修饰,并且是初始化在static块中
那么如果我们不用synchronized和lock,如何实现一个线程安全的单例呢?
答案是:CAS
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
实现单例代码:
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
优点:CAS依赖底层硬件的实现,没有线程之间切换和阻塞问题。
缺点:可能一直处于等待中,不断的循环,对CPU造成资源开销。
8、JDK中的单例
java.lang.Runtime
Runtime
类封装了Java运行时的环境。每一个java程序实际上都是启动了一个JVM进程,那么每个JVM进程都是对应这一个Runtime实例,此实例是由JVM为其实例化的。每个 Java 应用程序都有一个 Runtime
类实例,使应用程序能够与其运行的环境相连接。
由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
以上代码为JDK中Runtime
类的部分实现,可以看到,这其实是饿汉式单例模式。在该类第一次被classloader
加载的时候,这个实例就被创建出来了。
一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。
9、引用
参考:
单例与序列化的那些事儿
设计模式(二)——单例模式
不使用synchronized和lock,如何实现一个线程安全的单例?
JDK中的那些单例