设计模式中,最广为人知的就是单例模式了吧,相信知道单例模式的人,也知道实现单例模式有几种方法,目前用的最多的就是 双重锁,静态内部类,枚举。网上大都推荐枚举方式实现单例。因为使用枚举实现的单例模式,更简洁更安全。但是使用枚举实现的单例模式真的就万无一失了吗?
下面我使用枚举实现的单例模式:
@Slf4j
public enum LoggerQueue implements Serializable {
INSTANCE;
/**
* 队列大小
* 这个队列大小不宜太大,否则当在前端连接SSE时,想要查看当前的日志,需要等待比较久的时间
*/
public static final int QUEUE_MAX_SIZE = 100;
/**
* 阻塞队列
*/
private BlockingQueue<LoggerMessage> blockingQueue;
/**
* 可以省略此方法,通过LoggerQueue.INSTANCE进行操
*
* @return
*/
public static LoggerQueue getInstance() {
return INSTANCE;
}
/**
* 自定义构造方法,用于构造blockingQueue象
* 枚举的构造方法都是私有的
*/
LoggerQueue() {
ClassLoader classLoader = this.getClass().getClassLoader();
blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);
}
/**
* 消息入队
* 如果入队失败,则移除队头元素,再入队
*
* @param loggerMessage
* @return
*/
public boolean push(LoggerMessage loggerMessage) {
// offer: 将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则返回 false,不会抛异常
if (!blockingQueue.offer(loggerMessage)) {
log.debug("=======LoggerQueue 队列已满,移除旧日志,插入新日志=========");
blockingQueue.poll();
return blockingQueue.offer(loggerMessage);
}
return true;
}
/**
* 消息出队
*
* @return
*/
public LoggerMessage poll() {
// take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
// message = this.blockingQueue.take();
// poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
LoggerMessage message = blockingQueue.poll();
return message;
}
}
测试:
public class App18 {
public static void main(String[] args) {
LoggerQueue loggerQueue1 = LoggerQueue.getInstance();
LoggerQueue loggerQueue2 = LoggerQueue.INSTANCE;
System.out.println(loggerQueue1==loggerQueue2); //打印了true
}
}
显然了main方法通过了测试
但是当我把这个这个枚举实现的单例放到实际项目中,却发现了这个枚举实现的单例模式被破坏掉了,换句话讲,就是在我的项目中LoggerQueue 出现了两个对象:
以下是我使用LoggerQueue 的地方
public class WebLogFilter extends Filter<ILoggingEvent> {
private LoggerQueue loggerQueue = LoggerQueue.getInstance();
@Override
public FilterReply decide(ILoggingEvent event) {
LoggerMessage loggerMessage = new LoggerMessage();
loggerMessage.setBody(event.getMessage());
// event.getTimeStamp()得到的时间是时间抽 将其转换为 yyyy-MM-dd HH:mm:ss 形式
loggerMessage.setTimestamp(convertTimeToString(event.getTimeStamp()));
loggerMessage.setLevel(event.getLevel().levelStr);
loggerMessage.setThreadName(event.getThreadName());
// event.getLoggerName()得到的是类的全限定名
loggerMessage.setClassName(event.getLoggerName());
//将LoggerMessage 推入队列中去
loggerQueue.push(loggerMessage);
return FilterReply.ACCEPT;
}
}
@Slf4j
@RestController
@RequestMapping("/logger")
public class LoggerController {
private static final LoggerQueue loggerQueue = LoggerQueue.getInstance();
/**
* 向web端推送日志
* Server Send Event
* SSE–server send event是一种服务端推送的技术 需要设置类型为event-stream
* 只需要客户端请求一次,如果请求成功(如返回200),那么服务端就会不断向客户端发送数据,其实是关闭了request而没有关闭response
* 这里返回了SseEmitter,这其实是使用了基于SpringMvc的实现
* 使用SseEmitter的send()方法发送数据,如果发送的数据为null,前台将无法触发接收数据事件
* 同时打开浏览器的控制台查看EventStream也没有动静
* https://mp.weixin.qq.com/s/gACgyBGHNoexKTxkCpD7Ug
*
* @return
*/
@GetMapping(value = "/constantly", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter push() {
//timeout设置为0表示不超时,如果不设置为0,那么如果SseEmitter在指定的时间(AsyncSupportConfigurer设置的timeout,默认为30秒)未完成会抛出异常
final SseEmitter emitter = new SseEmitter(0L);
try {
// 创建定时循环线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
// 延迟500毫秒启动,每次间隔500毫秒
scheduledExecutorService.scheduleAtFixedRate(() -> {
log.debug("控制器:"+loggerQueue);
LoggerMessage loggerMessage = loggerQueue.poll();
try {
emitter.send(loggerMessage);
} catch (IOException e) {
e.printStackTrace();
}
}, 500, 500, TimeUnit.MILLISECONDS);
// emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
}
这两个类分别获得了LoggerQueue的对象,但是当我请求Controller对应的方法时,却发现LoggerQueue中的BlockQueue是空的,但是WebLogFilter往LoggerQueue中分明插入了对象了,BlockQueue的size不是空的,于是断点进行调试:
可以看到两个类中的LoggerQueue对象竟然不是同一个,可是之前的main方法明明是已经通过测试的啊,而且网上的都是枚举实现的单例是安全的,我去搜索 “枚举实现的单例模式被破坏”,却没有任何结果,那么问题出在哪里了呢。
各种办法都用了之后,我决定在项目启动时打断点调试,这次我把断点打在了LoggerQueue的构造方法中,如果LoggerQueue的构造方法进入了两次,那么确实说明,会产生两个对象。
项目启动完成后,确实进入了两次枚举的构造方法。
但是等等,哪里有些不对劲。可以看到两次进入的构造方法的classloader不一样,AppClassLoader这个比较熟悉,RestartClassLoader是个什么鬼,于是搜索RestartClassLoader,发现这个是spring-boot-devtools下的,用于热部署的工具,
于是果断把这个依赖去掉,发现一切正常了,枚举的构造方法只进去一次了,单例模式终于正常了,再次去看这个RestartClassLoader,根据搜索结果,还可以看到他会导致类型转换异常,为什么会导致类型转换异常呢?应为 jvm 是根据 classloader + 类的全限定名 来判断类型的, 所以有时候会遇到 A 不能转换为 A的 ClassCastException 异常。
spring-boot-devtools的其他操作:https://blog.csdn.net/isea533/article/details/70495714
总结:
枚举实现的单例模式并不是绝对安全的,多个类加载器会破坏枚举实现的单例