设计模式2-单例模式

劝退警告,本文非常长长长......

单例模式定义及应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
单例模式是创建型模式。

单例模式优点:

  1. 一个类在内存中只有一个对象,节省内存空间
  2. 避免频繁的创建销毁对象,可以提高性能
  3. 避免对共享资源的多重占用
  4. 可以全局访问

单例模式适用场景:

  1. 需要频繁实例化然后销毁的对象
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象
  3. 有状态的工具类对象
  4. 频繁访问数据库或文件的对象
  5. J2EE标准中的ServletContext、ServletContextConfig等、Spring框架应用中的ApplicationContext、数据库的连接池等也都是单例模式。

创建单例主要有以下几种方式:

  • 饿汉式单例
    • 简单的饿汉式单例
    • 静态代码块的饿汉式单例
  • 懒汉式单例
    • 简单实现的懒汉式单例
    • 同步方法式单例
    • 双重检查锁式单例
    • 静态内部类式单例
  • 注册式单例
    • 枚举式单例
    • 容器式单例
  • 线程单例

单例模式的选型考虑:

  1. 是否延迟加载
  2. 是否线程安全
  3. 并发访问性能
  4. 是否可以防止反射和反序列化破坏

一、饿汉式单例模式

饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。
它绝对线程安全,因为当第一个线程访问它的时候,就通过类加载机制完成了实例化,甚至后续的访问线程还没创建完成

1、标准的饿汉式

标准的饿汉式代码如下:

/**
 * 统计访问网站的用户数量-饿汉式1-标准式
 * 优点:执行效率高,性能高,没有任何的锁
 * 缺点:某些情况下,可能会造成内存浪费
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class HungryStandardWebCounter {
    /**
     * 类加载时直接初始化
     */
    private static final HungryStandardWebCounter WEB_COUNTER = new HungryStandardWebCounter();
    private final AtomicInteger count;

    private HungryStandardWebCounter() {
        count = new AtomicInteger();
    }

    public static HungryStandardWebCounter getInstance() {
        return WEB_COUNTER;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}", Thread.currentThread().getName(), count.incrementAndGet(), System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }
}

客户端并发测试:

