菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
342
0

单例模式

原创
05/13 14:22
阅读数 27891

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

一、单例模式的定义

单例模式指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点;所以想到的第一个创建方式就是隐藏掉它所有的构造方法,让用户不是通过构造方法去构造,而是通过他自身的类去构造一个实例

二、单例模式的应用场景

代码接近于生活,很有意思。比如一个网站的登录,点击登录后弹出一个登录弹框,即使再次点击,也不会再出现一个相同的弹框。又或者一个音乐播放程序,如果用户打开了一个音乐,又想打开一个音乐,那么之前的播放界面就会自动关闭,切换到当前的播放界面。这些都是单例模式的应用场景。
要实现一个单例模式,一个经典的方式是创建一个类,类中又一个方法能创建该类的实例对象,还有一个标记,记录是否已经创了过了实例对象。如果对象已经存在,就返回第一次实例化对象的引用 

三、饿汉式单例

饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。
/**
 *优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
 *缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。
 */
public class TheHungry {
    private static final TheHungry theHungry=new TheHungry();
    private TheHungry(){}
    private static TheHungry getInstance(){
        return theHungry;
    }
}

四、懒汉式单例

懒汉式单例的特点是:被外部类调用的时候内部类才会加载,下面看懒汉式单例的简单
/**
 * 优点:被外部类调用的时候内部类才会加载
 * 缺点:线程不安全
 */
public class Idler {
    private static Idler idler=null;

    private Idler(){}

    private static Idler getInstance(){
        if (idler==null){
            return idler= new Idler();
        }
        return idler;
    }
}
public class ExectorThread implements Runnable {
    @Override
    public void run() {
        Idler idler=  Idler.getInstance();
        System.out.println(Thread.currentThread().getName()+":"+idler);
    }
}
public class Test {
    public static void main(String[] args) {
        Thread t1=new Thread(new ExectorThread());
        Thread t2=new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("结束");
    }
}

从下图看可以发现创建了两个对象;下面分析下为什么出现这种情况:

那么怎么解决线程安全问题呢,第一个想到的加锁

public class Idler {
    private static Idler idler=null;

    private Idler(){}

    public  synchronized static Idler getInstance(){
        if (idler==null){
            return idler= new Idler();
        }
        return idler;
    }
}
这时候,我们再来调试。当我们将其中一个线程执行并调用 getInstance()方法时,另一个线程在调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复 RUNNING 状态继续调用 getInstance()
方法。如下图所示:
完美的展现了 synchronized 监视锁的运行状态,线程安全的问题便解决了。但是,用synchronized 加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既
兼顾线程安全又提升程序性能呢?那就是看双重检查锁的单例模式:
所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
public class Idler {
    private static Idler idler=null;
    private Idler(){}

    public   static Idler getInstance(){
        if (idler==null){
            synchronized (Idler.class){
                if (idler==null){
                    return idler= new Idler(); //1
                }
            }

        }
        return idler;
    }
}

上面代码看起来完美,但看过我写的多线程文章的朋友可能看出来了,上面代码带来了另一个问题,那就是指令重排序问题;问题的根源在于 1创建对象 的流程可能会被重排序。在聊指令重排前先聊一下创建对象的一般流程:

创建对象的流程可以简化为三步:分配内存空间、初始化对象、设置引用指向分配的内存地址。

memory = allocate(); // 1、分配对象的内存空间

initObject(memory); // 2、初始化对象

instance = memory; // 3、设置instance指向分配的内存地址

其中,2 和 3 之间可能被重排序。为什么看我的多线程文章;重排序之后的流程

memory = allocate(); // 1、分配对象的内存空间

instance = memory; // 2、设置instance指向分配的内存地址

initObject(memory); // 3、初始化对象

那么,在发生重排序的情况下,AB两个线程执行这段双重检查锁定的代码时的执行顺序可能是这样的:

 

 

 按照以上的顺序,B线程将会在instance对象还未初始化完成之前就访问它,导致NPE。解决这问题方法也很简单加个volatile关健字就好

public class Idler {
    private volatile static Idler idler=null; // 声明为volatile类型
    private Idler(){}

    public   static Idler getInstance(){
        if (idler==null){
            synchronized (Idler.class){
                if (idler==null){
                    return idler= new Idler();
                }
            }

        }
        return idler;
    }
}

volatile通过禁止重排序(初始化对象和设置instance指向分配的内存地址)来保证线程安全的延迟初始化。但是,用到 synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。可以从类初始化角度来考虑,看下面的代码,采用静态内部类的方式

