天天看點

Java并發程式設計LockSupport使用執行個體

最近負責的項目需要實作一個Web頁面監控功能,待監控的資料需要從資料庫中統計出來。本身來講這是一個很簡單的功能點,但是考慮到監控端頁面會被多人同時通路的業務場景,監控資料又要求每間隔一秒重新整理一次,如果每個監控界面都實時去通路資料庫,那麼資料庫的資源開銷就太大了,若在白天的業務繁忙期遇到監控端使用者數較多時有可能會影響正常的交易辦理。為了避免資料庫資源過度使用的問題我的設計是在web容器背景建構一塊監控資料緩存,無論前台有多少個人通路監控頁面,都隻是從web容器緩存中擷取監控資料,web容器背景有一個值守線程X每間隔一秒通路資料庫輪詢監控資料至記憶體中,示意圖如下:

Java并發程式設計LockSupport使用執行個體

螢幕快照 2018-07-27 下午4.50.43.png

僅僅實作以上業務流程其實也非常簡單,還用不上LockSupport支援,但是本着對系統資源的最低能耗及高性能需求,我有了更進一步的優雅實作願景,當沒有User監控請求通路容器時背景值守線程可以不幹活讓其處于阻塞狀态,當容器收到User端監控請求時背景值守線程X立即從阻塞狀态轉變成Running狀态,為此我們需要學習運用concurrent包中的LockSupport類來控制多線程間的運作狀态切換以實作需求

LockSupport學習

LockSupport是JDK中比較底層的類,用來建立鎖和其他同步工具類的基本線程阻塞原語。LockSupport很類似于二進制信号量(隻有1個許可證可供使用),如果這個許可還沒有被占用,目前線程擷取許可并繼續執行;如果許可已經被占用,目前線程阻塞,等待擷取許可。通過網上一些對LockSupport的源碼分析可知,其實作是通過調用本地代碼(C++)來做的,具有很強的OS平台相關性,是以性能應該是非常高的。對于JVM應用來說主要是通過調用LockSupport.park()和LockSupport.unpark()實作線程的阻塞和喚醒操作的,當然實作線程間的阻塞和喚醒我們還可以用到對象鎖,通過Synchronizer關鍵字來實作對象同步鎖,使用對象的wait()和notify()方法來實作,但是此方式的實作在性能上會大打折扣而且有些并發控制不當非常容易引發線程間死鎖,可以說非常不優雅。

LockSupport類核心方法

基于Unsafe類中的park和unpark方法

public static void park() {
        UNSAFE.park(false, 0L);
    }
           
public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
           
  • park()方法,調用native方法阻塞目前線程
  • unpark()方法,喚醒處于阻塞狀态的線程Thrread

LockSupport類測試Demo

如下編寫一個ThreadPark類來驗證park與unpark方法的成對使用