/**
 * 单例模式-饿汉式1-标准式
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class HungryStandardWebCounterClient {
    public static void main(String[] args) throws InterruptedException {
        int n = 10;
        CountDownLatch countDownLatch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                HungryStandardWebCounter.getInstance().count();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        log.info("总访问人数:{}", HungryStandardWebCounter.getInstance().getCount());
    }
}

//执行结果,以下结果中线程是随机的,并非固定,但总访问人数总是10,并且可以看到每个线程的统计器是同一个对象
Thread-1是第1个访问网站,统计器:1741587259
Thread-9是第7个访问网站,统计器:1741587259
Thread-2是第2个访问网站,统计器:1741587259
Thread-5是第5个访问网站,统计器:1741587259
Thread-4是第4个访问网站,统计器:1741587259
Thread-7是第10个访问网站,统计器:1741587259
Thread-8是第8个访问网站,统计器:1741587259
Thread-3是第3个访问网站,统计器:1741587259
Thread-10是第9个访问网站,统计器:1741587259
Thread-6是第6个访问网站,统计器:1741587259
总访问人数:10

2、静态代码块的饿汉式

另外一种写法,利用静态代码块的机制,本质上没太大区别,除非需要在实例初始化之前做一些其他的操作,比如加载配置文件,如下:

/**
 * 统计访问网站的用户数量-饿汉式2-静态代码块
 * 优点:执行效率高,性能高,没有任何的锁
 * 缺点:某些情况下,可能会造成内存浪费
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class HungryStaticWebCounter {
    private final AtomicInteger count;
    private static final HungryStaticWebCounter WEB_COUNTER;

    static {
        //初始化之前可以做一些其他处理,比如读取配置文件等
        log.info("饿汉式2-静态代码块初始化加载");
        WEB_COUNTER = new HungryStaticWebCounter();
    }

    private HungryStaticWebCounter() {
        count = new AtomicInteger();
    }

    public static HungryStaticWebCounter getInstance() {
        return WEB_COUNTER;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}", Thread.currentThread().getName(), count.incrementAndGet(), System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }
}

客户端并发测试:

/**
 * 单例模式-饿汉式2-静态代码块
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class HungryStaticWebCounterClient {
    public static void main(String[] args) throws InterruptedException {
        int n = 10;
        CountDownLatch countDownLatch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                HungryStaticWebCounter.getInstance().count();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        log.info("总访问人数:" + HungryStaticWebCounter.getInstance().getCount());
    }
}

//执行结果,以下结果中线程是随机的,并非固定,但总访问人数总是10,并且可以看到每个线程的统计器是同一个对象
饿汉式2-静态代码块初始化加载
Thread-1是第1个访问网站,统计器:841578049
Thread-7是第6个访问网站,统计器:841578049
Thread-10是第10个访问网站,统计器:841578049
Thread-4是第3个访问网站,统计器:841578049
Thread-2是第2个访问网站,统计器:841578049
Thread-6是第9个访问网站,统计器:841578049
Thread-3是第5个访问网站,统计器:841578049
Thread-9是第8个访问网站,统计器:841578049
Thread-5是第4个访问网站,统计器:841578049
Thread-8是第7个访问网站,统计器:841578049
总访问人数:10

总结:

两种写法都非常简单,也非常好理解。
多数情况下直接使用标准式,有特殊的实例化前置要求时使用静态代码块。

饿汉式单例优点:执行效率高,性能高,没有任何的锁。
饿汉式单例缺点:某些情况下,可能会造成内存浪费,因为不管对象用与不用都占着空间。

二、懒汉式单例模式

为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法。
懒汉式单例模式的特点是,单例对象要在被使用时才会被初始化。

1、简单实现的懒汉式

/**
 * 统计访问网站的用户数量-懒汉式1-简单实现
 * 优点:节省了内存
 * 缺点:性能相较饿汉式低,线程不安全
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazySimpleWebCounter {
    private final AtomicInteger count;
    private static LazySimpleWebCounter counter;

    private LazySimpleWebCounter() {
        count = new AtomicInteger();
    }

    public static LazySimpleWebCounter getInstance() {
        if (null == counter) {
            counter = new LazySimpleWebCounter();
        }
        return counter;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}", 
                Thread.currentThread().getName(), 
                count.incrementAndGet(), 
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }
}

这样写非常简单,但带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。
客户端测试如下:

/**
 * 单例模式-懒汉式1-简单实现
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazySimpleWebCounterClient {
    public static void main(String[] args) throws InterruptedException {
        int n = 10;
        CountDownLatch countDownLatch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                LazySimpleWebCounter.getInstance().count();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        log.info("总访问人数:" + LazySimpleWebCounter.getInstance().getCount());
    }
}

//执行结果,以下结果是随机的,并非固定
Thread-8是第7个访问网站,统计器:1762222186
Thread-5是第5个访问网站,统计器:1762222186
Thread-6是第4个访问网站,统计器:1762222186
Thread-7是第6个访问网站,统计器:1762222186
Thread-2是第2个访问网站,统计器:1762222186
Thread-9是第8个访问网站,统计器:1762222186
Thread-4是第3个访问网站,统计器:1762222186
Thread-1是第1个访问网站,统计器:1215424588
Thread-10是第9个访问网站,统计器:1762222186
Thread-3是第1个访问网站,统计器:1762222186
总访问人数:9

以上为随机的运行结果,总访问人数为9,正确应该是10。
可以看到问题出现在Thread-1和Thread-3,两个线程都是第1个访问,并且统计器对象不一样。
原因如下:

    public static LazySimpleWebCounter getInstance() {
        if (null == counter) {//Thread-3在判断if (null == counter)时,Thread-1的 new LazySimpleWebCounter() 还没完成
            counter = new LazySimpleWebCounter();//导致Thread-3也执行了new LazySimpleWebCounter() ,把Thread-1创建的实例覆盖掉了
        }
        return counter;
    }

总结:

优点:非常简单,节省了内存
缺点:性能相较饿汉式低,线程不安全

2、同步方法式单例

和简单实现的代码区别是getInstance方法添加了_synchronized_关键字,保证了方法的线程安全,但同时也降低了方法的性能

/**
 * 统计访问网站的用户数量-懒汉式2-同步方法
 * 优点:节省了内存,线程安全
 * 缺点:性能低
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazySynchronizedMethodWebCounter {
    private final AtomicInteger count;
    private static LazySynchronizedMethodWebCounter counter;

    private LazySynchronizedMethodWebCounter() {
        count = new AtomicInteger();
    }

    public static synchronized LazySynchronizedMethodWebCounter getInstance() {
        if (null == counter) {
            counter = new LazySynchronizedMethodWebCounter();
        }
        return counter;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}", 
                Thread.currentThread().getName(), 
                count.incrementAndGet(), 
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }
}

客户端测试:

/**
 * 单例模式-懒汉式2-同步方法
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazySynchronizedMethodWebCounterClient {
    public static void main(String[] args) throws InterruptedException {
        int n = 10;
        CountDownLatch countDownLatch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                LazySynchronizedMethodWebCounter.getInstance().count();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        log.info("总访问人数:" + LazySynchronizedMethodWebCounter.getInstance().getCount());
    }
}

//执行结果,以下结果中线程是随机的,并非固定,但总访问人数总是10,并且可以看到每个线程的统计器是同一个对象
Thread-2是第3个访问网站,统计器:1215424588
Thread-9是第9个访问网站,统计器:1215424588
Thread-6是第7个访问网站,统计器:1215424588
Thread-4是第2个访问网站,统计器:1215424588
Thread-1是第1个访问网站,统计器:1215424588
Thread-8是第8个访问网站,统计器:1215424588
Thread-7是第6个访问网站,统计器:1215424588
Thread-10是第10个访问网站,统计器:1215424588
Thread-3是第4个访问网站,统计器:1215424588
Thread-5是第5个访问网站,统计器:1215424588
总访问人数:10

用_synchronized_给方法加锁时,在线程数量较多的情况下,如果CPU分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。

总结:

优点:节省了内存,线程安全
缺点:给方法加锁,性能低

3、双重检查锁式单例

一种兼顾线程安全又能提升程序性能的方式
getInstance方法里面使用了两个if (null == counter)判断

/**
 * 统计访问网站的用户数量-懒汉式3-双重检查锁
 * 优点:性能高了,线程安全了,也能节约内存
 * 缺点:可读性差,不易理解,不够优雅,可能有指令重排序的问题
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyDoubleCheckWebCounter {
    private final AtomicInteger count;
    private static volatile LazyDoubleCheckWebCounter counter;

    private LazyDoubleCheckWebCounter() {
        count = new AtomicInteger();
    }

    public static LazyDoubleCheckWebCounter getInstance() {
        //第一个检查用来判断是否要阻塞
        if (null == counter) {
            log.info("{}开始加锁", Thread.currentThread().getName());
            synchronized (LazyDoubleCheckWebCounter.class) {
                //第二个检查用来判断是否需要创建实例
                if (null == counter) {
                    log.info("{}开始创建实例", Thread.currentThread().getName());
                    counter = new LazyDoubleCheckWebCounter();
                }
            }
        }
        return counter;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}",
                Thread.currentThread().getName(),
                count.incrementAndGet(),
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }
}

客户端测试:

/**
 * 单例模式-懒汉式3-双重检查锁
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyDoubleCheckWebCounterClient {
    public static void main(String[] args) throws InterruptedException {
        int n = 10;
        CountDownLatch countDownLatch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                LazyDoubleCheckWebCounter.getInstance().count();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        log.info("总访问人数:" + LazyDoubleCheckWebCounter.getInstance().getCount());
    }
}

//执行结果,以下结果中线程是随机的,并非固定,但总访问人数总是10,并且可以看到每个线程的统计器是同一个对象
Thread-8开始加锁
Thread-5开始加锁
Thread-4开始加锁
Thread-1开始加锁
Thread-3开始加锁
Thread-6开始加锁
Thread-10开始加锁
Thread-9开始加锁
Thread-7开始加锁
Thread-2开始加锁
Thread-8开始创建实例
Thread-3是第3个访问网站,统计器:678220082
Thread-9是第8个访问网站,统计器:678220082
Thread-2是第4个访问网站,统计器:678220082
Thread-4是第5个访问网站,统计器:678220082
Thread-10是第9个访问网站,统计器:678220082
Thread-6是第6个访问网站,统计器:678220082
Thread-7是第10个访问网站,统计器:678220082
Thread-1是第2个访问网站,统计器:678220082
Thread-8是第1个访问网站,统计器:678220082
Thread-5是第7个访问网站,统计器:678220082
总访问人数:10

以上结果可以看到,由于加了锁,导致实例创建速度变慢,10个线程都通过了第一个_if _(_null _== counter),如果没有第二个if判断,虽然不能同时创建实例,但是最终也还是会按照加锁的顺序去创建实例,所以在同步块内再进行第二个_if _(_null _== counter)判断,就可以避免后来拿到锁的线程重复创建实例,可以看到只有Thread-8完成了实例创建,其他线程在第二个if判断时实例已经被实例化了。

指令重排序问题:
counter 成员变量加了volatile 关键字修饰,是为了防止指令重排。
因为counter = new LazyDoubleCheckWebCounter(); 并不是一个原子操作,其在JVM中至少做了三件事:

  1. 给counter在堆上分配内存空间。(分配内存)
  2. 调用LazyDoubleCheckWebCounter的构造函数等来初始化counter。(初始化)
  3. 将counter对象指向分配的内存空间。(执行完这一步counter就不是null了)

在没有volatile修饰时,执行顺序可以是1,2,3,也可以是1,3,2。假设有两个线程,当一个线程先执行了3,还没执行2,此时第二个线程来到第一个check,发现counter不为null,就直接返回了,这就出现问题,这时的counter并没有完全完成初始化。

听说高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。当然本文未验证

总结:

兼顾线程安全又能提升程序性能
优点:性能高了,线程安全了,也能节约内存
缺点:可读性差,不易理解,不够优雅

4、静态内部类式单例

兼顾饿汉式的内存浪费问题和synchronized关键字的性能问题

/**
 * 统计访问网站的用户数量-懒汉式4-静态内部类
 * 优点:写法优雅,利用了Java本身语法特点,性能高,避免了内存浪费
 * 缺点:可以被反射破坏,可以被序列化破坏
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounter {
    private final AtomicInteger count;
    private static LazyStaticInnerClassWebCounter counter;

    private LazyStaticInnerClassWebCounter() {
        count = new AtomicInteger();
    }

    public static synchronized LazyStaticInnerClassWebCounter getInstance() {
        return LazyHolder.COUNTER;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}",
                Thread.currentThread().getName(),
                count.incrementAndGet(),
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }

    /**
     * 静态内部持有类
     */
    private static class LazyHolder {
        static {
            log.info("LazyHolder被{}加载", Thread.currentThread().getName());
        }

        private static final LazyStaticInnerClassWebCounter COUNTER = new LazyStaticInnerClassWebCounter();
    }
}

