多線程設計模式解讀6-single threaded Execution模式(附分佈式環境下的操作)

Single Threaded Execution模式主要是用於確保同一時間內只能讓一個線程執行處理,說通俗點就是對synchronized的標準化使用方式,這是比較基礎的,所以我們前面重點介紹下如何保證同一個Jvm進程內的多線程同步,後面擴展開來,保證多個Jvm進程間多線程同步(分佈式環境)。兩者有很大的相似性。

單jvm進程下:

先看下一個簡單的例子:

public class Ticket {

  private int counter = 100;

  public  void dec() {
    if(counter>0) {
      System.out.println(Thread.currentThread().getName() + "號窗口賣出:" + this.counter-- + "號票");
    }else{
      System.out.println("票已售完");
    }
  }

}


public class StationThread  extends Thread{
  Ticket ticket;

  public StationThread(Ticket ticket) {
    this.ticket = ticket;
  }
  @Override
  public void run() {
    while (true) {
      try{
        Thread.sleep(500);
      }catch(InterruptedException e){
        e.printStackTrace();
      }
      ticket.dec();
    }
  }

}



public class Main {

  public static void main(String[] args) {
    System.out.println("Testing...");
    Ticket ticket = new Ticket();
    new StationThread(ticket).start();
    new StationThread(ticket).start();
    new StationThread(ticket).start();
  }
}

這裏打印結果可知,執行明顯是錯誤的:

Testing...
Thread-0號窗口賣出:100號票
Thread-1號窗口賣出:99號票
Thread-2號窗口賣出:98號票
Thread-1號窗口賣出:97號票
Thread-2號窗口賣出:97號票
Thread-0號窗口賣出:97號票
Thread-2號窗口賣出:96號票
Thread-1號窗口賣出:96號票
Thread-0號窗口賣出:96號票
......

爲什麼會出錯呢?因爲Ticket不是線程安全的,this.counter--並不是一個原子性的操作,其中包含了讀取,修改,寫入,多個線程執行的時候,這些命令會交錯執行,導致執行結果與預期不一致。

接下來,我們改下Ticket的方法:

public synchronized void dec() {
    if(counter>0) {
      System.out.println(Thread.currentThread().getName() + "號窗口賣出:" + this.counter-- + "號票");
    }else{
      System.out.println("票已售完");
    }
  }

添加了synchronized後,執行結果正常:

Testing...
Thread-0號窗口賣出:100號票
Thread-1號窗口賣出:99號票
Thread-2號窗口賣出:98號票
Thread-0號窗口賣出:97號票
Thread-1號窗口賣出:96號票
Thread-2號窗口賣出:95號票
Thread-0號窗口賣出:94號票
Thread-1號窗口賣出:93號票
Thread-2號窗口賣出:92號票
Thread-1號窗口賣出:91號票
Thread-2號窗口賣出:90號票
Thread-0號窗口賣出:89號票
Thread-1號窗口賣出:88號票
Thread-0號窗口賣出:87號票
Thread-2號窗口賣出:86號票
Thread-1號窗口賣出:85號票
Thread-2號窗口賣出:84號票
Thread-0號窗口賣出:83號票
Thread-1號窗口賣出:82號票
Thread-2號窗口賣出:81號票
Thread-0號窗口賣出:80號票
Thread-1號窗口賣出:79號票
Thread-2號窗口賣出:78號票
Thread-0號窗口賣出:77號票
......

synchronized保證了方法只能由一個線程,防止了由多個線程交錯執行的情況。我們知道,編寫線程安全的代碼,核心在於對狀態訪問操作進行管理,特別是共享和可變的狀態,這裏Ticket就是一個共享資源(SharedResource),通過single thread execution模式,將非安全的方法聲明爲synchronized方法,確保同一時間只被一個線程訪問。

使用時注意事項:

1、死鎖問題:

死鎖指多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。就如同一座橋,只能容納一輛車,有兩輛車,相向而行,分到橋的中途,需要佔據對方的空間才能通過。因此,就一直處於阻塞狀態。

死鎖主要是由於多個線程加鎖順序不一致導致的,可以從這裏角度分析,防止死鎖。

2、性能

