java單例實例對象在springboot中實例化了2次,原因竟然是熱部署的鍋(記一次神奇的bug)

神奇的bug


前言:我寫的明明是單例,可是爲什麼初始化了二次?
今天寫的這個bug和單例設計模式有關。 所謂單例設計模式,這個不多說,詳情可以自行百度。

spring中的單例

springboot的control就是單例的,關於這個可以看看這個文章
spring的controller默認是單例還是多例

場景

在我這次的項目中遇到這樣一個功能需求:我需要設計一個單例的類,這個類裏面有個阻塞隊列。
日誌的生產和消費就需要放置在隊列中。
日誌的生產我會監聽logback的事件,如果logback打印日誌,就把打印的內容發送到隊列裏面。
然後webscoket消費數據。
說白了,這裏面的隊列需要單例的,而且起到中轉數據的作用。

代碼

這是對隊列進行一個簡單的封裝,如下

package cn.xiejx.jfun.config.websocket;


import cn.xiejx.jfun.vo.LogMessage;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 創建一個阻塞隊列,作爲日誌系統輸出的日誌的一個臨時載體
 * @author jie
 * @date 2018-12-24
 */
public class LoggerQueue {

    /**
     * 隊列大小
     */
    public static final int QUEUE_MAX_SIZE = 10000;

    private static LoggerQueue alarmMessageQueue = new LoggerQueue();
    /**
     * 阻塞隊列
     */
    private BlockingQueue blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);

    private LoggerQueue() {
        System.out.println("loggerquque:"+this.getClass().getClassLoader().toString());
    }

    public static LoggerQueue getInstance() {
        return alarmMessageQueue;
    }

    /**
     * 消息入隊
     * @param log
     * @return
     */
    public boolean push(LogMessage log) {
        return this.blockingQueue.add(log);
    }

    /**
     * 消息出隊
     *
     * @return
     */
    public LogMessage poll() {
        LogMessage result = null;
        try {
            result = (LogMessage) this.blockingQueue.take();
            System.out.println("消費一條消息"+result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return result;
    }
}

產生的問題

其實上面這個代碼是沒問題的。
我的項目是springboot,上面的單例使用的是沒有用spring註解實現(springboot也有可以使用註解來使用單例)。
然後這邊消費數據的時候調用take方法。
take方法沒有數據是會阻塞的。
而數據已經有add進去了。
按理說可以消費到數據纔是,可是數據卻沒有被消費。

分析

使用了java的一些分析工具,把堆dump了出來。
看到了消費數據的線程是處於等待狀態。
最傻的方法用System.out.println也分析出線程一直在阻塞中(卡在take那裏)

進一步分析

我確信單例的實現是沒有問題的,按理說,數據add進去,應該是可以消費到的纔對。
斷點繼續分析,不看不知道。
發現了消費和生產數據的時候,getInstance返回的對象的hashCode碼不一樣。
也就是說單例被破壞了,生產數據放在了a隊列裏面,而消費數據卻要在b隊列裏面拿數據。
爲什麼呢?我的代碼明明是單例啊啊啊啊!!!

解決問題

分析,如果是相同的類加載器,不可能出現這種情況。
那麼我就在初始化的時候加上下面的代碼

        System.out.println("loggerquque:"+this.getClass().getClassLoader().toString());

發現這個類果然被初始化了2次。確實不是‘單例’了(不同的類加載器分別初始化)。
輸出如下:

....
loggerquque:sun.misc.Launcher$AppClassLoader@18b4aac2
....
loggerquque:org.springframework.boot.devtools.restart.classloader.RestartClassLoader@2a5810d2

原來是這個熱部署的鍋!!!
撥雲見日!!!
pom.xml文件果斷去除熱部署依賴先(暫時我只能這麼解決)

 <!--熱部署工具-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <version>2.0.2.RELEASE</version>
            <optional>true</optional>
            <scope>runtime</scope>
        </dependency>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <!--配置熱啓動-->
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>

重啓idea,問題解決。

參考鏈接

說明

上面的代碼非原創,來自大佬的項目。
碼雲:https://gitee.com/elunez/eladmin-qt
github:https://github.com/elunez/eladmin-qd
這裏感謝作者的開源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章