客户端测试:

/**
 * 单例模式-懒汉式4-静态内部类
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounterClient {
    public static void main(String[] args) throws InterruptedException {
        int n = 10;
        CountDownLatch countDownLatch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                LazyStaticInnerClassWebCounter.getInstance().count();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        log.info("总访问人数:" + LazyStaticInnerClassWebCounter.getInstance().getCount());
    }
}

//执行结果,以下结果中线程是随机的,并非固定,但总访问人数总是10,并且可以看到每个线程的统计器是同一个对象
LazyHolder被Thread-1加载
Thread-10是第5个访问网站,统计器:942954247
Thread-8是第4个访问网站,统计器:942954247
Thread-5是第6个访问网站,统计器:942954247
Thread-6是第7个访问网站,统计器:942954247
Thread-9是第3个访问网站,统计器:942954247
Thread-7是第2个访问网站,统计器:942954247
Thread-2是第10个访问网站,统计器:942954247
Thread-4是第8个访问网站,统计器:942954247
Thread-3是第9个访问网站,统计器:942954247
Thread-1是第1个访问网站,统计器:942954247
总访问人数:10

以上结果可以看到,LazyHolder被Thread-1加载,也就是被第一个访问的线程加载了,并且在加载的时候进行了实例化,后续的线程不会再重复加载。

那这种方式的缺点是什么呢,缺点是可以被反射破坏(当然前面的几种方式也是可以被破坏的)。

反射破坏单例

即使构造方法被设置为private,利用反射也是可以破坏的。

/**
 * 单例模式-懒汉式4-静态内部类-反射破坏单例
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounterReflectClient {
    public static void main(String[] args) throws Exception {
        Class<lazystaticinnerclasswebcounter> clazz = LazyStaticInnerClassWebCounter.class;
        Constructor<lazystaticinnerclasswebcounter> constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);//强制构造方法可访问
        LazyStaticInnerClassWebCounter instance1 = constructor.newInstance();
        LazyStaticInnerClassWebCounter instance2 = constructor.newInstance();
        log.info("instance1==instance2? {}", instance1 == instance2);
    }
}

//执行结果
instance1==instance2? false

从结果可以看到,创建了两个不同的实例。
我们可以做一些优化,在构造方法中做一些限制,禁止反射调用构造方法,如下

/**
 * 统计访问网站的用户数量-懒汉式4-静态内部类--构造方法添加限制避免反射破坏
 * 优点:利用了Java本身语法特点,性能高,避免了内存浪费
 * 缺点:看起来好像没太大缺点了
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounter2 {
    private final AtomicInteger count;
    private static LazyStaticInnerClassWebCounter2 counter;

    private LazyStaticInnerClassWebCounter2() {
        if (LazyHolder.COUNTER != null){
            throw new RuntimeException("不允许非法访问");
        }
        count = new AtomicInteger();
    }

    public static synchronized LazyStaticInnerClassWebCounter2 getInstance() {
        return LazyHolder.COUNTER;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}",
                Thread.currentThread().getName(),
                count.incrementAndGet(),
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }

    /**
     * 静态内部持有类
     */
    private static class LazyHolder {
        static {
            log.info("LazyHolder被{}加载", Thread.currentThread().getName());
        }

        private static final LazyStaticInnerClassWebCounter2 COUNTER = new LazyStaticInnerClassWebCounter2();
    }
}