public class ThreadParkTest {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.setName("mt");
        mt.start();
        try {
            Thread.currentThread().sleep(10);
            mt.park();
            Thread.currentThread().sleep(10000);
            mt.unPark();
            Thread.currentThread().sleep(10000);
            mt.park();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    static class MyThread extends Thread {

        private boolean isPark = false;
        public void run() {
            System.out.println(" Enter Thread running.....");
            while (true) {
                if (isPark) {
                    System.out.println(Thread.currentThread().getName()+"Thread is Park.....");
                    LockSupport.park();
                }
                //do something
                System.out.println(Thread.currentThread().getName()+">> is running");
                try {
                    Thread.currentThread().sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        public void park() {
            isPark = true;
        }
        public void unPark() {
            isPark = false;
            LockSupport.unpark(this);
            System.out.println("Thread is unpark.....");
        }
    }
}
           

程式運作輸出:

Enter Thread running.....

mt>> is running

mtThread is Park.....

Thread is unpark.....

mtThread is Park

park翻譯過來即是停車的意思,我們可以這樣了解,每個被應用程式啟動的線程就是一輛在計算機總線賽道上奔馳着的跑車,當你想讓某台車停下來休息會時那麼就給它一個park信号,它就會立即停到賽道旁邊的停車位中,當你想讓它從停車位中駛出并繼續在賽道上奔跑時再給它一個unpark信号即可

LockSupport的業務實際應用

我們對技術基礎知識的掌握是為了更好,更優雅,更從容的實作業務需求,以最小的程式代價來實作業務最大收益化是計算機軟體工程的永恒追求主題之一。 差不多給自己埋好坑了(圍笑),不扯淡了,還是show me the code吧。

回到第一章的監控業務需求,首先我們需要編寫背景值守線程X類,Daemon線程類Run()方法中除實作從資料庫中加載監控資料到記憶體之外還必須實作具備滿足一定條件時調用park()方法線程自動停車,同時對外要提供unpack()方法用于外部喚醒線程。

背景值守線程MonitorWorkThread類代碼編寫:

class MonitorWorkThread extends Thread
    {
       //目前線程停車标志
        private volatile boolean isPark = false;
        
        //工作線程預設一秒鐘加載一次,count即為工作監控線程每一次unpack之後會繼續工作的時間,此值可根據實際需求配置化
    private int maxWorkCount = 300;
        
        @Override
        public void run() 
        {
            int indexCount=0;
            logger.info("成功啟動稽核任務監控工作線程,目前工作線程每次unpack連續工作的時間設定為"+maxWorkCount+"秒");
            while(true)
            {
                
                if(indexCount >= maxWorkCount)
                {
                    logger.info("目前監控工作線程已到達連續工作時間設定上限,現在進入pack休眠狀态");
                    isPark =true;
                    indexCount=0;
                    LockSupport.park();
                }
                //從資料庫中加載資料至記憶體
                try 
                {
                    loadDataFromDB();
                } catch (Exception e1) 
                {
                    logger.warn("從資料庫中加載監控資料至記憶體發生異常", e1);
                }
                try 
                {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) 
                {
                    logger.warn("工作線程被異常中斷喚醒", e);
                }
                indexCount++;
            }
        }
        /**
         * 假如目前線程正在運作狀态,donothing
         */
        public void unPack()
        {
            if(isPark)
            {
                //喚醒目前監控工作線程,此處有并發喚醒動作需加鎖
                synchronized (this)
                {
                     isPark = false;
                     LockSupport.unpark(this);
                     logger.info("目前監控工作線程已被喚醒");
                }
            }
        }
        
    }
           

接下來我們考慮編寫監控業務實作類,大體思路是我們先需要定義記憶體緩存map用于裝載資料庫監控資料,業務實作類在容器執行個體化時自動啟動上面的值守線程MonitorWorkThread,對外提供一個擷取記憶體資料的公共方法,公共方法體中需要調用值守線程的unPack()方法以實作當容器收到用戶端監控通路請求時若背景異步值守線程處于停車(阻塞)狀态時,會被喚醒繼續奔跑上路。

任務監控業務實作類TaskMonitorServiceImpl編寫:

/**
 * 
 * @author lyp
 *  稽核任務監控實作類
 */
@Service("TaskMonitorServiceImpl")
public class TaskMonitorServiceImpl 
{
    private static Logger logger= Logger.getLogger(TaskMonitorServiceImpl.class);
    
    //總任務狀态緩存表
    private List<TaskStatusBean> totalStatus = new ArrayList<TaskStatusBean>();
    //稽核櫃員任務處理緩存表
    private Map<String,UserTaskCountDto> userTaskMap = new ConcurrentHashMap<String, UserTaskCountDto>();
    //監控工作線程引用
    private static MonitorWorkThread workThread=null;
    
    @PostConstruct
    private void initalizal()
    {
        //執行個體化之後執行的初始化動作,用于啟動值守監控線程來重新整理加載資料
        workThread = new MonitorWorkThread();
        workThread.setDaemon(true);
        workThread.setName("AuthTaskMonitor");
        workThread.start();
    }

    /**
     * 此為對外提供方法用于外部根據監控使用者号擷取記憶體中緩存的監控資料
     * @param userno           監控使用者号
     * @return map key1:totalStatus key2:userno
     * @throws Exception
     */
    public Map<String,Object> monitorDataByUser(String userno) throws Exception
    {
        if(null == userno || "".equals(userno))
        {
            return null;
        }
        Map<String,Object> retMap = new HashMap<String, Object>();
        if(null !=workThread)
        {
           //每次請求都去看看異步值守線程是否需求喚醒
            workThread.unPack();
        }
        retMap.put("totalStatus", totalStatus);
        if(userTaskMap.containsKey(userno))
        {
            retMap.put(userno, userTaskMap.get(userno));
        }else
        {
            UserTaskCountDto dto =  new UserTaskCountDto(userno);
            retMap.put(userno, dto);
        }
        return retMap;
    }
    
    /**
     * 從資料庫中加載記憶體資料至記憶體
     */
    private void loadDataFromDB () throws Exception
    {
        
        logger.info("開始從資料庫中加載任務監控資料...");
        //do something about business....
        
        logger.info("從資料庫中加載任務監控資料完畢...");
    }
    
    
    /**
     * 清理監控緩存資料map 
     */
    public void clearMonitorCache()
    {
        this.totalStatus.clear();
        this.userTaskMap.clear();
    }
}

           

寫在最後

技術知識的學習本身就是枯燥無味的,靠解決問題的動力來驅動技術知識的掌握未嘗不是一個值得嘗試的高效學習方法。以上是我第一次在簡書書寫文章,選擇加入簡書的原因其實很簡單,一是看美劇的時候被大量廣告植入,二是簡書的編輯器完美支援MarkDown語言寫作。其實這也是我第一次使用MarkDown标記語言寫作排版,MarkDown的寫作方式對于程式員來說真的是太爽了,啊啊啊。

MarkDown語言

Markdown is intended to be as easy-to-read and easy-to-write as is feasible.

Readability, however, is emphasized above all else. A Markdown-formatted document should be publishable as-is, as plain text, without looking like it's been marked up with tags or formatting instructions.

Markdown's syntax is intended for one purpose: to be used as a format for writing for the web

寫作不易,看完本文如果你覺得對你的工作生活有幫助請給個贊賞,不在乎多少,這會給予我寫作無限的動力。

最後如果你需要轉載此文,請标明原創出處,謝謝。