//这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
// 完美地屏蔽了这两个缺点
public class Single {
    //默认使用 Single 的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private Single(){}
    //每一个关键字都不是多余的
    // static 是为了使单例的空间共享
    // 保证这个方法不会被重写,重载
    public static final Single getInstance(){
        //在返回结果以前,一定会先加载内部类
        return getInstance.INSTANCE;
        }
    //默认不加载 
    private static class getInstance{
        private static final Single INSTANCE = new Single();
    }     
}

五、反射破坏单例

上面介绍的单例模式的构造方法除了加上 private 以外,没有做任何处理。如果使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会两个不同的实例。现在来看一段测试代码
public class Test {
    public static void main(String[] args) {
//        Thread t1=new Thread(new ExectorThread());
//        Thread t2=new Thread(new ExectorThread());
//        t1.start();
//        t2.start();
//        System.out.println("结束");

        //反射破坏单例
        Class<?> clazz=Single.class;
        //通过反射拿到私有的构造方法=
        Constructor c= null;
        try {
            c = clazz.getDeclaredConstructor(null);
            //强吻
            c.setAccessible(true);
            //暴力初始化
            Object object=c.newInstance();
            //调用了两次构造方法,相当于 new 了两次
            Object object1=c.newInstance();
            System.out.println(object);
            System.out.println(object1);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }
}

看结果发现单例被破坏了

竟然问题是出现在构造方法上,那在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:

//这种形式兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
// 完美地屏蔽了这两个缺点
public class Single {


    //默认使用 Single 的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private Single(){
        //在构造方法中加入判断
        if(getInstance.INSTANCE != null)
        { throw new RuntimeException("已经有对象了"); }
        
    }
    //每一个关键字都不是多余的
    // static 是为了使单例的空间共享
    // 保证这个方法不会被重写,重载
    public static final Single getInstance(){
        //在返回结果以前,一定会先加载内部类
        return getInstance.INSTANCE;
        }
    //默认不加载
    private static class getInstance{
        private static final Single INSTANCE = new Single();
    }
}

到这里单例才算完成

六、序列化破坏单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例
 
//反序列化时导致单例破坏
public class Serialization implements Serializable {
    //序列化就是说把内存中的状态通过转换成字节码的形式
    // 从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO)
    // 内存中状态给永久保存下来了
    // 反序列化
    // 将已经持久化的字节码内容,转换为 IO 流 /
    // /通过 IO 流的读取,进而将读取的内容转换为 Java 对象
    // 在转换过程中会重新创建对象 new
    public final static Serialization INSTANCE = new Serialization(); 
    private Serialization(){} 
    public static Serialization getInstance(){ return INSTANCE; }
}
public class SerializationTest {
    public static void main(String[] args) {
        Serialization serialization=Serialization.getInstance();
        Serialization serialization1=null;

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("ghy");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(serialization);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("ghy");
            ObjectInputStream ois = new ObjectInputStream(fis);
            serialization1 = (Serialization)ois.readObject();
            ois.close();
            System.out.println(serialization);
            System.out.println(serialization1);
           
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果显示单例被破坏了

可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例的设计初衷。其实在这种情况下实现单例很简单,只需要增加 readResolve()方法即可。

//反序列化时导致单例破坏
public class Serialization implements Serializable {
    //序列化就是说把内存中的状态通过转换成字节码的形式
    // 从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO)
    // 内存中状态给永久保存下来了
    // 反序列化
    // 将已经持久化的字节码内容,转换为 IO 流 /
    // /通过 IO 流的读取,进而将读取的内容转换为 Java 对象
    // 在转换过程中会重新创建对象 new
    public final static Serialization INSTANCE = new Serialization();
    private Serialization(){}
    public static Serialization getInstance(){ return INSTANCE; }
    private Object readResolve(){return INSTANCE;}
}

测试程序一样,我就不截图了,走到这里一定在想一个readResolve()方法为什么就能保证单例,如果想搞清楚这个就要看看JDK的源码实现,进入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();
            }
        }
    }
发现在readObject中又调用了重写的readObject0()方法。进入readObject0()方法,代码如下:
 private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }
看到 TC_OBJECTD 中判断,调用了 ObjectInputStream 的 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);
        }

        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 的 isInstantiable()方法,而 isInstantiable()里面的代码如下: 
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }
代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。意味着,只要有无参构造方法就会实例化。这时候,其实还没有找到为什么加上 readResolve()方法就避免了单例被破坏的真正原
因。我再回到 ObjectInputStream 的 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);
        }

        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;
    }