客户端测试:

/**
 * 单例模式-懒汉式4-静态内部类-反射破坏单例
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounterReflectClient {
    public static void main(String[] args) throws Exception {
        Class<lazystaticinnerclasswebcounter> clazz = LazyStaticInnerClassWebCounter.class;
        Constructor<lazystaticinnerclasswebcounter> constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyStaticInnerClassWebCounter instance1 = constructor.newInstance();
        LazyStaticInnerClassWebCounter instance2 = constructor.newInstance();
        log.info("instance1==instance2? {}", instance1 == instance2);
        
        Class<lazystaticinnerclasswebcounter2> clazz2 = LazyStaticInnerClassWebCounter2.class;
        Constructor<lazystaticinnerclasswebcounter2> constructor2 = clazz2.getDeclaredConstructor(null);
        constructor2.setAccessible(true);
        LazyStaticInnerClassWebCounter2 instance3 = constructor2.newInstance();
        LazyStaticInnerClassWebCounter2 instance4 = constructor2.newInstance();
        log.info("instance3==instance4? {}", instance3 == instance4);
    }
}

//执行结果
instance1==instance2? false
LazyHolder被main加载
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at top.ccheng.design.learn.singleton.lazy.LazyStaticInnerClassWebCounterReflectClient.main(LazyStaticInnerClassWebCounterReflectClient.java:28)
Caused by: java.lang.RuntimeException: 不允许非法访问
	at top.ccheng.design.learn.singleton.lazy.LazyStaticInnerClassWebCounter2.<init>(LazyStaticInnerClassWebCounter2.java:22)
	... 5 more

从结果可以看到,构造方法已经被限制调用了。
至此,自认为最牛的单例模式的实现方式便大功告成。

但是,上面看似完美的单例写法还是有可能被破坏......

序列化破坏单例

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

在测试前,我们先给单例类加上Serializable接口,否则不支持序列化。同时我们保留了构造方法的异常限制

/**
 * 统计访问网站的用户数量-懒汉式4-静态内部类--构造方法添加限制避免反射破坏
 * 优点:写法优雅,利用了Java本身语法特点,性能高,避免了内存浪费
 * 缺点:看起来好像没太大缺点了
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounter2 implements Serializable {
    private final AtomicInteger count;
    private static LazyStaticInnerClassWebCounter2 counter;

    private LazyStaticInnerClassWebCounter2() {
        if (LazyHolder.COUNTER != null){
            throw new RuntimeException("不允许非法访问");
        }
        count = new AtomicInteger();
    }

    public static synchronized LazyStaticInnerClassWebCounter2 getInstance() {
        return LazyHolder.COUNTER;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}",
                Thread.currentThread().getName(),
                count.incrementAndGet(),
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }

    /**
     * 静态内部持有类
     */
    private static class LazyHolder {
        static {
            log.info("LazyHolder被{}加载", Thread.currentThread().getName());
        }

        private static final LazyStaticInnerClassWebCounter2 COUNTER = new LazyStaticInnerClassWebCounter2();
    }
}

