美文网首页
2019-05-26 单列设计模式

2019-05-26 单列设计模式

作者: 竹blue | 来源:发表于2019-05-26 22:56 被阅读0次

    单例模式

    • 定义:是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。

    饿汉式单例模式

    \color{red}{定义:} 类加载的时候就立即初始化,并且创建单例对象,线程安全。
    \color{red}{优点:} 无锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
    \color{red}{缺点:} 类加载的时候就初始化,占用空间、内存。
    \color{red}{适用场景:} 饿汉式适用在单例对象较少的情况。
    \color{red}{示例代码:}

    /**
     * 写法一
     */
    public class HungrySingleton {
    
        private static final HungrySingleton hungrySingleton = new HungrySingleton();
    
        private HungrySingleton() {
        }
    
        public static HungrySingleton getInstance(){
            return hungrySingleton;
        }
    }
    /**
     * 饿汉式静态块单例
     */
    class HungrySingLeton2{
    
        private static HungrySingLeton2 hungrySingLeton2;
        static {
            hungrySingLeton2 = new HungrySingLeton2();
        }
    
        private HungrySingLeton2() {}
    
        public HungrySingLeton2 getInstance(){
            return hungrySingLeton2;
        }
    }
    

    懒汉式单例模式

    \color{red}{定义:} 被外部需要使用的时候才进行实例化。
    \color{red}{优点:} 对内存的利用率较高。
    \color{red}{缺点:} 性能较饿汉式较低,并发量较大的场景,容易造成线程阻塞,影响程序运行性能。
    \color{red}{适用场景:} 对内存利用率要求较高,并发较低的场景。
    \color{red}{示例代码:}

    public class LazySimpleSingleton {
        private LazySimpleSingleton() {
        }
    
        //静态块,公共内存区域
        private static LazySimpleSingleton lazySimpleSingleton;
    
        //第一种无法保证现场安全,多线程调用存在多次实例化问题,即使得到的地址值相同也可能是多次实例化后的数据,可通过debug的Thead模式进行测试
        public static LazySimpleSingleton getInstance() {
            if(lazySimpleSingleton==null){
                lazySimpleSingleton = new LazySimpleSingleton();
            }
            return lazySimpleSingleton;
        }
        //第二种 加锁保证程序的原子性,synchronized修饰的代码只能串行访问,所以影响程序性能。
        public static LazySimpleSingleton getInstanceSycn() {
            if (lazySimpleSingleton == null) {
                // 互斥锁 锁级别为类锁,多线程只能串行访问该资源
                synchronized (LazySimpleSingleton.class) {
                    if (lazySimpleSingleton == null) {
                        lazySimpleSingleton = new LazySimpleSingleton();
                    }
                }
    
            }
            return lazySimpleSingleton;
        }
    }
    

    懒汉式内部类单例

    从类初始化角度来考虑,可以采用静态内部类的方式;这样即兼顾饿汉式的内存浪费,又兼顾 synchronized 性能问题。
    \color{red}{示例代码:}

    /**
     * LazyInnerClassSingleton类
     * 静态内部类
     * 兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
     * 内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题
     *
     * @author wangjixue
     * @date 2019-05-25 16:46
     */
    
    public class LazyInnerClassSingleton {
    
        private LazyInnerClassSingleton() {}
    
        //static:是为了使单例的空间在多个线程间共享
        //final : 保证这个方法不会被重写,重载
        public static final LazyInnerClassSingleton getInstance() {
            //在返回结果以前,一定会先加载内部类
            return LazyHodler.LAZY;
        }
    
        //默认不加载
        private static class LazyHodler {
            //1.分配内存地址 addr01 给LazyInnerClassSingleton对象
            //2.初始化LazyInnerClassSingleton对象
            //3.设置LAZY执行该刚分配的地址addr01
            //因为 static:代表多个线程间访问的成员变量LAZY是同一个成员变量。
            // final:代表映射的地址不变,所以每个线程T的成员变量LAZY指向的都是addr01对应的实例。
            private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
        }
    
    }
    

    破坏单例

    反射破坏单例

    反射如何破坏单例:使用反射来调用其私有的构造方法,然后再调用 newInstance()方法就会创建不同的实例。
    \color{red}{示例代码:}

    /**
     * 通过反射方式获取懒汉式内部类单例对象
     */
    class ReflectLazyInnerClassSingletonTest{
    
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            ////通过反射获取私有的构造方法
            Constructor<LazyInnerClassSingleton> constructor = LazyInnerClassSingleton.class.getDeclaredConstructor();
            // 强制访问
            constructor.setAccessible(true);
    
            // 初始化,此次通过私有构造方法new了两次
            LazyInnerClassSingleton obj01 = constructor.newInstance();
            LazyInnerClassSingleton obj02 = constructor.newInstance();
    
            System.err.println(obj01);
            System.err.println(obj02);
            System.out.println("=========equal===========");
            System.err.println(obj02==obj01);
    
        }
    }
    

    \color{red}{如何优化:}懒汉式内部类单例:在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。
    \color{red}{示例代码:}

    /**
     * LazyInnerClassSingleton类
     * 静态内部类
     * 兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
     *
     * @author wangjixue
     * @date 2019-05-25 16:46
     */
    
    public class LazyInnerClassSingleton {
    
        private LazyInnerClassSingleton() {
            //防止通过反射的方式获取
            if(LazyHodler.LAZY!=null){
                throw new RuntimeException("禁止通过单例的私有构造方法创建多个实例,因为违反了\"一个类在任何情况下都绝对只有一个实例\"的设计的初衷");
            }
        }
    、、、
    }
    

    反序列化破坏单例

     当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。

    \color{red}{示例代码:}

    /**
     * SeriableLazyInnerClassSingleton类
     * 可序列化静态内部类
     * 兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
     * 但是反序列化会new出不同的对象实例。
     *
     * @author wangjixue
     * @date 2019-05-25 16:46
     */
    
    public class SeriableLazyInnerClassSingleton implements Serializable {
    
        //序列化:将内存中的数据写入其他地方(硬盘,网络IO);
        //具体来说:序列化就是说把内存中的状态转换成字节码以I/O流的形式写入硬盘等IO设备中永久的保存起来。
    
    
    
        //反序列化:将其他地方的数据写入内存
        //具体来说:将IO设备中保存的字节码通过I/O流的形式读取到内存,并转换成Java对象的过程就是反序列化。
    
        //注意:转换成Java对象过程中会重新new对象实例;
    private SeriableLazyInnerClassSingleton() {
            //防止通过反射的方式获取
            if(LazyHodler.LAZY!=null){
                throw new RuntimeException("禁止通过单例的私有构造方法创建多个实例,因为违反了\"一个类在任何情况下都绝对只有一个实例\"的设计的初衷");
            }
        }
    
        //static:是为了使单例的空间在多个线程间共享
        //final : 保证这个方法不会被重写,重载
        public static final SeriableLazyInnerClassSingleton getInstance() {
            //在返回结果以前,一定会先加载内部类
            return LazyHodler.LAZY;
        }
    
        //默认不加载
        private static class LazyHodler {
            //1.分配内存地址 addr01 给LazyInnerClassSingleton对象
            //2.初始化LazyInnerClassSingleton对象
            //3.设置LAZY执行该刚分配的地址addr01
            //因为 static:代表多个线程间访问的成员变量LAZY是同一个成员变量。
            // final:代表映射的地址不变,所以每个线程T的成员变量LAZY指向的都是addr01对应的实例。
            private static final SeriableLazyInnerClassSingleton LAZY = new SeriableLazyInnerClassSingleton();
        }
    
    }
    
    
    /**
     * 通过反序列获取单例测试
     */
    public class SerializableLazyInnerClassSingletonTest {
        public static void main(String[] args) {
            // 通过反序列化得到的实例对象
            SeriableLazyInnerClassSingleton singleton01 = null;
            // 内存中的实例对象
            SeriableLazyInnerClassSingleton singleton02 = SeriableLazyInnerClassSingleton.getInstance();
    
            try {
                //序列化到IO设备中
                FileOutputStream fos = new FileOutputStream("SeriableLazyInnerClassSingleton.class");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(singleton02);
                oos.flush();
                oos.close();
    
                //反序列化为Java对象
                FileInputStream fis = new FileInputStream("SeriableLazyInnerClassSingleton.class");
                ObjectInputStream ois = new ObjectInputStream(fis);
                singleton01 = (SeriableLazyInnerClassSingleton) ois.readObject();
                ois.close();
    
                System.err.println("singleton01 = "+singleton01);
                System.err.println("singleton02 = "+singleton02);
                System.out.println("======验证========");
                System.err.println(singleton01==singleton02);
    
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
        }
    }
    

    \color{red}{解决办法:}只需要在单例中增加 readResolve()方法即可。

    public class SeriableLazyInnerClassSingleton implements Serializable {
    
        //序列化:将内存中的数据写入其他地方(硬盘,网络IO);
        //具体来说:序列化就是说把内存中的状态转换成字节码以I/O流的形式写入硬盘等IO设备中永久的保存起来。
    
    
    
        //反序列化:将其他地方的数据写入内存
        //具体来说:将IO设备中保存的字节码通过I/O流的形式读取到内存,并转换成Java对象的过程就是反序列化。
        //注意:转换成Java对象过程中会重新new对象实例;
    
    
        //防止反序列化new新的实例对象
        private Object readResolve(){
            return getInstance();
        }
    、、、
    }
    

     通过查看JDK源码发现:ObjectInputStream 类的 readObject()方法首先调用ObjectStreamClass 的 isInstantiable()方法来判断构造方法是否为空,不为空返回true, (\color{red}{意味着:}只要有无参构造方法就会实例化。)验证可以进行对象的实例化后调用hasReadResolveMethod()判断 readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法 ObjectStreamClass()方法中给 readResolveMethod 进行赋值。

        readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class);
    

     上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在 再 回 到 ObjectInputStream 的 readOrdinaryObject() 方 法 继 续 往 下 看 , 如 果 readResolve()存在,则调用 invokeReadResolve()方法。在invokeReadResolve内部通过反射方式调用readResolveMethod()方法。

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

     通过 JDK 源码分析我们可以看出,虽然增加 readResolve()方法返回实例,解决了反序列化获取单例被破坏的问题。但是,我们通过分析源码以及调试,发现实际上单例对象实例化了两 次,只不过新创建的对象没有被返回而已。如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,那么如果解决多次创建的问题--注册式单例。

    注册时单例

    \color{red}{定义:} 注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。

    • 枚举单例
       枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现。
    /**
     * EnumSingleton类
     * 枚举注册式单例
     * 《Effective Java》推荐的一种单例实现写法
     *
     * @author wangjixue
     * @date 2019-05-26 00:17
     */
    public enum  EnumSingleton {
    
        INSTANCE;
    
        private Object data;
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    
        public static EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    
    
    /**
     * EnumSingletonTest类
     *
     * @author wangjixue
     * @date 2019-05-26 00:36
     */
    public class EnumSingletonTest {
        public static void main(String[] args) {
            // 通过反序列化得到的实例对象
            EnumSingleton es01 = null ;
            // 内存中的实例对象
            EnumSingleton es02 = EnumSingleton.getInstance();
            es02.setData(new Object());
    
            try {
                //序列化到IO设备中
                FileOutputStream fos = new FileOutputStream("EnumSingleton.seri");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(es02);
                oos.flush();
                oos.close();
    
                //反序列化为Java对象
                FileInputStream fis = new FileInputStream("EnumSingleton.seri");
                ObjectInputStream ois = new ObjectInputStream(fis);
                es01 = (EnumSingleton) ois.readObject();
                ois.close();
    
                System.err.println("EnumSingleton01 = "+es01.getData());
                System.err.println("EnumSingleton02 = "+es02.getData());
                System.out.println("======验证========");
                System.err.println(es01==es02);
    
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    

     通过JDK 源码,发现 readObject0()中调用了 readEnum()方法
    我们发现枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。

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

     那么反射是否能破坏枚举式单例呢 ?同理通过JDK 源码分析发现进入 Constructor 的 newInstance()方法,做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型, 直接抛出异常。

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

     到这为止,我们分析处理枚举式单例枚举式单例也是《Effective Java》书中推荐的一种单例实现写法。在 JDK 枚举的语法特殊性,以及反射也为枚举保 驾护航,让枚举式单例成为一种比较优雅的实现。

    • 容器缓存
       容器式单例适用于创建实例非常多的场景,便于管理。但是是非线程安全的。
    /**
     * ContainerSingleton类
     * 容器缓存式单例
     * 优点:容器式写法适用于创建实例非常多的情况,便于管理。
     * 缺点:非线程安全
     *
     * @author wangjixue
     * @date 2019-05-26 00:19
     */
    
    public class ContainerSingleton {
    
        private ContainerSingleton() {}
    
        private static  Map<String ,Object> container = new ConcurrentHashMap<String ,Object>();
    
        public Object getBean(String className){
    
            synchronized (container){
                Object instance = null;
                if(!container.containsKey(className)){
                    try {
                        instance = Class.forName(className).newInstance();
                        container.put(className,instance);
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }else{
                   instance= container.get(className);
                }
                return instance;
            }
        }
    }
    

    ThreadLocal 线程单例

     ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。
    \color{red}{示例代码:}

    /**
     * ThreadLocalSingleton类
     * ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的
     * @author wangjixue
     * @date 2019-05-26 00:52
     */
    public class ThreadLocalSingleton {
    
        private ThreadLocalSingleton() {}
    
        // TODO: 2019-05-26 此处可与懒汉内部类式单例进行比较 
        private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
            @Override
            protected ThreadLocalSingleton initialValue() {
                return new ThreadLocalSingleton();
            }
        };
    
        public static ThreadLocalSingleton getInstance(){
            return threadLocalInstance.get();
        }
    }
    
    /**
     * ThreadLocalSingletonTest类
     * ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,具体原因如下:单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以 空间换时间来实现线程间隔离的。
     * @author wangjixue
     * @date 2019-05-26 00:52
     */
    public class ThreadLocalSingletonTest {
    
        public static void main(String[] args) {
            System.err.println(ThreadLocalSingleton.getInstance());
            System.err.println(ThreadLocalSingleton.getInstance());
            System.err.println(ThreadLocalSingleton.getInstance());
            System.err.println(ThreadLocalSingleton.getInstance()==ThreadLocalSingleton.getInstance());
            System.out.println("========多线程调用=======");
    
            Thread t1 = new Thread(new ThreadLocalExectorThread());
            Thread t2 = new Thread(new ThreadLocalExectorThread());
            t1.start();
            t2.start();
        }
    }
    
    class  ThreadLocalExectorThread implements Runnable{
    
        @Override
        public void run() {
            ThreadLocalSingleton lazy = ThreadLocalSingleton.getInstance();
            System.err.println(Thread.currentThread().getName()+"---"+lazy);
        }
    }
    

    总结

     单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。

    相关文章

      网友评论

          本文标题:2019-05-26 单列设计模式

          本文链接:https://www.haomeiwen.com/subject/kabftctx.html