2015/1/26

如何設定 MariaDB Cluster

MariaDB 建置備援環境有兩個選擇,Replication 或是 Cluster,以下將設定 cluster 的步驟記錄下來。

測試環境

DB1: 192.168.1.10
DB2: 192.168.1.20

目標是要建置這兩個 DB 的 cluster 環境

安裝 MariaDB galera

在兩個 DB 上分別安裝 MariaDB galera,將 MariaDB 的 respository 加到 yum 中。

vi /etc/yum.repos.d/MariaDB.repo
[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/5.5/centos6-amd64
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1

安裝 MariaDB cluster 版本

yum -y install MariaDB-Galera-server galera MariaDB-client MariaDB-devel

建立給 cluster 使用的 DB 帳號

mysql -u root -p

GRANT ALL PRIVILEGES ON *.* TO galerauser@localhost IDENTIFIED BY 'dbpassword';
GRANT ALL PRIVILEGES ON *.* TO 'galerauser'@'192.168.1.%' IDENTIFIED BY 'dbpassword';
flush privileges;
\q

設定 galera

將設定的 sample 複製到 MariaDB 的設定資料夾中

cp /usr/share/mysql/wsrep.cnf /etc/my.cnf.d/

修改 wsrep.cnf,調整下面列出來的設定項目,其他的設定值不需要修改。

vi /etc/my.cnf.d/wsrep.cnf

wsrep_provider=/usr/lib64/galera/libgalera_smm.so
wsrep_cluster_address="gcomm://"
wsrep_cluster_name="dbcluster"
wsrep_node_address='192.168.1.20'
wsrep_node_name='db1'
wsrep_sst_auth=galerauser:dbpassword
wsrep_sst_method=rsync

首先要注意的是 wsrep_cluster_address 這個設定值,因為我們是初始化 cluster,所以必須先填為 gcomm:// ,意思就是這個初始節點不需要依賴其他DB機器的資料,建立一個 cluster 初始節點。

利用以下指令初始化 cluster。

service mysql bootstrap

利用以下指令檢查 cluster 狀態,可注意 wsrep_ready 是否為 ON,另外就是查閱 wsrep_incoming_addresses, wsrep_cluster_conf_id, wsrep_cluster_size 這幾個欄位的資料,wsrep_cluster_size 目前應該為 1 。

mysql -e "SHOW STATUS LIKE 'wsrep_%'; " -p

檢查 mysql daemon 使用的網路 port,galera 是使用 TCP Port 4567。

netstat -anlp | grep -e 4567 -e 3306

設定第二台 DB 的 galera

依照上面的步驟編輯 wsrep.cnf,要注意的是 wsrep_cluster_address 必須為 gcomm://192.168.1.10,因為這個 DB 節點是 192.168.1.10 的備援節點。

wsrep_cluster_name 必須跟 DB1 的設定一樣,wsrep_node_name 是這個 DB 的識別,要填為 db2。

vi /etc/my.cnf.d/wsrep.cnf

wsrep_provider=/usr/lib64/galera/libgalera_smm.so
wsrep_cluster_address="gcomm://192.168.1.10"
wsrep_cluster_name="dbcluster"
wsrep_node_address='192.168.1.20'
wsrep_node_name='db2'
wsrep_sst_auth=galerauser:dbpassword
wsrep_sst_method=rsync

啟動 MariaDB

service mysql start

在兩台機器上檢查 cluster 的狀態,會發現 wsrep_cluster_size 變成 2。

mysql -e "SHOW STATUS LIKE 'wsrep_%'; " -p

回頭修改 DB1 的 wsrep_cluster_address

一開始初始化 cluster 時,我們是使用這樣的設定。

wsrep_cluster_address="gcomm://"

但為了讓 DB1 離線之後,也能從 DB2 將資料同步過來,我們必須調整設定為

wsrep_cluster_address="gcomm://192.168.1.20"

調整後將 MariaDB 重新啟動

service mysql restart

auto_increment 欄位在 cluster 環境要注意的狀況

通常我們會在 table 裡面使用 auto_increment 欄位,作為 DB table 的識別欄位。

create table mytable
(
   mytableseq   int(12) not null auto_increment comment '流水序號',
   primary key (mytableseq)
);

在 DB 單機的狀況下,我們預期看到 mytableseq 欄位的資料,會是 1, 2, 3, 4 ... 這樣按照順序,一個 record 增加 1 的資料。

而當 DB 設定為 cluster 時,如果這個 cluster 環境裡面只有一台機器,或是兩個 DB 節點中,有一個因故障而離線的時候,也就是 wsrep_cluster_size 為 1 的時候,mytableseq 還是一樣會以一個 record 增加 1 的狀況產生出來。

而當 DB 設定為 cluster,而且 wsrep_cluster_size 為 2 的時候,DB1 的 mytableseq 就會變成 1, 3, 5, 7 這樣,而 DB2 會是 2, 4, 6, 8。

以一個實例來看結果,如果先在 DB1 insert 兩筆資料,然後到 DB2 insert 兩筆資料,再回到 DB1 insert 兩筆資料,這時候,我們在 table 裡面看到的結果會是

1    (DB1)
3    (DB1)
4    (DB2)
6    (DB2)
7    (DB1)
9    (DB1)

參考資料
Auto increments in Galera

DB cluster 裡面有三個節點

在初始化 DB cluster 之後,分別在 DB1, DB2, DB3 給予不同的 wsrep_cluster_address 設定,每一台 DB 可根據另外兩個 DB 的資料進行資料同步作業,這時候當然 auto_increment 欄位就會變成,一次增加 3。

DB1(192.168.1.10)

wsrep_cluster_address="gcomm://192.168.1.20,192.168.1.30"


DB2(192.168.1.20)

wsrep_cluster_address="gcomm://192.168.1.10,192.168.1.30"


DB3(192.168.1.30)
wsrep_cluster_address="gcomm://192.168.1.10,192.168.1.20"

2015/1/19

如何在 Java 中計算出 2 + 1 = 4

My favourite Java puzzler 2 + 1 = 4 提供了一個很短的 Java 程式,可以在做整數計算 2+1 的時候,得到結果為 4 而不是 3。

文章是用問答題的方式問的,但我們資質駑鈍,偷懶直接看答案:

import java.lang.reflect.*;

public class Test {
    public static void main(String... args) throws Exception {
        Integer a = 2;
        Field valField = a.getClass().getDeclaredField("value");
        valField.setAccessible(true);
        valField.setInt(a, 3);

        Integer b = 2;
        Integer c = 1;

        System.out.println("b+c : " + (b + c)); // b+c : 4
    }
}

在程式的後半段,我們可看到 b + c 應該是 2 + 1 ,可是執行的結果卻得到 4 ,很明顯是前面那一半的部份,造成這樣的結果。

a.getClass().getDeclaredField

這一行是利用 Java 的 Reflection API 來取得 Integer 這個 class 的一個 private 欄位 value。

Field valField = a.getClass().getDeclaredField("value");

getField 跟 getDeclaredField 都是取得 class 裡面定義的 member 資料的 API,但 getField 只能取得 public fields,而 getDeclaredField 則不管 accessibility 是 public 或是 private,可以取得所有 fields。因此 a.getClass().getDeclaredField("value") 就是要取得 Integer 為 2 這個物件的 value 這個 field。

valField.setAccessible(true);
valField.setInt(a, 3);

接下來這兩行,則是將該 value 的欄位設定為可以修改的,並用 setInt 把 value 的值設定為 3。

新的 Integer 物件 2

我們在前面把物件 a 的 value 改成了 3,但是後半段卻是產生了一個新的 Interger 物件 2,就 Java 來說,只要是new 新的物件,那應該會不一樣才對啊!

但執行結果告訴我們,雖然有了一個新物件 b (Integer b = 2;),但很明顯地,在計算 b+c 的結果時,b 的數值被剛剛修改物件 a 的數值為 3 的時候,影響到了結果,也就是 b 物件就跟 a 物件是一樣的。

原因就在 Integer 的 source code 裡面

JDK 裡面的 Integer.java

JDK 裡面有 Integer 的 source code,我們節錄重要的部份:

public final class Integer extends Number implements Comparable<Integer> {

....

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    private final int value;

    public Integer(int value) {
        this.value = value;
    }
...
}

Interger Class 裡面放了一個靜態的 Inner Class: IntegerCache,這裡面 cache 了 -128 到 127 這麼多整數的物件。

Integer b=2;

這樣的語法是 auto boxing 的功能,就是 compiler 可以將一些 syntax sugar 自動 unboxing。像上面這個整數物件賦值的語法,compiler 會自動翻譯成以下這樣的語法。

Integer b = Integer.valueOf(2);

另外再看看 valueOf 這個 method,在 IntegerCache 負責的數值區間中,其實是直接使用 IntegerCache 而不是產生一個新的 Integer 物件。

換句話說,當我們使用 -128 到 127 的 Interger 物件時,其實都是參考到同一個物件。

為什麼要有 IntegerCache

cache 的用途基本上唯一的目的就是為了效能考量,而記憶體通常都是 cache 的第一選擇,在實做 Integer 的工程師認定一般使用者最常用到 -128 到 127 這些整數的物件,所以就預先產生出來,放置到記憶體中。

另外因為 IntergerCache 是 JVM 中靜態的物件,這樣子處理,也會因為 -128 ~ 127 使用率的增加,而達到記憶體空間節省的效果,表面上多花了一些記憶體,存放著 256 個整數物件,實際上當程式運作時間越久,產生出來的這些 IntergerCache 就會發生作用。

Integers caching in Java

2015/1/18

無盡之劍 - Brandon Sanderson

Brandon Sanderson 的每一本小說都有追到,無盡之劍當然沒有例外,不管內容是什麼都得買回來看,讀完後,實際上直接的感受是,這個故事的寫法跟以前好像不大一樣,沒有花很多篇幅去說明幾個角色的歷史,沒有很仔細地一直重複地去說明角色的特色,中間還有一個很奇怪的段落,兩個世仇的主角,關在牢裡不斷殺死對方之後,卻突然理解對方,轉而合作對抗神工。

Infinity Blade

無盡之劍 Infinity Blade是ios手機遊戲,於2010年12月9日在App Store上市,為了配合手機的遊戲體驗,這個遊戲首創在戰鬥中,玩家可以用手指在螢幕上點劃來使主角做出攻擊和閃避的動作,另外還強調是最精美的 3D 遊戲,所以遊戲本身的體積也不小,超過了 1GB。

如想果要能了解無盡之劍的全部劇情似乎也不是一見很容易的事情,可以參考 【劇情解析】無盡之劍背後的故事 這篇文章的說明,看一下 幾代的 Inifinity Blade 整理出來的劇情。

不斷死而復生的靈魂體驗

有玩過線上遊戲的人都知道,在遊戲中如果不小心掛掉了,除非馬上被當場復活,不然玩家只能在遙遠的城鎮裡面重生,損失掉一些經驗值或金錢甚至是裝備,然後再慢慢地繼續進行遊戲。

主角賽瑞斯就是在以打敗神王雷帝亞為目的,不斷地為了完成這個任務而努力,在每一次失敗之後,又因為自己是不朽戰士的身份,重生後,再繼續進行任務。

進行線上遊戲的過程中,死亡對玩家來說,不會有多大的直接感受,就像是馬莉兄弟一樣,反正遊戲一開始,就有一定的生命數量,死光了再投錢重來就好了,線上遊戲複雜一些,搭配一些資源損失的條件後,重生後就可以再繼續進行遊戲。

而無盡之劍的故事中,賽瑞斯並不了解,自己為了什麼目的,得去殺死神王,反正自己一出生,就被賦予這樣的任務,當然到了中間,賽瑞斯了解到自己每一次的重生,都有者同樣的使命,而這都是神王的策劃與陰謀。

在真實人生中,並沒有這樣的條件,能夠不斷重生,不斷被給予了下一次的機會,我們只能在遊戲裡面,體驗到可以重複累積的遊戲人生經驗,才能讓自己的過關技巧更豐富,也才能更接近破關的終點。

SAO

SAO Sword Art Online 刀劍神域 也是基於線上遊戲發展出來的一個動漫作品,在 SAO 中,桐谷和人跟其他的 VRMMORPG 玩家,因為無法登出 Sword Art Online 的遊戲,並被遊戲設計者茅場晶彥的限制下,必須在不被殺死的條件下,打倒位於「艾恩葛朗特」頂樓,第100層的Final Boss之後,才能結束這個線上遊戲並離開這個世界。在遊戲內Game Over或是嘗試脫下NERvGear,玩家會立刻被NERvGear發出的高頻率微波破壞腦部而死亡。

雖然也是線上遊戲的虛構故事,SAO 設計讓玩家用真實的生命作賭注,而不是用死亡去嘗試錯誤,累積過關的經驗,不過主角桐谷和人就是主角,不但可以作為一個 Solo Player,不跟其他人組隊,又可以跟眾多女生保持關係,最後還是破關。

試誤與保守的人生選擇

歐美國家的父母跟我們華人的父母最大的不同,就是在對小孩的態度與自由度的範圍有很大的差異,在照顧小朋友的時候,我們還是常常會以自己的經驗為出發點,認為小孩就是不該爬高、不應該跑跑跳跳、不應該如何如何,以免受傷跌倒或是生病感冒。

我們抱持的態度,就是生命只有一次,一旦受傷或遇到危機,也只有一次機會,因此會讓小孩畏畏縮縮,不敢做一些特別的嘗試。

歐美國家的小孩,父母普遍接受,小孩就是要爬來爬去,探索世界,只有他自己接觸到的經驗,才是他的人生經驗,如果因此受了一些傷,那下次就會知道該怎麼拿捏分寸,這也是一種「生命只有一次」的概念,但卻是完全相反的表現,正因為只有一次,所以該體驗各種不同的人生。

同樣都是人生體驗,確有不同的方式與感受,我自認自己也是用保守的方式生活,希望小孩可以在未來,活出自己的生活歷練與方法,不一定就要像他的父母一樣。

結語

我不認為無盡之劍能列入Brandon Sanderson的最佳作品的行列之中,但或許是故事本身就很龐大與複雜,而作者在配合遊戲原始劇情的限制下,才寫出這樣的作品,希望作者數本小說計畫中,還有多本舊故事的續集,不會再發生這樣的狀況。

參考

無盡之劍 wiki
Infifity Blade II: 玩家引頸期盼的魔幻冒險遊戲

2015/1/12

JVM 日期格式該使用 yyyy 或是 YYYY

去年年底有則 twitter 說明了誤用了日期格式化,使用了 YYYY 來格式化日期,在 2014/12/29 造成了日期錯誤,而把日期變成了 2015 年。

SimpleDateFormat 日期格式化

要在 Java 程式中格式化日期,最正確的格式化字串為

yyyy/MM/dd HH:mm:ss

第一次使用時,通常會遇到的問題是,MM 跟 mm 差別在哪裡,MM 代表是月份,而 mm 代表分鐘,這個部份的錯誤如果寫錯了,很容易就可以發現問題。

再來可能會遇到的問題,是把 HH 打成了 hh,HH 代表 24 小時制的小時,而 hh 是 Hour in am/pm (1-12) ,如果是在早上寫程式,那就不會發現自己寫錯了。

最糟糕的,就是年份遇到的問題,當日期落在 12/28~12/31 區間的時候,就會發生年份增加一年的錯誤。

測試程式

import java.util.Date;
import java.text.SimpleDateFormat;

class Test{
    public static void main(String[] args){

        System.out.println(
            new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));

    // 小時 錯誤
    System.out.println(
            new SimpleDateFormat("yyyy/MM/dd hh:mm:ss").format(new Date()));

    // 月份 錯誤
    System.out.println(
            new SimpleDateFormat("yyyy/mm/dd HH:mm:ss").format(new Date()));

    // 年份 錯誤
    System.out.println(
            new SimpleDateFormat("YYYY/MM/dd HH:mm:ss").format(new Date()));

    }
}

