RE|GoF的23种设计模式-3

单例模式

简介

  1. 单例模式是一种经常使用的软件设计模式,其定义是单例对象的类只能容许一个实例存在。java

  2. 许多时候整个系统只须要拥有一个的全局对象,这样有利于咱们协调系统总体的行为。好比在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,而后服务进程中的其余对象再经过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。设计模式

基本的实现思路

a. 单例模式要求类可以有返回对象一个引用(永远是同一个)和一个得到该实例的方法(必须是静态方法,一般使用getInstance这个名称)。缓存

b. 单例的实现主要是经过如下两个步骤: 将该类的构造方法定义为私有方法,这样其余处的代码就没法经过调用该类的构造方法来实例化该类的对象,只有经过该类提供的静态方法来获得该类的惟一实例; 在该类内提供一个静态方法,当咱们调用这个方法时,若是类持有的引用不为空就返回这个引用,若是类保持的引用为空就建立该类的实例并将实例的引用赋予该类保持的引用。安全

注意事项

单例模式在多线程的应用场合下必须当心使用。若是当惟一实例还没有建立时,有两个线程同时调用建立方法,那么它们同时没有检测到惟一实例的存在,从而同时各自建立了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例惟一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会下降效率)。服务器

1.饿汉式 (线程安全)

饿汉式单例是在类加载的时候就当即初始化,而且建立单例对象。绝对线程安全,在线 程还没出现之前就是实例化了,不可能存在访问安全问题。多线程

优势:没有加任何的锁、执行效率比较高,在用户体验上来讲,比懒汉式更好。并发

缺点:类加载的时候就初始化,无论用与不用都占着空间,浪费了内存,有可能占着茅 坑不拉屎。ide

/** * 静态变量,在类初始化的时候就建立了这个对象,占资源 */
private final static HungrySingPrinciple hungrySingPrinciple = new HungrySingPrinciple();

/** * 静态大代码块的写法 */
// private static HungrySingPrinciple hungrySingPrinciple;
//
// static {
// hungrySingPrinciple = new HungrySingPrinciple();
// }

private HungrySingPrinciple() {}

public static HungrySingPrinciple getInstance() {
    return hungrySingPrinciple;
}

复制代码

2.懒汉式 (线程不安全)

懒汉式被外部类调用的时候内部类才会加载,下面看懒汉式单例的简单测试

优势:这种写法起到了 Lazy Loading(延迟加载)的效果,可是只能在单线程下使用。flex

缺点:若是在多线程下,一个线程进入了if (slackerSingleton == null)判断语句块,还将来得及往下执行,另外一个线程也经过了这个判断语句,这时便会产生多个实例。因此在多线程环境下不可以使用这种方式。

private static LazySingPrinciple lazySingPrinciple;

private LazySingPrinciple() {}

public static LazySingPrinciple getInstance() {
    if (lazySingPrinciple == null) lazySingPrinciple = new LazySingPrinciple();
    return lazySingPrinciple;
}

