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();