我們直接用 date 指令修改日期,當機器日期在 12/27 的時候,年份是沒有問題的。

> date 122720292014
六 12月 27 20:29:00 CST 2014
> java Test
2014/12/27 20:29:02
2014/12/27 08:29:02        -> 小時錯誤
2014/29/27 20:29:02        -> 月份錯誤
2014/12/27 20:29:02

當機器日期在 12/28~12/31 區間的時候,測試結果如下

> date 122920292014
一 12月 29 20:29:02 CST 2014
> java Test
2014/12/29 20:29:05
2014/12/29 08:29:05        -> 小時錯誤
2014/29/29 20:29:05        -> 月份錯誤
2015/12/29 20:29:05        -> 年份錯誤

Gregorian Calendar vs ISO 8601

一般我們使用的日曆系統為 Gregorian Calendar,他是以400年為一個週期,在這個週期中,一共有97個閏日,閏日儘可能均勻地分佈在各個年份中,所以一年的長度可能有 365天或366天。

而另一個時間規格標準 ISO 8601 卻是用不同的方式處理閏日,他是用潤週的概念,因此一年的長度是364或371天。而且 ISO 8601 規定一年中第一個週四所在的那個星期,作為一年的第一個星期。

今年2015年剛好1月1日就是週四,所以 12/28~12/31 算在 2015年第一週,如果用 ISO 8601 的規則,12/28 是 2015 年。