判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod()方法
 boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }
逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回 true。那么readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法ObjectStreamClass()方法中给readResolveMethod 进行赋值,来看代码:
readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);
上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在再 回 到 ObjectInputStream 的 readOrdinaryObject() 方 法 继 续 往 下 看 , 如 果readResolve()存在则调用 invokeReadResolve()方法,来看代码:
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();
        }
    }
我们可以看到在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。通过JDK 源码分析我们可以看出,虽然,增加 readResolve()方法返回实例,解决了单例被破坏的问题。但是,通过分析源码以及调试,可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,难道真的就没办法从根本上解决问题吗?下面我们来注册式单例是怎么解决这些问题的

七、注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。先来看枚举式单例的写法
 
 
//枚举单例
public enum EnumEinheit {
    INSTANCE;
    private Object data;
    public Object getData(){
        return data;
    }
    public void setData(Object data){
        this.data=data;
    }
    public static EnumEinheit getInstance(){
        return INSTANCE;
    }
}

 

public class EnumEinheitTest {
    public static void main(String[] args) {

        try {
            EnumEinheit enumEinheit=null;
            EnumEinheit enumEinheit1=EnumEinheit.getInstance();
            enumEinheit1.setData(new Object());

            FileOutputStream fos=new FileOutputStream("ghy");
            ObjectOutputStream oos=new ObjectOutputStream(fos);
            oos.writeObject(enumEinheit1);
            oos.flush();
            oos.close();

            FileInputStream fis=new FileInputStream("ghy");
            ObjectInputStream ois=new ObjectInputStream(fis);
            enumEinheit= (EnumEinheit) ois.readObject();
            ois.close();

            System.out.println(enumEinheit==enumEinheit1);

        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

这代码我就不运行了,结果一定是true,这说明他们俩是一个实例,从而证明这是一个单例,下面通过分析源码来看下他是怎么做到的。在看源码前需要下载一个Jad工具进行反编译 通过工具可以很快发现下面代码

static 
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}

从上面可以看出,枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现,这里面说明了饿汉的问题权举也有。至此,还有另一个问题要解决,那就是序列化为什么破坏不了单例,不妨再来看一下 JDK源码,还是回到 ObjectInputStream 的 readObject0()方法:

 private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

从上面可以看到在 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;
    }
发现枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。那么下面来看下能否通过反射破坏枚举式单例
public class EnumEinheitTest {
    public static void main(String[] args) {

//        try {
//            EnumEinheit enumEinheit=null;
//            EnumEinheit enumEinheit1=EnumEinheit.getInstance();
//            enumEinheit1.setData(new Object());
//
//            FileOutputStream fos=new FileOutputStream("ghy");
//            ObjectOutputStream oos=new ObjectOutputStream(fos);
//            oos.writeObject(enumEinheit1);
//            oos.flush();
//            oos.close();
//
//            FileInputStream fis=new FileInputStream("ghy");
//            ObjectInputStream ois=new ObjectInputStream(fis);
//            enumEinheit= (EnumEinheit) ois.readObject();
//            ois.close();
//
//            System.out.println(enumEinheit==enumEinheit1);
//
//        }catch (Exception e){
//            e.printStackTrace();
//        }
        try {
           Class clazz=EnumEinheit.class;
            Constructor c = clazz.getDeclaredConstructor();
            c.newInstance();

        }catch (Exception e){
            e.printStackTrace();
        }


    }
}

报的错意思是没找到无参的构造方法。这时候,打开 java.lang.Enum 的源码代码,查看它的构造方法,只有一个 protected的构造方法,代码如下:

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

根据上面情况,那下面试下强吻能能访问的到

public class EnumEinheitTest {
    public static void main(String[] args) {

//        try {
//            EnumEinheit enumEinheit=null;
//            EnumEinheit enumEinheit1=EnumEinheit.getInstance();
//            enumEinheit1.setData(new Object());
//
//            FileOutputStream fos=new FileOutputStream("ghy");
//            ObjectOutputStream oos=new ObjectOutputStream(fos);
//            oos.writeObject(enumEinheit1);
//            oos.flush();
//            oos.close();
//
//            FileInputStream fis=new FileInputStream("ghy");
//            ObjectInputStream ois=new ObjectInputStream(fis);
//            enumEinheit= (EnumEinheit) ois.readObject();
//            ois.close();
//
//            System.out.println(enumEinheit==enumEinheit1);
//
//        }catch (Exception e){
//            e.printStackTrace();
//        }
        try {
           Class clazz=EnumEinheit.class;
            Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
            c.setAccessible(true);
            c.newInstance();

        }catch (Exception e){
            e.printStackTrace();
        }


    }
}

结果如下

这时错误已经非常明显了,告诉我们 Cannot reflectively create enum objects,不能用反射来创建枚举类型。还是习惯性地想来看看 JDK 源码,进入 Constructor 的newInstance()方法:
 @CallerSensitive
    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;
    }
在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型,直接抛出异常。到这为止,已经非常清晰枚举式单例在 JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。接下来看注册式单例还有另一种写法,容器缓存的写法,这种写法在源码中应用的比较多
public class Container {
    private Container(){}
    private static Map<String,Object> ioc=new ConcurrentHashMap<String,Object>();
    public static Object getBean(String className){
        synchronized (ioc){
            if (!ioc.containsKey(className)){
                Object obj=null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }else {
                return ioc.get(className); 
            }
        }
    }
}

八、ThreadLocal 线程单例

这个玩意我本来不想说的,不明白的可以看我多线程文章就很清楚的懂了,在这里之所以列出来,是因为在当时讲解多线程时有人问我应用场景,这种线程单例应用 最多的还是连接池,比喻数据库数据源切换时的线程单例,不说那么多,反正这写法很多人也用不上,就简单说明下

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){ 
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton(); 
        } 
    }; 
    private ThreadLocalSingleton(){} 
    public static ThreadLocalSingleton getInstance(){ 
        return threadLocalInstance.get();
    }
}

九、单例模式在开源代码中的应用

1、JDK 中 java.lang.Runtime 类,每个运行中的 Java 应用的环境信息,单例。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    private Runtime() {}
    .
    .
    .
}

2、Spring 的单例 bean 的实现

ApplicationContext context = new ClassPathXmlApplicationContext("beanFactoryTest.xml");
MyTestBean bean1 = (MyTestBean)context.getBean("myTestBean");
MyTestBean bean2 = (MyTestBean)context.getBean("myTestBean");
System.out.println(bean1 == bean2); // 打印true
        
ApplicationContext context2 = new ClassPathXmlApplicationContext("beanFactoryTest.xml");
MyTestBean bean3 = (MyTestBean)context2.getBean("myTestBean");

代码中 bean1 = bean2,因为 Spring 默认是单例,且 bean1 和 bean2 都取自 context 实例中容器;

bean1 != bean3,因为 bean1 和 bean3 分别取自不同的容器。

那 Spring 是如何实现单例的呢?

看下 org.springframework.beans.factory.support.AbstractBeanFactory 获取 bean 的代码

protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException {
    .
    .
    .
    //获取缓存的 bean 实例
    Object sharedInstance = getSingleton(beanName);
    .
    .
    .
    //创建 bean 实例
  if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
      @Override
      public Object getObject() throws BeansException {
        try {
          return createBean(beanName, mbd, args);
        }
        catch (BeansException ex) {
          // Explicitly remove instance from singleton cache: It might have been put there
          // eagerly by the creation process, to allow for circular reference resolution.
          // Also remove any beans that received a temporary reference to the bean.
          destroySingleton(beanName);
          throw ex;
        }
      }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }
    .
    .
    .
​
}

1、Srping IoC 容器在初始化的时候,若未配置懒加载,单例 bean 实例会在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory 创建,createBean -> doCreateBean -> createBeanInstance

 2、单例 bean 实例获取的逻辑在 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry 的 getSingleton 方法

public class DefaultSingletonBeanRegistry {
    //缓存单例 bean 实例
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
​
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        //从 ConcurrentHashMap 中获取 bean 实例
        Object singletonObject = this.singletonObjects.get(beanName);
    .
    .
    .
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
}

可见 Spring 的单例 bean 实例的创建,是通过代码自己控制的,单例 bean 实例保存在一个 ConcurrentHashMap 中,而不是上述单例模式的典型实现方式。

这里说的 Spring 的单例是在 IoC 容器中,实际开发中有时候还需要考虑线程间的单例和分布式中多进程间的单例。线程间的单例可以使用 ThreadLocal 实现,进程间的单例要借助分布式锁以及对单例实例进行序列化存储与反序列化。

十、总结

单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。单例模式看起来非常简单,实现起来其实也非常简单。

 

git源码:https://github.com/ljx958720/design_pattern.git

 

发表评论

0/200
342 点赞
0 评论
收藏