java筆記:熟練掌握線程技術—基礎篇之解決資源共用的問題(中)–前篇

來源:互聯網
上載者:User

  上篇裡我講到了java裡如何去建立一個線程的問題,上篇的代碼裡建立的線程都是獨立的,也就是說建立的線程都不會相互的幹擾,獨立的進行屬於自己的運算,更重要的是上篇博文裡建立的線程所使用的資源都是獨佔式的,不會有人跟它爭,但是實際對線程的應用中,更多的也是更難的還是幾個線程會搶奪同一個資源,比如火車的售票系統,碰到這樣的問題就麻煩多了。

  由於這個問題比較複雜我把線程的基礎篇中篇分為兩篇文章來發布,今天是前篇。回到主題吧,當N多的線程同時訪問一個資源,並且N多的線程都有對這個資源修改和訪問的能力,解決資源衝突的技術就太重要了,記得我在研究前端最佳化技術的時候,腦海裡浮現最多的名詞就是高並發,而對於網站在高並發下又能保證資料的準確性的問題,在我知道java線程調度機制是隨機切換時間片的時候,我就感到這個問題比想象中要複雜的多。

  為了便於闡述我要講的主題,我想要寫一個監控程式(Watcher),這個監控程式可以隨時檢查我們調用的資源的內容比如數字,代碼如下:

View Code

package cn.com.sxia;

public class AlwaysEven {

private int i;

public void next(){
i++;
i++;
}

public int getValue(){
return i;
}

public static void main(String[] args) {
final AlwaysEven ae = new AlwaysEven();

new Thread("Wacther"){
public void run(){
while(true){
int val = ae.getValue();
if (val % 2 == 0){
System.out.println(val);
System.exit(0);
}
}
}
}.start();

while(true)
{
ae.next();
}
}

}

  程式註解如下:

  AlwaysEven類裡有一個屬性i,next方法每執行一次i的值會自動加2,getValue返回i的數值。在main函數裡我們構建了一個AlwaysEven對象ae,注意這個變數前一定要用final,否則在監控線程裡是不能訪問到這個變數,最後我們寫了一個死迴圈:調用next方法。

  當我們多次執行這個main函數,發現列印出來的結果都會不一樣。這個現象道出了運用線程所會遇到的一個基本問題:我們永遠都不知道線程何時會運行。這個感覺就像我們創造了一支筆,想用它寫字,寫著寫著,在沒有任何徵兆的情況下筆不見了,這個實在是很鬱悶,但這種情況就是我們在寫並發程式經常會遇到的問題。

  上面的例子也表現了不同線程共同使用一個資源的現象,監控線程監視ae對象裡i屬性的數值變化,在主線程main裡面又不斷調用next方法增加i的數值。這就是在爭搶同一個資源的執行個體。

  為了更好闡述我後面要闡述的內容,這裡我要補充一下在上篇裡漏掉的一部分線程的知識:後台線程(daemon)。後台線程(daemon)是指在程式啟動並執行時候在後台提供一種泛型服務的線程,並且這種線程不是屬於程式裡不可或缺的部分。所以,當所有的非後台線程結束時,程式也就終止了,反過來說只要有任何非後台線程還在運行,程式就不會終止。大家看下面的代碼:

View Code

package cn.com.sxia;

public class SimpleDaemon extends Thread {

public SimpleDaemon(){
setDaemon(true);
start();
}

public void run(){
while(true){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this);
}
}

public static void main(String[] args) {
for (int i = 0;i < 10;i++){
new SimpleDaemon();
}
}

}

  要想使線程成為後台線程,必須則線上程啟動前調用setDaemon()方法,才能把這個線程設定為後台線程。當我們運行這個程式時候,發現沒有任何結果列印到控制台,這就是因為沒有非後台線程(除了main,main是一個非後台線程)使得程式保持運行。因此,程式沒有列印任何資訊就停止了。

  好了,現在回到我們講到的第一個執行個體代碼,我想根據這個代碼改寫下,寫一個測試架構,這個架構可以簡化對我們遇到這種類型線程例子的測試工作。我們的watcher線程實際上是觀察特定情況下監控對象內部是否違反了約束條件,對於客戶而言,客戶指向知道我們定義的約束條件是否被違反了,還要知道這個違反約束條件的數值是多少,如是我定義了下面的介面:

package cn.com.sxia;

public interface InvariantState {

}

  這介面就是為了查看數值是否違反我們定義約束的介面,它有兩個實作類別:

  表示成功的:

package cn.com.sxia;

public class InvariantOK implements InvariantState {

}

  表示失敗的:

package cn.com.sxia;

