众所周知,单例模式分为饿汉式和懒汉式,昨天在看了《spring5核心原理与30个类手写实战》之后才知道饿汉式有很多种写法,分别适用于不同场景,避免反射,线程不安全问题。下面就各种场景、采用的方式及其优缺点介绍。
饿汉式 (绝对的线程安全)
代码示例
1.第一种写法 ( 定义即初始化)
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- 第二种写法 (静态代码块)
public class Singleton{
private static final Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
饿汉式基本上就这两种写法。在spring框架中IoC的ApplicantsContext
就是使用的饿汉式单例,保证了全局只有一个ApplicationContext
,在应用启动后就能获取实例,以便于进行接下来的操作.
优点
因其在程序启动后就已经初始化,也不需要任何锁保证线程安全 ,所以执行效率高
缺点
因为在程序启动后就已经进行了初始化,即便是不用也进行了初始化,所以无论何时都占用内存空间,浪费了内存空间。
懒汉式 (线程安全需要另外的操作)
代码示例
-
第一种写法
public class Singleton{
private static final Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
上面的代码不难看出,在单线程下执行是没有问题的,但在多线程情况下,线程执行速度和顺序无法控制确定,故有可能会产生多个实例对象,这样就违背了单例模式的初衷了。
-
第二种写法
加锁保证线程安全(
synchronized
关键字)
public class Singleton{
private static final Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
可以看到在getInstance()
上加了synchronized
关键字,就能保证线程同步。但又有一个问题:使用synchronized关键字是,当一个线程调用获取实例的方法时,会锁住整个类,其他的线程再调用,会使线程状态由 RUNNING 变成 MONITOR ,进而导致线程阻塞,执行效率下降;知道这个线程执行完实例方法,其他线程才能继续执行,两个线程时,效率下降还在可以接受范围内,但在实际应用场景中,使用线程池来管理线程的调度,会有大量的线程,如果这些线程都阻塞了,其结果可以预见。
上述问题有什么更好的问题解决呢?使用双重检查锁机制可以完美的解决这个问题。其代码如下
public class Singleton{
private volatile static final Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null){
synchronized(Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里需要解释下,童鞋们都知道一个对象使用要经历一下步骤:
为对象分配内存
初始化对象
-
实例对象指向第一布分配的内存地址
在java中JVM为了提高执行效率,会进行指令重排。那什么时指令重排呢?指令重排是指JVM为了优化指令,提高程序的运行效率,在不影响单线程执行结果的情况下,进行指令重排序,以期提高并行度。
有上述可以指令重排在单线程情况下,对程序的执行不会产生影响,但在多线程情况下就不一定了。所以上述过程的执行顺序可能发生变化,进而导致程序并不会按照预想的执行。
为解决上述问题以及保证并发编程的正确性,java中定义了 **happens-before**原则。在 《JSR-133:Java Memory Model and Thread Specification》 书中关于happens-before定义是这样的:
1.如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2.两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法。
在Java中 为 避免指令重排出现,引入了volatile 关键字。正如你所看到那样在实例对象前就能保证执行结果的正确性。当一个线程调用` getInstance()` 方法时,执行到synchronized关键字时就会上锁,其他线程也调用时就会发生阻塞,当然这种阻塞不是锁住整个类,而是仅仅锁住了方法。如过方法中的逻辑不是太复杂的话,对于外界来说是感知不到的。
这种方法终归还是要加锁的,只要加锁就会对程序性能产生影响。有什么解决办法可以实现不加锁,又能保证线程安全呢?
内部类:是指 一个类定义在另一个类里面或者一个方法里面 的类。有以下特点:
- 隐藏机制:内部封装性好,即便是同一个包下的类也不能直接访问
- 内部类可以访问外围类的私有数据
- 内部类对象可以不依赖外部实例被实例化
静态内部类:顾名思义 就是在内部类上加个static关键字 ,其特点有:
- 可以访问外部类静态成员
- 可以定义静态成员,非静态内部类不可以
静态内部类在载入Java的时候默认不加载,只有调用时进行加载。根据此特点双锁检查机制的单例模式可以改进使用静态内部类。
-
使用静态内部类
代码示例
public class Singleton{
private Singleton() {}
public static Singleton getInstance() {
return SingletonIner.instance;
}
//static是为了单例内存共享,保证这个方法不会被重写,重载
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
}
上述方法及解决了饿汉式的内存浪费问题,又解决了懒汉式的锁的性能问题。
进一步思考
反射破坏单例
大家都知道在Java的各个框架中因为要实现某种功能,不可避免的使用到反射。反射有破坏封装性和性能低下的问题。在这里不考虑性能,只考虑封装性被破坏的问题。调用者使用反射,破坏了封装性,进而使实例有可能不止一个,这样就违背了使用单例模式的初衷。
如何解决呢?很简单,就是在创建另外的对象抛出异常,警告调用者,使其按照我们预想的方式进行调用。
代码示例
public class Singleton{
private Singleton() {
if(SingletonIner.instance!=null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static Singleton getInstance() {
return SingletonIner.instance;
}
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
}
上面代码可以使调用者按照我们的想法使用。
序列化破坏单例
在实际应用中,为保存对象到磁盘或其他的存储介质,不可避免的要使用序列化。一个单例创建好之后,将其序列化保存在磁盘上,下次使用时在反序列化取出放到内存中使用。反序列化后的对象会重新分配内存,即重新创建,这样就违反了单例模式的初衷。以使用静态内部类的代码为我们单例模式类,下面进行简单测试。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) {
Singleton s1=null;
Singleton s2 = Singleton.getInstance();
FileOutputStream fos = null;
try {
fos=new FileOutputStream("singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis =new FileInputStream("singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (Singleton)ois.readObject();
ois.close();
System.out.println(s1==s2)
}
catch(Exception e){
e.printStackTrace();
}
}
}
上面代码运行后发现,输出竟然时false,这就说明反序列化后和序列话前的对象不是同一个,实例化了两次,根本不符合单例模式的原则。
如何改进呢? 改进 的方法也很简单就是增加readResolve()
方法就可以。下面看代码
import java.io.Serializable;
public class Singleton implements Serializable{
private Singleton() {
if(SingletonIner.instance!=null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static Singleton getInstance() {
return SingletonIner.instance;
}
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
private Object readResolve() {
return SingletonIner.instance;
}
}
深究一下,为什么会这样呢?下面我们来看看ObjectInputStream
里的readObject()
方法一探究竟。代码如下:
/**
* Read an object from the ObjectInputStream. The class of the object, the
* signature of the class, and the values of the non-transient and
* non-static fields of the class and all of its supertypes are read.
* Default deserializing for a class can be overridden using the writeObject
* and readObject methods. Objects referenced by this object are read
* transitively so that a complete equivalent graph of objects is
* reconstructed by readObject.
*
* The root object is completely restored when all of its fields and the
* objects it references are completely restored. At this point the object
* validation callbacks are executed in order based on their registered
* priorities. The callbacks are registered by objects (in the readObject
* special methods) as they are individually restored.
*
*
Exceptions are thrown for problems with the InputStream and for
* classes that should not be deserialized. All exceptions are fatal to
* the InputStream and leave it in an indeterminate state; it is up to the
* caller to ignore or recover the stream state.
*
* @throws ClassNotFoundException Class of a serialized object cannot be
* found.
* @throws InvalidClassException Something is wrong with a class used by
* serialization.
* @throws StreamCorruptedException Control information in the
* stream is inconsistent.
* @throws OptionalDataException Primitive data was found in the
* stream instead of objects.
* @throws IOException Any of the usual Input/Output related exceptions.
*/
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();
}
}
}
根据注释,我们知道readObject()
方法读取一个对象的类,类的签名以及该类机器所有超类的非瞬时和非静态的值。我们看到在try后面又调用了重写的readObject0()
方法,其代码如下:
/**
* Underlying readObject implementation.
*/
private Object readObject0(boolean unshared) throws IOException {
.......
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
.......
}
因篇幅的问题我省略了不重要的代码。
由上面看到,在TC_OBJECT处又调用了readOrdinaryObject()
方法,其源码如下:
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
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);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
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);
}
}
return obj;
}
由上述代码可知,由调用了ObjectStreamClass
的isInstanctiable()
方法,方法体非常简单,源码如下 :
/**
* 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);
}
其作用就是构造方法是否为空,构造方法不为空就返回true。这意味着只要时无参构造方法就会实例化。
再回去看readOrdinaryObject()
的源码。先是判断readResloveMethod
是否为空,通过全局查找可知在私有方法ObjectStreamClass()
给其赋值,赋值代码如下:
readResolveMethod = gerInheritableMethod(c1,"readResolve",null,Object.class);
之后上述的逻辑找到一个readResolve()
方法如果存在就调用invokeReadResolve()
方法,其代码如下:
/**
* Invokes the readResolve method of the represented serializable class and
* returns the result. Throws UnsupportedOperationException if this class
* descriptor is not associated with a class, or if the class is
* non-serializable or does not define readResolve.
*/
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
由invokeReadResource()
方法又使用反射调用readResolveMethod()
,进而执行readResolve()
方法。
通过分析源码可以看出,readResolve()
方法虽然解决了单例模式被破坏的问题,但是其实例化两次,只不过新创建的对象被覆盖了而已 。如果创建的对象动作发生加快,就意味着内存开销也随之增大。这个问题如何解决呢?使用注册式单例即可完美解决上诉问题。
注册式单例
-
枚举式单例
代码示例
public enum EnumSingleton{
INSTANCE;
private Object data;
/**
* @return Object return the data
*/
public Object getData() {
return data;
}
/**
* @param data the data to set
*/
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
经过反编译分析源码可知枚举式单例是在静态代码块中为INSTANCE赋值,使饿汉式单例的体现。
那么序列化和反序列化能否破坏吗枚举式单例呢? 答案是不能。同查看源码可知枚举类型是通过类名和对象名找到全局唯一的对象。所以,枚举对象不可能加载多次。
那么反射呢?答案也是不能。在程序运行时会报java.lang.NoSuchMethodException
异常,其意思为没有找到无参的构造方法。查看java.lang.Enum
源码可知枚举类型只有一个protect
构造方法。经过测试,使用反射直接实例化枚举对象时会出现Cannot reflectively create objects
查看Constructor
的newInstsnce()
方法可知,在方法体做了判断,如果是枚举类型则直接抛出异常。
看到这个词,有的小伙伴的心里就想什么是容器式单例。容器式单例就是在单例类中维护一个类似与Map的容器,这种方式在Spring中是非常常见的,众所周知,Spring的Bean是全局单例的;Spring在内部维护着一个Map结构。在org.springframework.beans.factory.support
包下SimpleBeanDefinitionRegistry
为我们完美的解释容器式单例,其源码如下:
public class SimpleBeanDefinitionRegistry extends SimpleAliasRegistry implements BeanDefinitionRegistry {
/** Map of bean definition objects, keyed by bean name. */
private final Map beanDefinitionMap = new ConcurrentHashMap<>(64);
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
Assert.hasText(beanName, "'beanName' must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
this.beanDefinitionMap.put(beanName, beanDefinition);
}
@Override
public void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
if (this.beanDefinitionMap.remove(beanName) == null) {
throw new NoSuchBeanDefinitionException(beanName);
}
}
@Override
public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
BeanDefinition bd = this.beanDefinitionMap.get(beanName);
if (bd == null) {
throw new NoSuchBeanDefinitionException(beanName);
}
return bd;
}
@Override
public boolean containsBeanDefinition(String beanName) {
return this.beanDefinitionMap.containsKey(beanName);
}
@Override
public String[] getBeanDefinitionNames() {
return StringUtils.toStringArray(this.beanDefinitionMap.keySet());
}
@Override
public int getBeanDefinitionCount() {
return this.beanDefinitionMap.size();
}
@Override
public boolean isBeanNameInUse(String beanName) {
return isAlias(beanName) || containsBeanDefinition(beanName);
}
}
其中BeanDefinition
是一个接口,储存着各个单例对象的信息,由其实现类实现。对象名作为Map的Key,BeanDefinition
作为Map的值,维护着这个map 保证每个对象全局单例.
因为Spring比较复杂,讨论暂告一段落。下面会到我们的主题,那我们的singleton
类如何实现容器式单例呢。下面看代码:
import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashMap;
public class Singleton {
private static Map ioc =new ConcurrentHashMap();
private Singleton() {}
public static Object getInstance(String name) {
synchronized(ioc) {
if (!ioc.containsKey(name)){
Object o=null;
try {
o=Class.forName(name).newInstance();
ioc.put(name, o);
}catch(Exception e) {
e.printStackTrace();
}
return o;
}
else {
return ioc.get(name);
}
}
}
}
容器式单例适用于单例实例对象比较多的情况下,方便管理。值得注意的是,他是线程不安全的。
注册式单例就包括上面两种形式,每个都有不同的应用场景以及特点,要根据实际情况灵活选择。
下面我来介绍一种特殊的单例模式-----拥有ThreadLocal
单例模式。
扩展
ThreadLocal
与单例模式
话不多说,直接看代码。
public class Singleton {
private static final ThreadLocal instance = new ThreadLocal<> (){
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
private Singleton() {}
public static Object getInstance() {
return instance.get();
}
}
为什么说他特殊呢?因为加了ThreadLocal
关键字的单例类是线程内单例的,单线程共享不是单例的。大家可以测试下,使用下面的测试代码。
public class Main{
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Singleton.getInstance());
}
} ;
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
System.out.println("end");
}
}
执行结果如下:
测试后发现主线程无论执行多少次,获取的实例都是同一个,而两个子线程却获得了不同的实例。
声明
本文章为作者原创,其中参考了《spring5核心原理与30个类手写实战》以及互联网上的内容。如要转载请注明来源。
如有错误,请评论或者私聊我,欢迎探讨技术问题