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

本文主要介绍Java中的单例模式,单例模式确保类只有一个实例。阐述了单例模式的基本思路和注意事项,列举了饿汉式、懒汉式等多种实现方式及其优缺点,还提到了单例模式在多线程下的使用问题。此外,介绍了反射和序列化对单例的破坏。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单例模式

简介

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

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

基本的实现思路

a. 单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)。

b. 单例的实现主要是通过以下两个步骤: 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例; 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

注意事项

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

1.饿汉式 (线程安全)

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

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

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


/**
 * 静态变量,在类初始化的时候就创建了这个对象,占资源
 */
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(延迟加载)的效果,但是只能在单线程下使用。

缺点:如果在多线程下,一个线程进入了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
复制代码

转载于:https://juejin.im/post/5cde02cbf265da039b089612

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值