在 Java SimpleDateFormat 裡面,代表 ISO 8601的年份格式符號是 YYYY,Gregorian Calendar 的符號是 yyyy。

如果使用了 YYYY 作為 DateFormat 的格式符號,就會發生日期瞬間移動一年的問題。

結語

發生 HH 與 hh 的問題,頂多只是 12 小時的錯誤,如果發生了 YYYY 與 yyyy 的問題,就會有一年的落差。

不管如何,在處理日期格式時,必須將 文件 看清楚,在寫程式時,多花一點時間 review 一下,用直覺跟印象寫程式,就很容易出錯。

2015/1/6

Concurrency in Java

關於 Java Concurrency 議題,Java 並發編程的藝術 將所有相關的文章集結成了一本迷你書。在實作 java server side 程式時,一定會面臨到的問題就是 thread 之間的記憶體資料共享,另外還有在面對耗時的工作時,必須要同時執行的工作,也就是非同步呼叫。

synchronized vs volatile

volatile 是一種輕量級的同步機制,使用這種變數,就可以保證不會發生 context switching 或是 thread 切換的動作,但 volatile 的限制是,當變數的新值是依據舊值產生的時候,就不能使用 volatile。volatile 保證了共享變數的 visibility,當一個 thread 修改此共享變數時,另一個 thread 就能夠讀到這個修改後的值,簡單地說,就是 JVM 保證所有 thread 看到這個變數的值,都是一致的。

