<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          線程池源碼解析系列:為什么要使用位運算表示線程池狀態(tài)

          共 13092字,需瀏覽 27分鐘

           ·

          2021-03-06 05:24


          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ā)場景。假設在系統中用戶一共有三種角色:普通用戶、管理員、超級管理員,現在需要設計一張用戶角色表記錄這類信息。我們不難設計出如下方案:

          idnamesuperadminnormal
          101用戶一100
          102用戶二010
          103用戶三001
          104用戶四111

          我們使用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字段暫時不填。

          idnameuser_flag
          101用戶一暫時不填
          102用戶二暫時不填
          103用戶三暫時不填
          104用戶四暫時不填


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


          我們使用位圖法表示如下數據表:

          idnamesuperadminnormal
          101用戶一100
          102用戶二010
          103用戶三001
          104用戶四111


          用戶一位圖如下十進制數值等于4:


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


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


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


          現在我們可以填寫數據表第三列:

          idnameuser_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(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線程池源碼的應用,然后我們又介紹了位圖法,這樣一個字段就可以表示多個含義,從而減少了字段冗余,節(jié)省了對接和開發(fā)的成本。當然位圖法也有缺點,例如數據庫字段含義不直觀需要進行轉義,增加了代碼理解成本,大家可以根據需求場景選擇使用,希望本文對大家有所幫助。



          JAVA前線 


          歡迎大家關注公眾號「JAVA前線」查看更多精彩分享,主要包括源碼分析、實際應用、架構思維、職場分享、產品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學習


          瀏覽 34
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲日本在线播放 | 成人自拍在线视频 | 日本AA片免费 | 国产精品国产精品国产专区不片 | 日本乱码视频在线播放 |