为了账号安全,请及时绑定邮箱和手机立即绑定

源码学习之设计模式(单例模式)

标签:
Java

众所周知,单例模式分为饿汉式和懒汉式,昨天在看了《spring5核心原理与30个类手写实战》之后才知道饿汉式有很多种写法,分别适用于不同场景,避免反射,线程不安全问题。下面就各种场景、采用的方式及其优缺点介绍。

饿汉式 (绝对的线程安全)

代码示例

1.第一种写法 ( 定义即初始化)

public class Singleton{
	private static final Singleton instance = new Singleton();
	private Singleton() {}
	public static Singleton getInstance() {
		return instance;
	}
}
  1. 第二种写法 (静态代码块)
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 ,在应用启动后就能获取实例,以便于进行接下来的操作.

优点
因其在程序启动后就已经初始化,也不需要任何锁保证线程安全 ,所以执行效率高
缺点
因为在程序启动后就已经进行了初始化,即便是不用也进行了初始化,所以无论何时都占用内存空间,浪费了内存空间。

懒汉式 (线程安全需要另外的操作)

代码示例
  1. 第一种写法
public class Singleton{
    private static final Singleton instance = null;
 
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }

}

上面的代码不难看出,在单线程下执行是没有问题的,但在多线程情况下,线程执行速度和顺序无法控制确定,故有可能会产生多个实例对象,这样就违背了单例模式的初衷了。

  1. 第二种写法

    加锁保证线程安全(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的时候默认不加载,只有调用时进行加载。根据此特点双锁检查机制的单例模式可以改进使用静态内部类。

  1. 使用静态内部类

    代码示例

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;
    }

由上述代码可知,由调用了ObjectStreamClassisInstanctiable() 方法,方法体非常简单,源码如下 :

 /**
     * 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() 方法虽然解决了单例模式被破坏的问题,但是其实例化两次,只不过新创建的对象被覆盖了而已 。如果创建的对象动作发生加快,就意味着内存开销也随之增大。这个问题如何解决呢?使用注册式单例即可完美解决上诉问题。

注册式单例

  1. 枚举式单例
代码示例
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 查看ConstructornewInstsnce() 方法可知,在方法体做了判断,如果是枚举类型则直接抛出异常。

看到这个词,有的小伙伴的心里就想什么是容器式单例。容器式单例就是在单例类中维护一个类似与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");
    }
}

file

执行结果如下:

测试后发现主线程无论执行多少次,获取的实例都是同一个,而两个子线程却获得了不同的实例。

声明

本文章为本人看书总结,其中参考了《spring5核心原理与30个类手写实战》以及互联网上的内容。如要转载请注明来源。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消