设计模式2-单例模式
劝退警告,本文非常长长长......
单例模式定义及应用场景
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
单例模式是创建型模式。
单例模式优点:
- 一个类在内存中只有一个对象,节省内存空间
- 避免频繁的创建销毁对象,可以提高性能
- 避免对共享资源的多重占用
- 可以全局访问
单例模式适用场景:
- 需要频繁实例化然后销毁的对象
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象
- 有状态的工具类对象
- 频繁访问数据库或文件的对象
- J2EE标准中的ServletContext、ServletContextConfig等、Spring框架应用中的ApplicationContext、数据库的连接池等也都是单例模式。
创建单例主要有以下几种方式:
- 饿汉式单例
- 简单的饿汉式单例
- 静态代码块的饿汉式单例
- 懒汉式单例
- 简单实现的懒汉式单例
- 同步方法式单例
- 双重检查锁式单例
- 静态内部类式单例
- 注册式单例
- 枚举式单例
- 容器式单例
- 线程单例
单例模式的选型考虑:
- 是否延迟加载
- 是否线程安全
- 并发访问性能
- 是否可以防止反射和反序列化破坏
一、饿汉式单例模式
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。
它绝对线程安全,因为当第一个线程访问它的时候,就通过类加载机制完成了实例化,甚至后续的访问线程还没创建完成
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中至少做了三件事:
- 给counter在堆上分配内存空间。(分配内存)
- 调用LazyDoubleCheckWebCounter的构造函数等来初始化counter。(初始化)
- 将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

java.io.ObjectInputStream#readObject0又调用了java.io.ObjectInputStream#readOrdinaryObject

java.io.ObjectInputStream#readOrdinaryObject方法先实例化了一个对象
然后判断是否存在readResolve方法,存在的话则通过反射调用readResolve方法读取
最后将readResolve结果覆盖掉前面实例化的对象


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

通过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进行了赋值,是饿汉式单例模式的实现,所以线程安全。

再来看为啥反射不能破坏
我们可以看到是继承了Enum类,并调用了父类的构造方法,并没有无参构造方法,所以前面的反射调用抛出了NoSuchMethodException。


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

最后来看一下为啥序列化也不能破坏
回到ObjectInputStream的源码
java.io.ObjectInputStream#readObject调用了java.io.ObjectInputStream#readObject0
java.io.ObjectInputStream#readObject0判断如果是枚举类型,则调用java.io.ObjectInputStream#readEnum

java.io.ObjectInputStream#readEnum实际上是通过java.lang.Enum#valueOf方法

java.lang.Enum#valueOf方法实际上是从枚举的容器中直接取出,并没有重新创建对象,所以反序列化也是同一个对象

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

</string,>
浙公网安备 33010602011771号