線程池源碼解析系列:爲何要使用位運算表示線程池狀態

歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我我的微信「java_front」一塊兒交流學習java

0 文章概述

咱們閱讀ThreadPoolExecutor源碼時在開篇就會發現不少位運算代碼:數據庫

public class ThreadPoolExecutor extends AbstractExecutorService {
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    private static int runStateOf(int c)     return c & ~CAPACITY; }
    private static int workerCountOf(int c)  return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) return rs | wc; }
}

不難發現線程狀態都用位運算表示,可是爲何要這樣作呢?爲何不定義爲直觀的數字呢?下面咱們進行分析。雖然代碼量很少,可是想要理解線程池就必需要理解爲何使用位運算。微信

ThreadPoolExecutor在設計時就是用一個int數值表示了兩個業務含義:線程池狀態和線程數量。其中高3位表示線程池狀態,低29位表示線程數量,這個設計思想體如今如下三句代碼:架構

// 代碼1
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 代碼2
private static final int COUNT_BITS = Integer.SIZE - 3;
// 代碼3
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

代碼1表示用一個int保存線程池信息,代碼2表示一共有(32-3)=29位能夠表示線程數量,代碼3表示理論上最大線程數量爲536870911,這個理論值足以支撐線程池使用。源碼分析

 

1 線程池狀態

咱們明白了線程池上述設計思想,下面就來分析線程池狀態值:學習

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

咱們知道COUNT_BITS=29則上述代碼等價於:this

private static final int RUNNING    = -1 << 29;
private static final int SHUTDOWN   =  0 << 29;
private static final int STOP       =  1 << 29;
private static final int TIDYING    =  2 << 29;
private static final int TERMINATED =  3 << 29;

如今咱們算一算這些狀態等於多少,在這裏咱們從後往前算,由於RUNNING狀態是負數左移運算,計算步驟稍微多一些。spa

(1) TERMINATED = 3 << 29

初始值:00000000 00000000 00000000 00000011
左移後:01100000 00000000 00000000 00000000

(2) TIDYING = 2 << 29

初始值:00000000 00000000 00000000 00000010
左移後:01000000 00000000 00000000 00000000

(3) STOP = 1 << 29

初始值:00000000 00000000 00000000 00000001
左移後:00100000 00000000 00000000 00000000

(4) SHUTDOWN = 0 << 29

初始值:00000000 00000000 00000000 00000000
左移後:00000000 00000000 00000000 00000000

(5) RUNNING = -1 << 29

原碼:10000000 00000000 00000000 00000001
反碼:11111111 11111111 11111111 11111110(原碼符號位不變、數值位取反)
補碼:11111111 11111111 11111111 11111111(反碼+1)
左移:11100000 00000000 00000000 00000000

 

2 位運算應用

runStateOf方法用來獲取線程池狀態信息,workerCountOf方法用來獲取線程池線程數,ctlOf方法用來設置當前線程池狀態和線程數量信息,咱們分別進行計算。線程

private static int runStateOf(int c)     return c & ~CAPACITY; }
private static int workerCountOf(int c)  return c & CAPACITY; }
private static int ctlOf(int rs, int wc) return rs | wc; }

(1) CAPACITY = (1 << 29) - 1

先左移:00100000 00000000 00000000 00000000
再減一:00011111 11111111 11111111 11111111

(2) ~CAPACITY

原始值:00011111 11111111 11111111 11111111
取反後:11100000 00000000 00000000 00000000

(3) runStateOf

如今一個線程池狀態是RUNNING而且線程數量等於3用二進制表示以下:設計

11100000 00000000 00000000 00000011

執行runStateOf方法就能夠獲得線程池狀態:

11100000 00000000 00000000 00000011
&
11100000 00000000 00000000 00000000
=
11100000 00000000 00000000 00000000

(4) workerCountOf

如今一個線程池狀態是RUNNING而且線程數量等於4用二進制表示以下:

11100000 00000000 00000000 00000100

執行workerCountOf方法就能夠獲得線程數量:

11100000 00000000 00000000 00000100
&
00011111 11111111 11111111 11111111
=
00000000 00000000 00000000 00000100

(5) ctlOf

如今咱們要設置一個狀態是RUNNING且線程數量等於4的線程池ctl值:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 4));

11100000 00000000 00000000 00000000
|
00000000 00000000 00000000 00000100
=
11100000 00000000 00000000 00000100

這個方法真正體現了高3位表示線程池狀態,低29位表示線程數量這個設計思想優勢,本來須要兩步設置動做如今只須要一步,從而實現了操做原子性,這樣就能夠知足線程池的不少CAS操做,例如線程池在調用addWorker新增工做線程數時會調用compareAndIncrementWorkerCount方法增長線程數量。

可是假設同一時刻shutdownNow方法致使線程池狀態發生改變,那麼新增工做線程數方法就不會調用成功,須要繼續執行自旋進行嘗試,這體現了線程狀態和線程數量維護的原子性。
 

3 位圖法應用

3.1 需求背景

咱們看看位運算怎樣應用在實際開發場景。假設在系統中用戶一共有三種角色:普通用戶、管理員、超級管理員,如今須要設計一張用戶角色表記錄這類信息。咱們不難設計出以下方案:

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