客户端测试:

/**
 * 单例模式-懒汉式4-静态内部类-序列化破坏单例
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounterSerializableClient {
    public static void main(String[] args) throws Exception {
        LazyStaticInnerClassWebCounter2 counter1 = LazyStaticInnerClassWebCounter2.getInstance();
        //将counter1对象序列化写入LazyStaticInnerClassWebCounter2.obj文件
        String filename = "LazyStaticInnerClassWebCounter2.obj";
        FileOutputStream fos = new FileOutputStream(filename);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(counter1);
        oos.flush();
        oos.close();

        //从LazyStaticInnerClassWebCounter2.obj反序列化到counter2
        FileInputStream fis = new FileInputStream(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        LazyStaticInnerClassWebCounter2 counter2 = (LazyStaticInnerClassWebCounter2) ois.readObject();
        ois.close();
        
        log.info("counter1={}",counter1);
        log.info("counter2={}",counter2);
        log.info("counter1=counter2? {}", counter1 == counter2);
    }
}

//执行结果
LazyHolder被main加载
counter1=top.ccheng.design.learn.singleton.lazy.LazyStaticInnerClassWebCounter2@29f69090
counter2=top.ccheng.design.learn.singleton.lazy.LazyStaticInnerClassWebCounter2@105fece7
counter1=counter2? false

由测试结果,我们发现,反序列化出来的counter2与序列化之前的counter1是两个不同的对象,并且反序列过程并没有调用构造方法!!!

如何保证在序列化的情况下也能够实现单例模式呢?
其实很简单,只需要增加readResolve方法即可。

/**
 * 统计访问网站的用户数量-懒汉式4-静态内部类-构造方法添加限制避免反射破坏,同时增加readResolve方法避免序列化破坏
 * 优点:利用了Java本身语法特点,性能高,避免了内存浪费
 * 缺点:又要写内部类,又要限制构造方法,又要增加readResolve,好像不怎么优雅了
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounter3 implements Serializable {
    private final AtomicInteger count;
    private static LazyStaticInnerClassWebCounter3 counter;

    private LazyStaticInnerClassWebCounter3() {
        if (LazyHolder.COUNTER != null) {
            throw new RuntimeException("不允许非法访问");
        }
        count = new AtomicInteger();
    }

    public static synchronized LazyStaticInnerClassWebCounter3 getInstance() {
        return LazyHolder.COUNTER;
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}",
                Thread.currentThread().getName(),
                count.incrementAndGet(),
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }

    /**
     * 增加readResolve方法避免序列化破坏
     * @return
     */
    private Object readResolve() {
        return LazyHolder.COUNTER;
    }

    /**
     * 静态内部持有类
     */
    private static class LazyHolder {
        static {
            log.info("LazyHolder被{}加载", Thread.currentThread().getName());
        }

        private static final LazyStaticInnerClassWebCounter3 COUNTER = new LazyStaticInnerClassWebCounter3();
    }
}