java 的 atomic operations 如下

  1. all assignments of primitive data types except for long and double
  2. all assignments of references
  3. all operations of java.concurrent.Atomic* classes
  4. all assignments of volatile longs and doubles

long foo = 87654321L;
並不是 thread-safe 的 operation,因為 java 會分成兩個步驟,先寫入 32bits 再寫入後 32bits,所以要改成

volatile long foo = 87654321L;

volatile 並不能完全取代 synchronied,甚至我們要注意,不能誤用 volatile,要不然就很容易出錯。一般基本判斷要不要使用 volatile 的條件,就是對一個變數讀取的次數遠高於寫入的次數。

可以使用 volatile 的情境如下:

狀態標籤

在另一個 thread 中呼叫 shutdown 用以中止 doWork 的無窮迴圈,這樣的寫法可保障 doWork 不回被惡意強制中斷。

volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

one-time safe publication

建立 singleton 物件,可使用 volatile 搭配 Double-checked locking 處理。

public class SingletonVolatile {

    private static volatile SingletonVolatile _instance;

    public static SingletonVolatile getInstance() {
        if (_instance == null) {
            synchronized (SingletonVolatile.class) {
                if (_instance == null)
                    _instance = new SingletonVolatile();
            }
        }

        return _instance;
    }

    public static void main(String[] args) {
        System.out.println(SingletonVolatile.getInstance());
        System.out.println(SingletonVolatile.getInstance());
        System.out.println(SingletonVolatile.getInstance());
    }
}

independent observation

定期 「發佈」 觀察結果供程序內部使用,一個後台thread可能會每隔幾秒讀取一次感應器,並更新包含當前文檔的 volatile 變量。然後,其他thread可以讀取這個變量,從而隨時能夠看到最新的溫度值。

以下是身份驗證機制記憶最近一次登錄的用戶的名字的範例。

public class UserManager {
    public volatile String lastUser;

    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

volatile bean

JavaBean 的所有數據成員都是 volatile 類型的,並且 getter 和 setter 方法必須非常普通 —— 除了獲取或設置相應的屬性外,不能包含任何邏輯,對於物件引用的成員,引用的對象必須是有效不可變的。

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }

    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }

    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }

    public void setAge(int age) { 
        this.age = age;
    }
}

開銷較低的讀-寫鎖策略

如果讀操作遠遠超過寫操作,可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;

    public int getValue() { return value; }

    public synchronized int increment() {
        return value++;
    }
}

CAS(compare and swap)

獨佔鎖是一種悲觀鎖,synchronized 是一種獨佔鎖,它只有在確保其它線程不會造成干擾的情況下執行。樂觀鎖假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。

實現 lock-fre 的 non-blocking 演算法,最有名的方式就是使用 CAS (compare and swap),CAS是個樂觀鎖技術,當有多個threads嘗試同時更新同一個變數時,只有其中一個 thread 能成功更新變數的值,而其它threads都會被告知更新失敗,然後再重試一次,失敗的threads並不會被暫停執行的程序。