public class InvariantFailure implements InvariantState {
public Object value;

public InvariantFailure(Object value)
{
this.value = value;
}
}

  在InvariantFailure對象將包括一個對象,這個對象表示了有關失敗原因的資訊,當監控到失敗情況我們就可以列印出有關失敗的錯誤資訊。

  下面我們再定義一個介面,任何需要對我們定義的約束條件進行測試的類都必須要實現這個介面:

package cn.com.sxia;

public interface Invariant {
InvariantState invariant();
}

  為了防止程式因為所啟動並執行平台(例如不同版本的windows,linux,多核系統等)對java底層支援人員的問題我們再定義一個逾時類,這個類當程式在一定時間內無法正常運行時候,程式會自動終止,代碼如下:

package cn.com.sxia;

import java.util.Timer;
import java.util.TimerTask;

public class Timeout extends Timer {

public Timeout(int delay,final String msg){
super(true);//設為true表明該線程是一個後台線程(Daemon)
schedule(new TimerTask() {

@Override
public void run() {
System.out.println(msg);
System.exit(0);
}
}, delay);
}

}

  代碼裡我們繼承了Timer類,在建構函式裡我們調用了super(true),這個設定表明此線程將作為一個背景程式建立,前面我們講到後台線程不會影響到非背景程式,也就是說當其他線程讓程式退出時候,這個建立的Timeout對象不會干擾其他線程的運行。Timer類非常有用,java裡設計它就是為了處理大量並發調度任務,下面是Timer在jdk文檔裡的解釋:

    public class Timer extends Object
一種工具,線程用其安排以後在後台線程中執行的任務。可安排任務執行一次,或者定期重複執行。
與每個 Timer 對象相對應的是單個後台線程,用於順序地執行所有計時器任務。計時器任務應該迅速完成。如果完成某個計時器任務的時間太長,那麼它會“獨佔”計時器的任務執行線程。因此,這就可能延遲後續任務的執行,而這些任務就可能“堆在一起”,並且在上述不友好的任務最終完成時才能夠被快速連續地執行。
對 Timer 對象最後的引用完成後,並且 所有未處理的任務都已執行完成後,計時器的任務執行線程會正常終止(並且成為記憶體回收的對象)。但是這可能要很長時間後才發生。預設情況下,任務執行線程並不作為守護線程 來運行,所以它能夠阻止應用程式終止。如果調用者想要快速終止計時器的任務執行線程,那麼調用者應該調用計時器的 cancel 方法。
如果意外終止了計時器的任務執行線程,例如調用了它的 stop 方法,那麼所有以後對該計時器安排任務的嘗試都將導致 IllegalStateException,就好像調用了計時器的 cancel 方法一樣。
此類是安全執行緒的:多個線程可以共用單個 Timer 對象而無需進行外部同步。
此類不 提供即時保證:它使用 Object.wait(long) 方法來安排任務。
實現注意事項:此類可擴充到大量同時安排的任務(存在數千個都沒有問題)。在內部,它使用二進位堆來表示其任務隊列,所以安排任務的開銷是 O(log n),其中 n 是同時安排的任務數。
實現注意事項:所有構造方法都啟動計時器線程。

  一切都準備好了,我們建立用於進行監控的完美類了,代碼如下:

package cn.com.sxia;

public class InvariantWatcher extends Thread {

private Invariant invariant;

public InvariantWatcher(Invariant invariant){
this.invariant = invariant;
setDaemon(true);
start();
}

public InvariantWatcher(Invariant invariant,final int timeout){
this(invariant);
new Timeout(timeout, "逾時了....");
}

public void run(){
while(true){
InvariantState state = invariant.invariant();
if (state instanceof InvariantFailure){
System.out.println("Invariant violated: " + ((InvariantFailure)state).value);
System.exit(0);
}
}
}

}

  InvariantWatcher類就是我們定義好的監控類,InvariantWatcher類裡我定義了兩個建構函式,第一個建構函式接受一個要測試的Invariant對象的引用作為參數,然後啟動線程。第二個建構函式調用第一個建構函式,然後建立一個Timeout,用來在一定的時間延遲之後終止所有的線程。

  特別注意:我們不能再線程裡拋出異常,因為這隻會終止線程而不會終止程式,所以我都是寫的是System.exit(0);

  下面我們將我們的個執行個體代碼修改下,代碼如下:

package cn.com.sxia;