// 测试

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    //信号量,此处用于控制并发的线程数
    final Semaphore semaphore = new Semaphore(100);
    //闭锁,可实现计数器递减
    final CountDownLatch countDownLatch = new CountDownLatch(100);
    for (int i = 0  ; i < 100 ; i++) {
        executorService.execute(() -> {
            try {
                //执行此方法用于获取执行许可,当总计未释放的许可数不超过200时,
                //容许通行,不然线程阻塞等待,直到获取到许可。
                semaphore.acquire();
                System.out.println(LazySingPrinciple.getInstance());
                //释放许可
                semaphore.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
            //闭锁减一
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();//线程阻塞,直到闭锁值为0时,阻塞才释放,继续往下执行
    executorService.shutdown();
}

// 结果
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4c4b666e // 不同
com.reape.design.pattern.sing.LazySingPrinciple@6f1cf112 // 不同
com.reape.design.pattern.sing.LazySingPrinciple@15585f4c // 不同
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5
com.reape.design.pattern.sing.LazySingPrinciple@4e36b2b5

复制代码

3.懒汉式【加锁-同步方法】 (线程安全)

private static SynchronizationMethodLazySingPrinciple synchronizationMethodLazySingPrinciple;

private SynchronizationMethodLazySingPrinciple() {}

// 锁住了方法----效率低下
public synchronized static SynchronizationMethodLazySingPrinciple getInstance() {
    if (synchronizationMethodLazySingPrinciple == null)
        synchronizationMethodLazySingPrinciple = new SynchronizationMethodLazySingPrinciple();
    return synchronizationMethodLazySingPrinciple;
}


复制代码

优势:这种写法起到了 Lazy Loading(延迟加载)的效果,能够在多线程下使用。

缺点:效率过低了,每一个线程在想得到类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想得到该类实例,直接return就好了。方法进行同步效率过低要改进。

4.懒汉式【加锁-同步代码块】 (线程安全)

private static SynchronizationBlockLazySingPrinciple synchronizationBlockLazySingPrinciple;

private SynchronizationBlockLazySingPrinciple() {}

public static SynchronizationBlockLazySingPrinciple getInstance() {
    // 若是此处为null 者进入同步代码块,若是同时也有其余方法也进入,同时执行完成了建立了实例
    // 若是咱们不判断null则会建立多个SynchronizationBlockLazySingPrinciple实例
    if (synchronizationBlockLazySingPrinciple == null) {
        synchronized (SynchronizationBlockLazySingPrinciple.class) {
            if (synchronizationBlockLazySingPrinciple == null)
                synchronizationBlockLazySingPrinciple = new SynchronizationBlockLazySingPrinciple();
        }
    }
    return synchronizationBlockLazySingPrinciple;
}
    
复制代码

优势:这种写法起到了 Lazy Loading(延迟加载)的效果,能够在多线程下使用,效率较高。

缺点:会被反射暴力破解(下面讲解)

5.静态内部类 (线程安全)

/** * 建立一个私有的静态内部类 * 静态内部类是在外面的类实例化后建立的类 */
private static class SingletonInstance {
    private final static InnerClassSingPrinciple instance = new InnerClassSingPrinciple();
}

/** * 私有的外部类不能实例化 */
private InnerClassSingPrinciple() {}

/** * 返回实例化的对象 */
public static InnerClassSingPrinciple getInstance() {
    return SingletonInstance.instance;
}

复制代码

优势:这种写法起到了 Lazy Loading(延迟加载)的效果,能够在多线程下使用,效率较高。

缺点:会被反射暴力破解(下面讲解)

6.注册式单例[枚举单例] (线程安全)

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

public enum  EnumSingPrinciple {

    INSTANCE;

    private EnumSingPrincipleObject object;

    EnumSingPrinciple() {
        this.object = new EnumSingPrincipleObject();
    }

    public EnumSingPrincipleObject getEnumSingPrincipleObject() {
        return object;
    }

}

复制代码

优势:这种写法起到了 Lazy Loading(延迟加载)的效果,能够在多线程下使用,效率较高, 并且不会被反射暴力破解(下面讲解)。

7.ThreadLocal 线程单例 (单线程安全)

ThreadLocal 不能保证其建立的对象是全局惟一,可是能保证在单个线程中是惟一的,天生的线程安全。

public class ThreadLocalSingPrinciple {

    private static final ThreadLocal<ThreadLocalSingPrinciple> threadLocalInstance =
            ThreadLocal.withInitial(ThreadLocalSingPrinciple::new);
    
    private ThreadLocalSingPrinciple(){}

    public static ThreadLocalSingPrinciple getInstance(){
        return threadLocalInstance.get();
    }
}

// 测试

public static void main(String[] args) throws InterruptedException {
    System.out.println(ThreadLocalSingPrinciple.getInstance());
    System.out.println(ThreadLocalSingPrinciple.getInstance());
    System.out.println(ThreadLocalSingPrinciple.getInstance());
    System.out.println(ThreadLocalSingPrinciple.getInstance());

    new Thread(){
        @Override
        public void run() {
            super.run();
            System.out.println("# " + ThreadLocalSingPrinciple.getInstance());
            System.out.println("# " + ThreadLocalSingPrinciple.getInstance());
            System.out.println("# " + ThreadLocalSingPrinciple.getInstance());
            System.out.println("# " + ThreadLocalSingPrinciple.getInstance());
        }
    }.start();
    
}

// 结果
com.reape.design.pattern.sing.ThreadLocalSingPrinciple@41629346
com.reape.design.pattern.sing.ThreadLocalSingPrinciple@41629346
com.reape.design.pattern.sing.ThreadLocalSingPrinciple@41629346
com.reape.design.pattern.sing.ThreadLocalSingPrinciple@41629346
# com.reape.design.pattern.sing.ThreadLocalSingPrinciple@6018d1f6
# com.reape.design.pattern.sing.ThreadLocalSingPrinciple@6018d1f6
# com.reape.design.pattern.sing.ThreadLocalSingPrinciple@6018d1f6
# com.reape.design.pattern.sing.ThreadLocalSingPrinciple@6018d1f6


// 特色
咱们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。
ThreadLocal将全部的对象所有放在 ThreadLocalMap中,为每一个线程都提供一个对象,其实是以空间换时间来实现线程间隔离的。
复制代码

总结

单例模式能够保证内存里只有一个实例,减小了内存开销;能够避免对资源的多重占用。单例模式看起来很是简单,实现起来其实也很是简单。

单例破解

反射破坏单例

public static void main(String[] args) {
    List<Class<?>> classList = new ArrayList<>(
            Arrays.asList(
                    HungrySingPrinciple.class,
                    LazySingPrinciple.class,
                    SynchronizationBlockLazySingPrinciple.class,
                    SynchronizationMethodLazySingPrinciple.class,
                    InnerClassSingPrinciple.class,
                    EnumSingPrinciple.class));
    classList.forEach(item -> {
        try {
            //经过反射拿到私有的构造方法
            Constructor c = item.getDeclaredConstructor();
            //强制访问,强吻,不肯意也要吻
            c.setAccessible(true);
            //暴力初始化
            Object o1 = c.newInstance();
            Object o2 = c.newInstance();
            System.out.println(item.getName() + " : " + (o1 == o2));
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}

// 结果
com.reape.design.pattern.sing.HungrySingPrinciple  : false
com.reape.design.pattern.sing.LazySingPrinciple  : false
com.reape.design.pattern.sing.SynchronizationBlockLazySingPrinciple  : false
com.reape.design.pattern.sing.SynchronizationMethodLazySingPrinciple  : false
com.reape.design.pattern.sing.InnerClassSingPrinciple  : false

java.lang.NoSuchMethodException: com.reape.design.pattern.sing.EnumSingPrinciple.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.reape.design.pattern.sing.ReflexDestructionSing.lambda$main$0(ReflexDestructionSing.java:26)
	at java.util.ArrayList.forEach(ArrayList.java:1249)
	at com.reape.design.pattern.sing.ReflexDestructionSing.main(ReflexDestructionSing.java:23)

// 总结: 除了枚举其余的单例都会被反射破解
// 如何防止单例被反射破解?

// 饿汉式,懒汉式 都须要一个变量来标记当前的类是否被加载,可是以前都是保证惟一的入口。
// 经过反射很容易就出现第二个入口,因此必须使用一个反射访问不到的属性来标记.
// 咱们使用内部类,这样又和内部类单例矛盾。
// 下面介绍内部类防御反射破解

private InnerClassSingPrinciple() {
    // 这个地方不能去掉判断-->由于第一次执行的时候 SingletonInstance.instance 为 null
    if (SingletonInstance.instance != null) throw new RuntimeException("滚 ~~");
}

// 为何枚举不会被破坏?
// 由于枚举自己就是一个单例的类,他在构造方法实现了,跟上述错很少的判断类型,来判断从而拒绝重复建立。

复制代码

序列化破坏单例

public static void main(String[] args) {
    InnerClassSingPrinciple o1 = InnerClassSingPrinciple.getInstance();
    FileOutputStream fos = null;
    FileInputStream fis = null;
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;
    try {
        // 写入文件
        fos = new FileOutputStream("SeriableSingleton");
        oos = new ObjectOutputStream(fos);
        oos.writeObject(o1);
        oos.flush();
        oos.close();
        // 读取文件
        fis = new FileInputStream("SeriableSingleton");
        ois = new ObjectInputStream(fis);
        InnerClassSingPrinciple o2 = (InnerClassSingPrinciple) ois.readObject();
        ois.close();
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1 == o2);  // false
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (fos != null) fos.close();
            if (fis != null) fis.close();
            if (oos != null) oos.close();
            if (ois != null) ois.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 结果
com.reape.design.pattern.sing.InnerClassSingPrinciple@7f31245a
com.reape.design.pattern.sing.InnerClassSingPrinciple@6d6f6e28
false

// 若是咱们吧InnerClassSingPrinciple类里面写入一个readResolve方法,以下
private Object readResolve(){
    return SingletonInstance.instance;
}

// 结果
com.reape.design.pattern.sing.InnerClassSingPrinciple@7f31245a
com.reape.design.pattern.sing.InnerClassSingPrinciple@7f31245a
true

// 为何?
// 1. 咱们去看看 InnerClassSingPrinciple o2 = (InnerClassSingPrinciple) ois.readObject(); 的readObject方法
// readObject --->
// readObject0 --->
// readObject0[方法里面] case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));


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 {
        // 1.执行这个方法
        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);

    // 2.上面赋值了obj
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod()) // 3.执行了这个方法
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

// 对应上面的 --> 1
boolean isInstantiable() {
    // 是判断一下构造方法是否为空,构造方法不为空就返回 true
    requireInitialized();
    return (cons != null);
}



// 对应上面的 --> 3
Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException {
    requireInitialized();
    if (readResolveMethod != null) {
        try {
            // 经过反射调用了 readResolve 方法
            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();
    }
}

 
// 虽然,增长 readResolve()方法返回实例,解决了单例被破坏的问题。
// 可是,咱们经过分析源码以及调试,咱们能够看到实际上实例化了两次,只不过新建立的对象没有被返回而已。
// 动态代理的时候没有调用 原来的 readResolve 而是调用的为 重写的 readResolve
复制代码
相关文章
相关标签/搜索