CAS內部有3個內部變數,(1) 記憶體實際值V (2) 目標變數的舊預期值A (3) 目標變數要被修改的新值B,當 A==V時,才能將 V 修改為 B,否則什麼都不做。

ConcurrentHashMap

ConcurrentHashMap 是 HashMap 的 multithread 版本,只要會有多個 thread 會同時存取的資料結構,就必須使用 ConcurrentHashMap。至於早期 Java 版本使用的 HashTable,由於內部是以 synchronized 的方式來實作,再面對大量儲存資料的狀況下,其使用效率就會遠低於比 ConcurrentHashMap。

ConcurrentHashMap 是以 Segment 與 HashEntry 組成,Segment 就類似於 HashMap,因此我們可以同時存取對多個 Segment,但到了 Segment 這一層,就會是 synchronzied 的呼叫,所以只要 hash 過後的資料,落入了不同的 Segment,這樣才能保障 ConcurrentHashMap 可以發揮它的功能。

ConcurrentHashMap 的 get method 把所有分享的變數都設定為 volatile,在處理過程中不需要加鎖,除非讀到的值是空的才會加鎖重讀,volatile 可保證只能被單一 thread 修改,且修改後也不會讀到舊的資料,重要的是可讓多個 thread 同時讀取。

put method 為了 thread-safe 必須加鎖處理。size method 裡面是以累加 Segment 裡面的 volatile 變數 count 實作的,由於 count 的變化機率很小,累加過程多半沒有發生 count 的變化,size 會先嘗試兩次不加鎖累加。但如果發生了,就會再進行加鎖累加。

count 的變化判斷是以 modCount 變數進行,在 put, remove, clean 之前,都會先將 modCount 加 1。

ThreadPool

通常在使用 J2EE Server 連結 DB 時,會利用 DB Connection Pool 當作中間層,這可以減少 DB Connection 建立與銷毀所消耗掉的資源。

至於 JVM 的 thread 也一樣,可以使用 ThreadPool 來進行 Thread 的 reuse 與 管理,不僅可減少建立與銷毀 thread 所消耗掉的資源,也可以縮短任務處理的時間。

thread 跟 db connection 都屬於 JVM 的外部資源,這些資源都有使用的上限,以 pool 的方式進行使用管理,而不是無限制地建立與使用,可以保護 server 並提高穩定度。

建立 ThreadPool

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

相關的參數

  1. corePoolSize(pool的基本大小)
    ThreadPool 會持續建立 thread,直到 thread 的數量達到 corePoolSize。如果呼叫 prestartAllCoreThreads method,ThreadPool 就會提前建立所有基本線程。

  2. runnableTaskQueue
    儲存等待執行的任務的blocking queue

    2.1 ArrayBlockingQueue:以 Array 實作,FIFO(先進先出)

    2.2 LinkedBlockingQueue:以 LinkedList 實作,FIFO (先進先出),throughput比ArrayBlockingQueue好。Executors.newFixedThreadPool() 就是使用這個queue。

    2.3 SynchronousQueue:enqueue必須等到另一個線程呼叫移除才能執行,否則enqueue就會一直處於阻塞狀態,throughput比LinkedBlockingQueue高,Executors.newCachedThreadPool使用了這個queue。

    2.4 PriorityBlockingQueue

  3. maximumPoolSize(pool最大大小)
    ThreadPool允許建立的最大線程數。

  4. ThreadFactory
    用於建立thread的factory

  5. RejectedExecutionHandler(飽和策略)
    當 Queue 與 ThreadPool 都滿了,必須採取一種策略處理提交的新任務。以下是JDK1.5提供的四種策略。
    5.1 AbortPolicy:預設值,直接拋出異常。
    5.2 CallerRunsPolicy:使用呼叫端所在線程來執行任務
    5.3 DiscardOldestPolicy:丟棄 queue 裡最近的一個任務,並執行當前任務
    5.4 DiscardPolicy:不處理,直接丟棄
    5.5 自訂 RejectedExecutionHandler

  6. keepAliveTime
    thread 空閒後,保持存活的時間。如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。

  7. TimeUnit
    天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)

提交處理任務的方式有兩種

  1. execute方法沒有返回值,所以無法判斷任務是否被線程池執行成功

    threadsPool.execute(new Runnable() {
             @Override
             public void run() {
             }
         });
    
  2. 使用 submit 來提交任務,它會返回一個future,我們可以通過這個future來判斷任務是否執行成功。future.get()會 blocking 住,直到任務完成,如果使用 get(long timeout, TimeUnit unit) 則會阻塞一段時間後立即返回。

    Future<Object> future = executor.submit(harReturnValuetask);
    try {
      Object s = future.get();
    } catch (InterruptedException e) {
     // 處理中斷異常
    } catch (ExecutionException e) {
     // 處理無法執行任務異常
    } finally {
     // 關閉線程池
     executor.shutdown();
    }
    

當提交一個新任務到 ThreadPool 處理流程如下:

  1. 判斷基本線程池是否已滿?沒滿,建立一個工作線程來執行任務。滿了,則進入下個流程。
  2. 判斷工作隊列是否已滿?沒滿,則將新提交的任務儲存在工作隊列裡。滿了,則進入下個流程。
  3. 判斷整個線程池是否已滿?沒滿,則建立一個新的工作線程來執行任務,滿了,則交給 RejectedExecutionHandler 飽和策略來處理這個任務。

使用 ThreadPool 之前必須先分析任務特性,可以從以下幾個角度來進行分析:

  1. 任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
    CPU密集型任務,儘可能小的線程,如配置N(cpu)+1個線程的線程池
    IO密集型任務則由於線程並不是一直在執行任務,則配置儘可能多的線程,如2*N(cpu)
    混合型的任務,如果可以拆分,則將其拆成一個CPU密集型任務和一個IO密集型任務
    Runtime.getRuntime().availableProcessors() 可取得當前的CPU個數
  2. 任務的優先級:高,中和低。
    優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先得到執行,但如果一直有優先級高的任務,那麼優先級低的任務可能永遠會不能被執行。
  3. 任務的執行時間:長,中和短。
    執行時間不同的任務可以交給不同規模的線程池來處理,也可以使用優先級隊列,讓執行時間短的任務先執行。
  4. 任務的依賴性:是否依賴其他系統資源,如DB連接。
    因為發送SQL後需要等待DB返回結果,如果等待的時間越長CPU空閒時間就越長,因此thread 數量應該設置越大,這樣才能更好的利用CPU。

監控 ThreadPool

有一些屬性在監控 ThreadPool 的時候可以使用

  1. taskCount:需要執行的任務數量
  2. completedTaskCount:在執行過程中已完成的任務數量
  3. largestPoolSize:曾經建立過的最大線程數量
  4. getPoolSize:Thread數量如果線程池不銷毀的話,池裡的線程不會自動銷毀,所以這個大小只增不+ getActiveCount:獲取活動的線程數。

可以繼承 ThreadPool 並覆寫 beforeExecute,afterExecute 和 terminated方法,可以在任務執行前,執行後和線程池關閉前,監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法在線程池裡是空的方法。

protected void beforeExecute(Thread t, Runnable r) { }

Reference

聊聊並發系列文章

非阻塞同步算法與CAS(Compare and Swap)無鎖算法

JDK, Jetty, Tomcat 線程池的實現算法分析
Java 理論與實踐: 正確使用 Volatile 變量