设计模式:枚举实现的单例模式真的就万无一失了吗?

设计模式中,最广为人知的就是单例模式了吧,相信知道单例模式的人,也知道实现单例模式有几种方法,目前用的最多的就是 双重锁,静态内部类,枚举。网上大都推荐枚举方式实现单例。因为使用枚举实现的单例模式,更简洁更安全。但是使用枚举实现的单例模式真的就万无一失了吗?

下面我使用枚举实现的单例模式:

@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

总结:

枚举实现的单例模式并不是绝对安全的,多个类加载器会破坏枚举实现的单例
————————————————
版权声明:本文为CSDN博主「kanyun123」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/kanyun123/article/details/104047715

也可以看下这篇文章:用Java枚举实现单例的缺点是什么?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值