線程池源碼解析系列:為什么要使用位運算表示線程池狀態(tài)
JAVA前線
歡迎大家關注公眾號「JAVA前線」查看更多精彩分享,主要包括源碼分析、實際應用、架構思維、職場分享、產品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學習
0 文章概述
我們閱讀ThreadPoolExecutor源碼時在開篇就會發(fā)現很多位運算代碼:
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; }
}
不難發(fā)現線程狀態(tài)都用位運算表示,但是為什么要這樣做呢?為什么不定義為直觀的數字呢?下面我們進行分析。雖然代碼量不多,但是想要理解線程池就必須要理解為什么使用位運算。
ThreadPoolExecutor在設計時就是用一個int數值表示了兩個業(yè)務含義:線程池狀態(tài)和線程數量。其中高3位表示線程池狀態(tài),低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 線程池狀態(tài)
我們明白了線程池上述設計思想,下面就來分析線程池狀態(tài)值:
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則上述代碼等價于:
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;
現在我們算一算這些狀態(tài)等于多少,在這里我們從后往前算,因為RUNNING狀態(tài)是負數左移運算,計算步驟稍微多一些。
(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方法用來獲取線程池狀態(tài)信息,workerCountOf方法用來獲取線程池線程數,ctlOf方法用來設置當前線程池狀態(tài)和線程數量信息,我們分別進行計算。
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
現在一個線程池狀態(tài)是RUNNING并且線程數量等于3用二進制表示如下:
11100000 00000000 00000000 00000011
執(zhí)行runStateOf方法就可以得到線程池狀態(tài):
11100000 00000000 00000000 00000011
&
11100000 00000000 00000000 00000000
=
11100000 00000000 00000000 00000000
(4) workerCountOf
現在一個線程池狀態(tài)是RUNNING并且線程數量等于4用二進制表示如下:
11100000 00000000 00000000 00000100
執(zhí)行workerCountOf方法就可以得到線程數量:
11100000 00000000 00000000 00000100
&
00011111 11111111 11111111 11111111
=
00000000 00000000 00000000 00000100
(5) ctlOf
現在我們要設置一個狀態(tài)是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位表示線程池狀態(tài),低29位表示線程數量這個設計思想優(yōu)點,原本需要兩步設置動作現在只需要一步,從而實現了操作原子性,這樣就可以滿足線程池的很多CAS操作,例如線程池在調用addWorker新增工作線程數時會調用compareAndIncrementWorkerCount方法增加線程數量。
但是假設同一時刻shutdownNow方法導致線程池狀態(tài)發(fā)生改變,那么新增工作線程數方法就不會調用成功,需要繼續(xù)執(zhí)行自旋進行嘗試,這體現了線程狀態(tài)和線程數量維護的原子性。
3 位圖法應用
3.1 需求背景
我們看看位運算怎樣應用在實際開發(fā)場景。假設在系統中用戶一共有三種角色:普通用戶、管理員、超級管理員,現在需要設計一張用戶角色表記錄這類信息。我們不難設計出如下方案:
| id | name | super | admin | normal |
|---|---|---|---|---|
| 101 | 用戶一 | 1 | 0 | 0 |
| 102 | 用戶二 | 0 | 1 | 0 |
| 103 | 用戶三 | 0 | 0 | 1 |
| 104 | 用戶四 | 1 | 1 | 1 |
我們使用1表示是,0表示否,那么觀察上表不難得出,用戶一有用超級管理員角色,用戶二具有管理員角色,用戶三具有普通用戶角色,用戶四同時具有三種角色。如果此時新增加一種角色呢?那么新增一個字段即可。
3.2 發(fā)現問題
按照上述一個字段表示一種角色進行表設計功能上是沒有問題的,優(yōu)點是容易理解結構清晰,但是我們想一想有沒有什么問題?筆者遇到過如下問題:在復雜業(yè)務環(huán)境一份數據可能會使用在不同的場景,例如上述數據存儲在MySQL數據庫,這一份數據還會被用在如下場景:
檢索數據需要同步一份到ES
業(yè)務方使用此表通過Flink計算業(yè)務指標
業(yè)務方訂閱此表Binlog消息進行業(yè)務處理
如果表結構發(fā)生變化,數據源之間就要重新進行對接,業(yè)務方也要進行代碼修改,這樣開發(fā)成本比較非常高。有沒有辦法避免此類問題?
3.3 解決方案
我們可以使用位圖法,這樣同一個字段可以表示多個業(yè)務含義。首先設計如下數據表,userFlag字段暫時不填。
| id | name | user_flag |
|---|---|---|
| 101 | 用戶一 | 暫時不填 |
| 102 | 用戶二 | 暫時不填 |
| 103 | 用戶三 | 暫時不填 |
| 104 | 用戶四 | 暫時不填 |
我們設計位圖每一個bit表示一種角色:

我們使用位圖法表示如下數據表:
| id | name | super | admin | normal |
|---|---|---|---|---|
| 101 | 用戶一 | 1 | 0 | 0 |
| 102 | 用戶二 | 0 | 1 | 0 |
| 103 | 用戶三 | 0 | 0 | 1 |
| 104 | 用戶四 | 1 | 1 | 1 |
用戶一位圖如下十進制數值等于4:

用戶二位圖如下十進制數值等于2:

用戶三位圖如下十進制數值等于1:

用戶四位圖如下十進制數值等于7:

現在我們可以填寫數據表第三列:
| id | name | user_flag |
|---|---|---|
| 101 | 用戶一 | 4 |
| 102 | 用戶二 | 2 |
| 103 | 用戶三 | 1 |
| 104 | 用戶四 | 7 |
3.4 代碼實例
(1) 枚舉定義
定義枚舉時不要直接定義為1、2、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(1, 2));
System.out.println(removeRole(3, 1));
System.out.println(hasRole(3, 1));
}
}
(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線程池源碼的應用,然后我們又介紹了位圖法,這樣一個字段就可以表示多個含義,從而減少了字段冗余,節(jié)省了對接和開發(fā)的成本。當然位圖法也有缺點,例如數據庫字段含義不直觀需要進行轉義,增加了代碼理解成本,大家可以根據需求場景選擇使用,希望本文對大家有所幫助。
JAVA前線
歡迎大家關注公眾號「JAVA前線」查看更多精彩分享,主要包括源碼分析、實際應用、架構思維、職場分享、產品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學習