咱們使用1表示是,0表示否,那麼觀察上表不可貴出,用戶一有用超級管理員角色,用戶二具備管理員角色,用戶三具備普通用戶角色,用戶四同時具備三種角色。若是此時新增長一種角色呢?那麼新增一個字段便可。

3.2 發現問題

按照上述一個字段表示一種角色進行表設計功能上是沒有問題的,優勢是容易理解結構清晰,可是咱們想想有沒有什麼問題?筆者遇到過以下問題:在複雜業務環境一份數據可能會使用在不一樣的場景,例如上述數據存儲在MySQL數據庫,這一份數據還會被用在以下場景:

檢索數據須要同步一份到ES

業務方使用此表經過Flink計算業務指標

業務方訂閱此表Binlog消息進行業務處理

若是表結構發生變化,數據源之間就要從新進行對接,業務方也要進行代碼修改,這樣開發成本比較很是高。有沒有辦法避免此類問題?

3.3 解決方案

咱們可使用位圖法,這樣同一個字段能夠表示多個業務含義。首先設計以下數據表,userFlag字段暫時不填。

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

咱們設計位圖每個bit表示一種角色:

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

咱們使用位圖法表示以下數據表:

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

用戶一位圖以下十進制數值等於4:

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

用戶二位圖以下十進制數值等於2:

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

用戶三位圖以下十進制數值等於1:

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

用戶四位圖以下十進制數值等於7:

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

如今咱們能夠填寫數據表第三列:

線程池源碼解析系列:爲何要使用位運算表示線程池狀態

 

3.4 代碼實例

(1) 枚舉定義

定義枚舉時不要直接定義爲一、二、4這類數字,而是採用位移方式進行定義,這樣使用者能夠明白設計者的意圖。

/**  * 用戶角色枚舉  *  * @author 微信公衆號「JAVA前線」  *  */
public enum UserRoleEnum {

    // 1 -> 00000001
    NORMAL(1"普通用戶"),

    // 2 -> 00000010
    MANAGER(1 << 1"管理員"),

    // 4 -> 00000100
    SUPER(1 << 2"超級管理員")

    ;

    private int code;
    private String description;

    private UserRoleEnum(Integer code, String description) {
        this.code = code;
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public int getCode() {
        return this.code;
    }
}

假設用戶已經具備普通用戶角色,咱們須要爲其增長管理員角色,這就是新增角色,與之對應還有刪除角色和查詢角色,這些操做須要用到爲位運算,詳見代碼註釋。

/**  * 用戶角色枚舉  *  * @author 微信公衆號「JAVA前線」  *  */
public enum UserRoleEnum {

    // 1 -> 00000001
    NORMAL(1"普通用戶"),

    // 2 -> 00000010
    MANAGER(1 << 1"管理員"),

    // 4 -> 00000100
    SUPER(1 << 2"超級管理員")

    ;

    // 新增角色 -> 位或操做
    // oldRole -> 00000001 -> 普通用戶
    // addRole -> 00000010 -> 新增管理員
    // newRole -> 00000011 -> 普通用戶和管理員
    public static Integer addRole(Integer oldRole, Integer addRole) {
        return oldRole | addRole;
    }

    // 刪除角色 -> 位異或操做
    // oldRole -> 00000011 -> 普通用戶和管理員
    // delRole -> 00000010 -> 刪除管理員
    // newRole -> 00000001 -> 普通用戶
    public static Integer removeRole(Integer oldRole, Integer delRole) {
        return oldRole ^ delRole;
    }

    // 是否有某種角色 -> 位與操做
    // allRole -> 00000011 -> 普通用戶和管理員
    // qryRole -> 00000001 -> 是否有管理員角色
    // resRole -> 00000001 -> 有普通用戶角色
    public static boolean hasRole(Integer allRole, Integer qryRole) {
        return qryRole == (role & qryRole);
    }

    private int code;
    private String description;

    private UserRoleEnum(Integer code, String description) {
        this.code = code;
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public int getCode() {
        return this.code;
    }

    public static void main(String[] args) {
        System.out.println(addRole(12));
        System.out.println(removeRole(31));
        System.out.println(hasRole(31));
    }
}

(2) 數據查詢

假設在運營後臺查詢界面中,須要查詢具備普通用戶角色的用戶數據,可使用以下SQL語句:

select * from user_role where (user_flag & 1) = user_flag;
select * from user_role where (user_flag & b'0001') = user_flag;

咱們也可使用以下MyBatis語句:

<select id="selectByUserRole" resultMap="BaseResultMap" parameterType="java.util.Map">
  select * from user_role 
  where user_flag & #{userFlag} = #{userFlag}
</select>

<select id="selectByUserIdAndRole" resultMap="BaseResultMap" parameterType="java.util.Map">
  select * from user_role 
  where id = #{userId} and user_flag & #{userFlag} = #{userFlag}
</select>

 

4 文章總結

本文首先分析了位運算在Java線程池源碼的應用,而後咱們又介紹了位圖法,這樣一個字段就能夠表示多個含義,從而減小了字段冗餘,節省了對接和開發的成本。固然位圖法也有缺點,例如數據庫字段含義不直觀須要進行轉義,增長了代碼理解成本,你們能夠根據需求場景選擇使用,但願本文對你們有所幫助。

歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我我的微信「java_front」一塊兒交流學習

相關文章
相關標籤/搜索