客户端测试:

/**
 * 单例模式-懒汉式4-静态内部类-序列化破坏单例
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class LazyStaticInnerClassWebCounterSerializableClient2 {
    public static void main(String[] args) throws Exception {
        LazyStaticInnerClassWebCounter3 counter1 = LazyStaticInnerClassWebCounter3.getInstance();
        //将counter1对象序列化写入LazyStaticInnerClassWebCounter3.obj文件
        String filename = "LazyStaticInnerClassWebCounter3.obj";
        FileOutputStream fos = new FileOutputStream(filename);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(counter1);
        oos.flush();
        oos.close();

        //从LazyStaticInnerClassWebCounter3.obj反序列化到counter2
        FileInputStream fis = new FileInputStream(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        LazyStaticInnerClassWebCounter3 counter2 = (LazyStaticInnerClassWebCounter3) ois.readObject();
        ois.close();
        
        log.info("counter1={}",counter1);
        log.info("counter2={}",counter2);
        log.info("counter1=counter2? {}", counter1 == counter2);
    }
}

//执行结果
LazyHolder被main加载
counter1=top.ccheng.design.learn.singleton.lazy.LazyStaticInnerClassWebCounter3@29f69090
counter2=top.ccheng.design.learn.singleton.lazy.LazyStaticInnerClassWebCounter3@29f69090
counter1=counter2? true

从测试结果可以看到,增加了readResolve方法后,反序列化出来的counter2与序列化之前的counter1是同一个对象!

为什么会有这么神奇的事情发生呢,查看JDK的ObjectInputStream类的源码就可以找到答案。
java.io.ObjectInputStream#readObject调用了重写的java.io.ObjectInputStream#readObject0
image.png
java.io.ObjectInputStream#readObject0又调用了java.io.ObjectInputStream#readOrdinaryObject
image.png
java.io.ObjectInputStream#readOrdinaryObject方法先实例化了一个对象
然后判断是否存在readResolve方法,存在的话则通过反射调用readResolve方法读取
最后将readResolve结果覆盖掉前面实例化的对象
image.png
image.png

而readResolveMethod其实是通过ObjectStreamClass通过反射的方式加载进来
image.png

通过JDK源码分析我们可以看出,虽然增加了readResolve方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,就意味着内存分配开销也会随之增大。

总结:

懒汉式单例又有很多种写法,同时也衍生了线程安全问题。

三、注册式单例模式

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

1、枚举式单例

/**
 * 统计访问网站的用户数量-注册式1-枚举式
 * 优点:写法简洁,线程安全,防止反射和序列化破坏
 * 缺点:因为不是类所以无法使用继承等特性
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public enum RegisterEnumWebCounter {
    /**
     * 唯一实例
     */
    INSTANCE;
    
    private final AtomicInteger count;

    private RegisterEnumWebCounter() {
        count = new AtomicInteger();
    }
    
    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}", 
                Thread.currentThread().getName(), 
                count.incrementAndGet(), 
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }
}

客户端测试:

/**
 * 单例模式-注册式1-枚举式
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class RegisterEnumWebCounterClient {
    public static void main(String[] args){
        try {
            //测试是否线程安全
            int n = 10;
            CountDownLatch countDownLatch = new CountDownLatch(n);
            for (int i = 0; i < n; i++) {
                new Thread(() -> {
                    RegisterEnumWebCounter.INSTANCE.count();
                    countDownLatch.countDown();
                }).start();
            }
            countDownLatch.await();
            log.info("总访问人数:" + RegisterEnumWebCounter.INSTANCE.getCount());
        }catch (Exception e) {
            log.info("线程安全测试异常", e);
        }
        
        log.info("####################分割线######################");
        
        try {
            //测试是否可以反射破坏
            Class<registerenumwebcounter> clazz = RegisterEnumWebCounter.class;
            Constructor<registerenumwebcounter> constructor = clazz.getDeclaredConstructor(null);
            constructor.setAccessible(true);//强制构造方法可访问
            RegisterEnumWebCounter instance1 = constructor.newInstance();
            RegisterEnumWebCounter instance2 = constructor.newInstance();
            log.info("instance1==instance2? {}", instance1 == instance2);
        }catch (Exception e){
            log.info("反射破坏测试异常", e);
        }
        
        log.info("####################分割线######################");
        
        try {
            //测试是否可以序列化破坏
            RegisterEnumWebCounter counter1 = RegisterEnumWebCounter.INSTANCE;
            //将counter1对象序列化写入RegisterEnumWebCounter.obj文件
            String filename = "RegisterEnumWebCounter.obj";
            FileOutputStream fos = new FileOutputStream(filename);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(counter1);
            oos.flush();
            oos.close();

            //从RegisterEnumWebCounter.obj反序列化到counter2
            FileInputStream fis = new FileInputStream(filename);
            ObjectInputStream ois = new ObjectInputStream(fis);
            RegisterEnumWebCounter counter2 = (RegisterEnumWebCounter) ois.readObject();
            ois.close();

            log.info("counter1={}",counter1);
            log.info("counter2={}",counter2);
            log.info("counter1=counter2? {}", counter1 == counter2);
        }catch (Exception e){
            log.info("序列化破坏测试异常", e);
        }
    }
}


//执行结果
Thread-7是第10个访问网站,统计器:2128322798
Thread-8是第6个访问网站,统计器:2128322798
Thread-6是第9个访问网站,统计器:2128322798
Thread-2是第5个访问网站,统计器:2128322798
Thread-1是第1个访问网站,统计器:2128322798
Thread-4是第8个访问网站,统计器:2128322798
Thread-3是第7个访问网站,统计器:2128322798
Thread-9是第3个访问网站,统计器:2128322798
Thread-10是第2个访问网站,统计器:2128322798
Thread-5是第4个访问网站,统计器:2128322798
总访问人数:10
####################分割线######################
反射破坏测试异常 java.lang.NoSuchMethodException: top.ccheng.design.learn.singleton.register.RegisterEnumWebCounter.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082) ~[?:1.8.0_191]
	at java.lang.Class.getDeclaredConstructor(Class.java:2178) ~[?:1.8.0_191]
	at top.ccheng.design.learn.singleton.register.RegisterEnumWebCounterClient.main(RegisterEnumWebCounterClient.java:43) [classes/:?]

####################分割线######################
counter1=INSTANCE
counter2=INSTANCE
counter1=counter2? true

由测试结果可以看到,枚举单例模式写法简洁,线程安全,天生防止反射和序列化破坏!!!

Joshua Bloch大佬在《Effective Java》中明确表明,枚举类型实现的单例模式是最佳的方式。

揭开枚举单例的神秘面纱

为了揭开枚举单例的神秘面纱,我们通过Java反编译工具jad(下载地址:https://varaneckas.com/jad/),将编译好的RegisterEnumWebCounter.class反编译成RegisterEnumWebCounter.jad,通过idea打开查看源码

原来,枚举单例模式在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例模式的实现,所以线程安全。
image.png
再来看为啥反射不能破坏
我们可以看到是继承了Enum类,并调用了父类的构造方法,并没有无参构造方法,所以前面的反射调用抛出了NoSuchMethodException。
image.png
image.png

那如果反射调用有参的构造方法是否可行呢?其实也不行,但并非是构造方法有问题,构造方法并没有抛出异常。
其实是在java.lang.reflect.Constructor#newInstance方法中做了限制,如果是枚举类型,则直接抛出异常
image.png

最后来看一下为啥序列化也不能破坏
回到ObjectInputStream的源码
java.io.ObjectInputStream#readObject调用了java.io.ObjectInputStream#readObject0
java.io.ObjectInputStream#readObject0判断如果是枚举类型,则调用java.io.ObjectInputStream#readEnum
image.png
java.io.ObjectInputStream#readEnum实际上是通过java.lang.Enum#valueOf方法
image.png
java.lang.Enum#valueOf方法实际上是从枚举的容器中直接取出,并没有重新创建对象,所以反序列化也是同一个对象
image.png

2、容器式单例

在程序启动的时候就将所有的对象初始化放容器中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。
被管理的对象所对应的类本身并没有做单例限制,而是由容器的使用行为来提供这种单例。

举个例子:我们平时写了很多spring的service类,这些类我们本身并没有做单例限制,但是spring容器在启动的时候帮我们进行了初始化并放到容器中,我们在使用的时候通过注入等方式从容器中取出service对象,通过这种行为取得的对象是唯一的,但并没有限制我们不能自己new一个service,只是自己new一个的意义不大。

被容器管理的类:

/**
 * 统计访问网站的用户数量-注册式2-容器式-被管理的类
 * 
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class RegisterContainerWebCounter {
    
    private final AtomicInteger count;

    public RegisterContainerWebCounter() {
        count = new AtomicInteger();
    }
    
    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}", 
                Thread.currentThread().getName(), 
                count.incrementAndGet(), 
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }
}

容器类:

/**
 * 统计访问网站的用户数量-注册式2-容器式-容器类
 *
 * @author ccheng
 * @date 2021/12/5
 */
