众所周知,单例模式分为饿汉式和懒汉式,昨天在看了《spring5核心原理与30个类手写实战》以后才知道饿汉式有不少种写法,分别适用于不一样场景,避免反射,线程不安全问题。下面就各类场景、采用的方式及其优缺点介绍。java
1.第一种写法 ( 定义即初始化)spring
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;
}
}复制代码
这里须要解释下,童鞋们都知道一个对象使用要经历一下步骤:app
在java中JVM为了提升执行效率,会进行指令重排。那什么时指令重排呢?**指令重排**是指JVM为了优化指令,提升程序的运行效率,在不影响单线程执行结果的状况下,进行指令重排序,以期提升并行度。复制代码
有上述能够指令重排在单线程状况下,对程序的执行不会产生影响,但在多线程状况下就不必定了。因此上述过程的执行顺序可能发生变化,进而致使程序并不会按照预想的执行。框架
为解决上述问题以及保证并发编程的正确性,java中定义了 **happens-before**原则。在 《JSR-133:Java Memory Model and Thread Specification》 书中关于happens-before定义是这样的:复制代码
1.若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。ide
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.
*
* <p>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.
*
* <p>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<String, BeanDefinition> 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 <String,Object > 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<Singleton> 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个类手写实战》以及互联网上的内容。如要转载请注明来源。若有错误,请评论或者私聊我,欢迎探讨技术问题