獲取鎖花費時間,線程衝突會引起等待,爲了提高性能,需要管理好鎖的臨界區,確定同步代碼塊的合理大小。

多jvm進程下:

分佈式鎖的方式:

一、通過如下命令在redis中實現分佈式鎖的功能

SET key value NX EX max-lock-time

其中NX是操作模式,表示只在鍵不存在時,纔對鍵進行設置操作。

EX max-lock-time用於設置鍵的過期時間爲max-lock-time秒。

這個命令連續兩次執行結果如下:

test-redis:0>SET not-exists-key "value" NX EX 60
OK

test-redis:0>SET not-exists-key "value" NX EX 60
NULL

過六十秒後再執行:

test-redis:0>SET not-exists-key "value" NX EX 60
OK

這樣,當一個線程執行命令成功,說明key原本不存在,該線程成功得到了鎖;當設置失敗時,說明key已經存在,該線程搶鎖失敗。

當到達過期時間,或者key被刪除(del),說明鎖被釋放,其他線程可以繼續執行這個命令來獲取鎖。

通過redis實現分佈式鎖有許多實現細節需要注意的:

1、很多人會通過setnx代替SET key value NX命令,但前者沒有設置過期時間的參數,因此設置key值和設置過期時間便成爲一個複合操作,不具備原子性,當一個線程設置了key值,但未設置過期時間,這時相關的節點掛了,但key一直存在,那其他線程就永遠無法獲取這個鎖了(死鎖)。

2、過期時間很難設置,如果設置短了,假設獲得鎖的A線程的任務還沒執行完成,這時候鎖就被釋放了,其他線程就會獲得鎖,導致難以預料的一系列後果。如A線程執行完後誤刪了後一個線程的鎖,共享數據被破壞等等。對此,我們可以通過開一個守護線程,當線程任務未執行完成,給鎖續期。

二、zookeeper實現分佈式鎖

zookeeper分佈式鎖,實現更加完善,封裝更好一點,因此,使用更加方便。

首先介紹下zookeeper分佈式鎖的實現原理。

爲了構建這個鎖,zookeeper創建一個持久的znode,它將作爲父節點。試圖獲得鎖的客戶端將在父節點下面創建順序的、臨時的子節點。鎖是由子節點具有最低的序列號的客戶端進程擁有的。在圖1中,鎖節點有三個子節點,而節點1在這個時間點擁有鎖,因爲它的序列號是最低的。如果客戶端創建的節點不是最小節點,就獲得該節點的上一順序節點,並給它註冊watcher,同時在這裏阻塞,等待監聽事件的發生。當完成之後,關閉ZooKeeper連接,進而可以引發監聽事件,釋放該鎖(在刪除節點1之後,鎖被釋放), 然後擁有節點2的客戶端擁有這個鎖,以此類推。

zookeeper實現類似等待隊列的機制,大大提升了搶鎖的效率。

另外我們來看下,zookeeper有沒有類似redis分佈式鎖那樣的問題。我們發現它是不需要設置過期時間的,當任務完成時,客戶端會刪除節點,進而釋放鎖;當客戶端掛掉,相應的臨時會自動刪除,鎖被釋放,其下一個序列的節點會收到通知,獲取鎖;當連接中斷時則根據配置的重試機制重新連接。

Apache Curator,包含了對zookeeper分佈式鎖的實現,下面是使用代碼,有興趣可以研究下源碼:

1、創建一個重試策略,然後使用CuratorFrameworkFactory.newClient()來獲得CuratorFramework的實例

RetryPolicy retryPolicy = new ExponentialBackoffRetry(baseSleepTimeMills, maxRetries);

CuratorFramework client = CuratorFrameworkFactory.newClient(hosts, retryPolicy);

client.start();

2、爲特定的鎖路徑(lockPath)創建一個進程互斥鎖,獲取鎖,執行一些操作,然後釋放鎖。

InterProcessLock lock = new InterProcessMutex(client, lockPath);

if (lock.acquire(waitTimeSeconds, TimeUnit.SECONDS)) {

  try {

    // do work while we hold the lock

  } catch (Exception ex) {

    // handle exceptions as appropriate

  } finally {

    lock.release();

  }

} else {

  // we timed out waiting for lock, handle appropriately

}

3、不要忘記關閉client

client.close();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章