public class RegisterContainer {
    private RegisterContainer() {
    }

    private static final Map<string, object=""> IOC = new ConcurrentHashMap<>();

    public static void putInStance(String key, Object value) {
        if (key != null && !key.isEmpty() && value != null && !IOC.containsKey(key)) {
            IOC.put(key, value);
        }
    }

    public static Object getInstance(String key) {
        return IOC.get(key);
    }
}

客户端测试:

/**
 * 单例模式-注册式2-容器式
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class RegisterContainerWebCounterClient {
    public static void main(String[] args) throws InterruptedException {
        //启动时初始化容器
        RegisterContainerWebCounter counter = new RegisterContainerWebCounter();
        RegisterContainer.putInStance(RegisterContainerWebCounter.class.getName(), counter);

        int n = 10;
        CountDownLatch countDownLatch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                //从容器取出对象使用
                RegisterContainerWebCounter webCounter = (RegisterContainerWebCounter) RegisterContainer.getInstance(RegisterContainerWebCounter.class.getName());
                webCounter.count();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        RegisterContainerWebCounter webCounter = (RegisterContainerWebCounter) RegisterContainer.getInstance(RegisterContainerWebCounter.class.getName());
        log.info("总访问人数:" + webCounter.getCount());
    }
}

四、线程单例

不能保证程序全局唯一,但能保证线程唯一,天生是线程安全,多线程下为每个线程提供实例。

/**
 * 统计访问网站的用户数量-线程单例
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class ThreadLocalWebCounter {
    private final AtomicInteger count;
    private static final ThreadLocal<threadlocalwebcounter> THREAD_LOCAL = new ThreadLocal<threadlocalwebcounter>() {
        @Override
        protected ThreadLocalWebCounter initialValue() {
            return new ThreadLocalWebCounter();
        }
    };

    private ThreadLocalWebCounter() {
        count = new AtomicInteger();
    }

    public static ThreadLocalWebCounter getInstance() {
        return THREAD_LOCAL.get();
    }

    public void count() {
        log.info("{}是第{}个访问网站,统计器:{}",
                Thread.currentThread().getName(),
                count.incrementAndGet(),
                System.identityHashCode(count));
    }

    public int getCount() {
        return count.get();
    }
}

客户端测试:

/**
 * 统计访问网站的用户数量-线程单例
 *
 * @author ccheng
 * @date 2021/12/5
 */
@Slf4j
public class ThreadLocalWebCounterClient {
    public static void main(String[] args) throws InterruptedException {
        int n = 10;
        CountDownLatch countDownLatch = new CountDownLatch(n);
        for (int i = 0; i < n; i++) {
            new Thread(() -> {
                ThreadLocalWebCounter.getInstance().count();
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();

        ThreadLocalWebCounter.getInstance().count();
        ThreadLocalWebCounter.getInstance().count();
        ThreadLocalWebCounter.getInstance().count();
        log.info("总访问人数:" + ThreadLocalWebCounter.getInstance().getCount());
    }
}

//执行结果
Thread-1是第1个访问网站,统计器:1611861660
Thread-6是第1个访问网站,统计器:1848973929
Thread-4是第1个访问网站,统计器:1356743723
Thread-9是第1个访问网站,统计器:465932577
Thread-5是第1个访问网站,统计器:1610186110
Thread-2是第1个访问网站,统计器:1762222186
Thread-10是第1个访问网站,统计器:580172354
Thread-3是第1个访问网站,统计器:378785343
Thread-7是第1个访问网站,统计器:942954247
Thread-8是第1个访问网站,统计器:692384328
main是第1个访问网站,统计器:439928219
main是第2个访问网站,统计器:439928219
main是第3个访问网站,统计器:439928219
总访问人数:3

执行结果可以看到,对每个线程都是一个独立的统计器,main线程也是独立的。

五、JDK中的单例

java.lang.Runtime
image.png
</string,>

posted on 2021-12-06 00:17  _ccheng  阅读(64)  评论(0)    收藏  举报