public class EvenGenerator implements Invariant {

private int i;

public void next(){
i++;
i++;
}

public int getValue(){
return i;
}

@Override
public InvariantState invariant() {
int val = i;
if (val % 2 == 0)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}

public static void main(String[] args){
EvenGenerator gen = new EvenGenerator();
new InvariantWatcher(gen);
while(true){
gen.next();
}
}

}

  我們為學習共用資源的java線程問題所設計的監控測試架構已經完成了,或許有些人可能不太明白為啥要這麼設計,沒事,先把代碼在eclipse裡跑跑就會有點感覺了,我們接著往下看了。

  我們先從理論開始,共用資源的線程難題到底是啥呢?我們還是套用用筆的例子,有一支筆,兩個人同時都要使用它,結果是兩個人爭執不下,最後誰都沒用到這支筆,大家都苦耗在哪裡。

  因此我們應該在使用多線程時候避免這樣的事情的發生,要防止這樣問題的發生,只要線上程使用資源的時候給它加一把鎖就行了。那麼情形就會變成這樣,訪問該資源的第一個線程給資源加鎖後,其他線程只能等待第一個線程把鎖解開才能訪問資源,鎖解除的同時另外一個線程就可以對該資源加鎖並且進行訪問了。

  這裡我又將引入線程裡又一個重要的概念:訊號量

  什麼是訊號量了?這個問題似乎很複雜,我現在獲得的理解應該是最簡單的理解,下面是我從網上資料總結出來的結論:

多個線程訪問某一個資源,例如資料庫的串連。假想在伺服器上運行著若干個回答用戶端請求的線程。這些線程需要串連到同一資料庫,但任一時刻只能獲得一定數目的資料庫連接。你要怎樣才能夠有效地將這些固定數目的資料庫連接分配給大量的線程?一種控制訪問一組資源的方法(除了簡單地上鎖之外),就是使用眾所周知的訊號量計數 (counting semaphore)。Java多線程訊號量計數將一組可獲得資源的管理封裝起來。訊號量是在簡單上鎖的基礎上實現的,相當於能令安全執行緒執行,並初始化為可用資源個數的計數器。例如我們可以將一個訊號量初始化為可獲得的資料庫連接個數。一旦某個線程獲得了Java多線程訊號量,可獲得的資料庫連接數減一。線程消耗完資源並釋放該資源時,計數器就會加一。當訊號量控制的所有資源都已被佔用時,若有線程試圖訪問此訊號量,則會進入阻塞狀態,直到有可用資源被釋放。Java多線程訊號量最常見的用法是解決“消費者-生產者問題”。當一個線程進行工作時,若另外一個線程訪問同一共用變數,就可能產生此問題。消費者線程只能在生產者線程完成生產後才能夠訪問資料。使用訊號量來解決這個問題,就需要建立一個初始化為零的訊號量,從而讓消費者線程訪問此訊號量時發生阻塞。每當完成單位工作時,生產者線程就會向該訊號量發訊號(釋放資源)。

  對於訊號量我們可以簡單的這麼來理解它,訊號量就是兩個線程間通訊的標誌對象。訊號量為0,則表明訊號量監控的資源是可用的,不為零則訊號量監控的資源是停用,線程們都要等待了,當資源可用的時候,線程會增加訊號量的值,然後繼續執行並使用這個監控資源,而訊號量這種增加值和減少值的操作是不能被中斷的,很保險,所以訊號量能夠保證兩個線程同時訪問同一個資源的時候不產生衝突。下面是訊號量概念的簡化版:

package cn.com.sxia;

public class Semaphore implements Invariant {

private volatile int semaphore = 0;

public void acquire(){
++semaphore;
}

public boolean available(){
return semaphore == 0;
}

public void release(){
--semaphore;
}

@Override
public InvariantState invariant() {
int val = semaphore;
if (val == 0 || val == 1){
return new InvariantOK();
}else{
return new InvariantFailure(new Integer(val));
}
}

}

  這個代碼裡包括三個方法,既然線程在擷取資源的時候要檢查可用性,我們讓調用該類對象,在邏輯上使得semaphore的值都不會是0或1,下面是我寫的測試代碼了:

package cn.com.sxia;

public class SemaphoreTester extends Thread {

private volatile Semaphore semaphore;

public SemaphoreTester(Semaphore semaphore){
this.semaphore = semaphore;
setDaemon(true);
start();
}

public void run(){
while(true){
if (semaphore.available()){
yield();
semaphore.acquire();
yield();
semaphore.release();
yield();
}
}
}

public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore();
new SemaphoreTester(semaphore);
new SemaphoreTester(semaphore);
new InvariantWatcher(semaphore).join();
}

}

 大家可以看到run方法裡的內容保證了semaphore值都是在0或1來進行,但是我們運行這個main函數總會有報錯的時候,例如:

Invariant violated: -1

  程式報錯退出了,多個線程訪問同一個資源會造成資料的錯誤,這是我們寫多線程程式最大的風險所在。

  好了,今天學到這裡了,明天我將解決共用資源的博文寫完。

 

  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.