2019-05-26 单列设计模式

2019-05-26  本文已影响0人  竹blue

单例模式

饿汉式单例模式

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

/**
 * 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);
    }
}

总结

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

上一篇下一篇

猜你喜欢

热点阅读