Java并发教程–锁定:内在锁
在以前的文章中,我們回顧了在不同線程之間共享數據的一些主要風險(例如原子性和可見性 )以及如何設計類以安全地共享( 線程安全的設計 )。 但是,在許多情況下,我們將需要共享可變數據,其中一些線程將寫入而其他線程將充當讀取器。 可能的情況是,您只有一個域,與其他域無關,需要在不同線程之間共享。 在這種情況下,您可以使用原子變量。 對于更復雜的情況,您將需要同步。
1.咖啡店的例子
讓我們從一個簡單的示例開始,例如CoffeeStore 。 此類開設了一家商店,客戶可以在此購買咖啡。 客戶購買咖啡時,會增加一個計數器,以跟蹤所售商品的數量。 商店還注冊誰是最后一個來商店的客戶。
public class CoffeeStore {private String lastClient;private int soldCoffees;private void someLongRunningProcess() throws InterruptedException {Thread.sleep(3000);}public void buyCoffee(String client) throws InterruptedException {someLongRunningProcess();lastClient = client;soldCoffees++;System.out.println(client + " bought some coffee");}public int countSoldCoffees() {return soldCoffees;}public String getLastClient() {return lastClient;} }在以下程序中,四個客戶決定來商店購買咖啡:
public static void main(String[] args) throws InterruptedException {CoffeeStore store = new CoffeeStore();Thread t1 = new Thread(new Client(store, "Mike"));Thread t2 = new Thread(new Client(store, "John"));Thread t3 = new Thread(new Client(store, "Anna"));Thread t4 = new Thread(new Client(store, "Steve"));long startTime = System.currentTimeMillis();t1.start();t2.start();t3.start();t4.start();t1.join();t2.join();t3.join();t4.join();long totalTime = System.currentTimeMillis() - startTime;System.out.println("Sold coffee: " + store.countSoldCoffees());System.out.println("Last client: " + store.getLastClient());System.out.println("Total time: " + totalTime + " ms"); }private static class Client implements Runnable {private final String name;private final CoffeeStore store;public Client(CoffeeStore store, String name) {this.store = store;this.name = name;}@Overridepublic void run() {try {store.buyCoffee(name);} catch (InterruptedException e) {System.out.println("interrupted sale");}} }主線程將使用Thread.join()等待所有四個客戶端線程完成。 一旦客戶離開,我們顯然應該算出我們商店中售出的四種咖啡,但是您可能會得到與上面的類似的意外結果:
Mike bought some coffee Steve bought some coffee Anna bought some coffee John bought some coffee Sold coffee: 3 Last client: Anna Total time: 3001 ms我們丟了一杯咖啡,最后一位客戶(John)也不是那一位(Anna)。 原因是由于我們的代碼未同步,因此線程交錯。 我們的buyCoffee操作應設為原子操作。
2.同步如何工作
同步塊是由鎖保護的代碼區域。 當線程進入同步塊時,它需要獲取其鎖,并且一旦獲取,它就不會釋放它,直到退出該塊或引發異常。 這樣,當另一個線程嘗試進入同步塊時,只有所有者線程釋放它后,它才能獲取其鎖。 這是Java機制,可確保僅在給定時間在線程上執行同步的代碼塊,從而確保該塊內所有動作的原子性。
好的,所以您使用鎖來保護同步塊,但是什么是鎖? 答案是任何Java對象都可以用作鎖,稱為內在鎖。 現在,我們將看到使用同步時這些鎖的一些示例。
3.同步方法
同步方法由兩種類型的鎖保護:
- 同步實例方法 :隱式鎖定為“ this”,這是用于調用該方法的對象。 此類的每個實例將使用自己的鎖。
- 同步靜態方法 :鎖是Class對象。 此類的所有實例將使用相同的鎖。
像往常一樣,用一些代碼可以更好地看到這一點。
首先,我們將同步一個實例方法。 它的工作方式如下:我們有兩個線程(線程1和線程2)共享該類的一個實例,而另一個線程(線程3)使用了另一個實例:
public class InstanceMethodExample {private static long startTime;public void start() throws InterruptedException {doSomeTask();}public synchronized void doSomeTask() throws InterruptedException {long currentTime = System.currentTimeMillis() - startTime;System.out.println(Thread.currentThread().getName() + " | Entering method. Current Time: " + currentTime + " ms");Thread.sleep(3000);System.out.println(Thread.currentThread().getName() + " | Exiting method");}public static void main(String[] args) {InstanceMethodExample instance1 = new InstanceMethodExample();Thread t1 = new Thread(new Worker(instance1), "Thread-1");Thread t2 = new Thread(new Worker(instance1), "Thread-2");Thread t3 = new Thread(new Worker(new InstanceMethodExample()), "Thread-3");startTime = System.currentTimeMillis();t1.start();t2.start();t3.start();}private static class Worker implements Runnable {private final InstanceMethodExample instance;public Worker(InstanceMethodExample instance) {this.instance = instance;}@Overridepublic void run() {try {instance.start();} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + " interrupted");}}} }由于doSomeTask方法是同步的,因此您希望在給定的時間只有一個線程將執行其代碼。 但這是錯誤的,因為它是一個實例方法。 不同的實例將使用不同的鎖,如輸出所示:
Thread-1 | Entering method. Current Time: 0 ms Thread-3 | Entering method. Current Time: 1 ms Thread-3 | Exiting method Thread-1 | Exiting method Thread-2 | Entering method. Current Time: 3001 ms Thread-2 | Exiting method由于線程1和線程3使用不同的實例(因此使用了不同的鎖),因此它們都同時進入該塊。 另一方面,線程2使用與線程1相同的實例(和鎖)。 因此,它必須等到線程1釋放鎖。
現在,讓我們更改方法簽名并使用靜態方法。 除以下行外, StaticMethodExample具有相同的代碼:
public static synchronized void doSomeTask() throws InterruptedException {如果執行main方法,將得到以下輸出:
Thread-1 | Entering method. Current Time: 0 ms Thread-1 | Exiting method Thread-3 | Entering method. Current Time: 3001 ms Thread-3 | Exiting method Thread-2 | Entering method. Current Time: 6001 ms Thread-2 | Exiting method由于同步方法是靜態的,因此它由Class對象鎖保護。 盡管使用了不同的實例,所有線程仍需要獲取相同的鎖。 因此,任何線程都必須等待上一個線程釋放鎖。
4.回到咖啡店的例子
我現在修改了Coffee Store示例,以使其方法同步。 結果如下:
public class SynchronizedCoffeeStore {private String lastClient;private int soldCoffees;private void someLongRunningProcess() throws InterruptedException {Thread.sleep(3000);}public synchronized void buyCoffee(String client) throws InterruptedException {someLongRunningProcess();lastClient = client;soldCoffees++;System.out.println(client + " bought some coffee");}public synchronized int countSoldCoffees() {return soldCoffees;}public synchronized String getLastClient() {return lastClient;} }現在,如果我們執行該程序,我們將不會失去任何銷售:
Mike bought some coffee Steve bought some coffee Anna bought some coffee John bought some coffee Sold coffee: 4 Last client: John Total time: 12005 ms完善! 好吧,真的是嗎? 現在程序的執行時間為12秒。 您肯定已經注意到在每次銷售期間都會執行someLongRunningProcess方法。 它可以是與銷售無關的操作,但是由于我們同步了整個方法,所以現在每個線程都必須等待它執行。 我們可以將這段代碼放在同步塊之外嗎? 當然! 下一節將介紹同步塊。
5.同步塊
上一節向我們展示了我們可能并不總是需要同步整個方法。 由于所有同步代碼都強制對所有線程執行進行序列化,因此我們應最小化同步塊的長度。 在我們的咖啡店示例中,我們可以省去長時間運行的過程。 在本節的示例中,我們將使用同步塊:
在SynchronizedBlockCoffeeStore中 ,我們修改buyCoffee方法,以將長時間運行的進程排除在同步塊之外:
public void buyCoffee(String client) throws InterruptedException {someLongRunningProcess();synchronized(this) {lastClient = client;soldCoffees++;System.out.println(client + " bought some coffee");} }public synchronized int countSoldCoffees() {return soldCoffees;}public synchronized String getLastClient() {return lastClient;}在上一個同步塊中,我們將“ this”用作其鎖。 它與同步實例方法中的鎖相同。 當心使用另一個鎖,因為我們正在此類的其他方法( countSoldCoffees和getLastClient )中使用此鎖。
讓我們看看執行修改后的程序的結果:
Mike bought some coffee John bought some coffee Anna bought some coffee Steve bought some coffee Sold coffee: 4 Last client: Steve Total time: 3015 ms在保持代碼同步的同時,我們大大減少了程序的時間。
6.使用私人鎖
上一節對實例對象使用了鎖定,但是您可以將任何對象用作其鎖定。 在本節中,我們將使用私人鎖,看看使用私人鎖會有什么風險。
在PrivateLockExample中 ,我們有一個由私有鎖(myLock)保護的同步塊:
public class PrivateLockExample {private Object myLock = new Object();public void executeTask() throws InterruptedException {synchronized(myLock) {System.out.println("executeTask - Entering...");Thread.sleep(3000);System.out.println("executeTask - Exiting...");}} }如果一個線程進入executeTask方法將獲取myLock鎖。 在由相同的myLock鎖保護的此類中進入其他方法的任何其他線程都必須等待才能獲取它。
但是現在,讓我們想象一下,有人想要擴展此類以添加自己的方法,并且由于需要使用相同的共享數據,因此這些方法也需要同步。 由于該鎖在基類中是私有的,因此擴展類將無法訪問它。 如果擴展類同步其方法,則將通過“ this”進行保護。 換句話說,它將使用另一個鎖。
MyPrivateLockExample擴展了上一個類,并添加了自己的同步方法executeAnotherTask :
public class MyPrivateLockExample extends PrivateLockExample {public synchronized void executeAnotherTask() throws InterruptedException {System.out.println("executeAnotherTask - Entering...");Thread.sleep(3000);System.out.println("executeAnotherTask - Exiting...");}public static void main(String[] args) {MyPrivateLockExample privateLock = new MyPrivateLockExample();Thread t1 = new Thread(new Worker1(privateLock));Thread t2 = new Thread(new Worker2(privateLock));t1.start();t2.start();}private static class Worker1 implements Runnable {private final MyPrivateLockExample privateLock;public Worker1(MyPrivateLockExample privateLock) {this.privateLock = privateLock;}@Overridepublic void run() {try {privateLock.executeTask();} catch (InterruptedException e) {e.printStackTrace();}}}private static class Worker2 implements Runnable {private final MyPrivateLockExample privateLock;public Worker2(MyPrivateLockExample privateLock) {this.privateLock = privateLock;}@Overridepublic void run() {try {privateLock.executeAnotherTask();} catch (InterruptedException e) {e.printStackTrace();}}} }該程序使用兩個工作線程,分別執行executeTask和executeAnotherTask 。 輸出顯示線程如何交錯,因為它們沒有使用相同的鎖:
executeTask - Entering... executeAnotherTask - Entering... executeAnotherTask - Exiting... executeTask - Exiting...7.結論
我們已經使用Java的內置鎖定機制回顧了內部鎖定的使用。 這里的主要關注點是需要使用共享數據的同步塊。 必須使用相同的鎖。
這篇文章是Java Concurrency Tutorial系列的一部分。 檢查此處以閱讀本教程的其余部分。
- 您可以在Github上找到源代碼。
翻譯自: https://www.javacodegeeks.com/2014/09/java-concurrency-tutorial-locking-intrinsic-locks.html
總結
以上是生活随笔為你收集整理的Java并发教程–锁定:内在锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 乘联会崔东树:今年 92 款新车中燃油车
- 下一篇: 飞利浦 49M2C8900 显示器上架: