在Java
併發系列的文章中,這個是第二篇文章。在前面的一篇文章中,咱們學習了Java
中的Executor
池和Excutors
的各類類別。java
在這篇文章中,咱們會學習synchronized
關鍵字以及咱們在多線程的環境中如何使用。git
在一個多線程的環境中,多個線程同時訪問相同的資源的狀況是存在的。例如,兩個線程試圖寫入同一個文本文件。它們之間沒有任何的同步,當兩個或多個線程對同一文件具備寫訪問權時,寫入該文件的數據可能會損壞。
同理,在JVM
中,每一個線程在各自的棧上存儲了一份變量的副本。某些其餘線程可能會更改這些變量的實際值。可是更改後的值可能不會刷新到其餘線程的本地副本中。
這可能致使程序執行錯誤和非肯定性行爲。github
爲了不這種問題,Java
給咱們提供了synchronized
這有助於實現線程之間的通訊,使得只有一個線程訪問同步資源,而其餘線程等待資源變爲空閒。多線程
synchronized
關鍵字能夠被用在下面一些不一樣的方式中,好比一個同步塊:併發
synchronized(someobject){ //thread-safe code here }
對方法進行同步:ide
public synchronized void someMethod(){ //thread-safe code here }
當一個線程試圖進入一個同步塊或者同步方法中的時候,它必須先得到一個同步對象上的鎖。一次只能夠有一個線程獲取鎖,而且執行塊中的代碼。學習
若是其餘線程嘗試訪問該同步塊,則必須等待,直到當前線程執行完同步塊的代碼。當前線程退出後,鎖將被自動釋放,其它線程能夠獲取鎖並進入同步代碼塊。測試
synchronized
塊來講,在synchronized
關鍵字後的括號中指定的對象上獲取鎖;synchronized static
方法,鎖是在.class對象上獲取的;synchronized
實例方法來講,鎖定是在該類的當前實例上得到的,即該實例(this
);定義同步方法就像在返回類型以前簡單地包含關鍵字同樣簡單。咱們定義一個順序打印數字1-5之間的方法。會有兩個線程來訪問這個方法,因此讓咱們來看看在沒有使用synchronized
關鍵字它們的運行狀況, 和咱們使用關鍵字來鎖住共享對象會發生什麼:this
public class NonSynchronizedMethod { public void printNumbers() { System.out.println("Starting to print Numbers for " + Thread.currentThread().getName()); for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } System.out.println("Completed printing Numbers for " + Thread.currentThread().getName()); } }
如今,讓咱們實現兩個訪問該對象並但願運行 printNumbers()
方法的自定義線程:spa
class ThreadOne extends Thread { NonSynchronizedMethod nonSynchronizedMethod; public ThreadOne(NonSynchronizedMethod nonSynchronizedMethod) { this.nonSynchronizedMethod = nonSynchronizedMethod; } @Override public void run() { nonSynchronizedMethod.printNumbers(); } } class ThreadTwo extends Thread { NonSynchronizedMethod nonSynchronizedMethod; public ThreadTwo(NonSynchronizedMethod nonSynchronizedMethod) { this.nonSynchronizedMethod = nonSynchronizedMethod; } @Override public void run() { nonSynchronizedMethod.printNumbers(); } }
這些線程共享一個相同的對象NonSynchronizedMethod
,它們會在這個對象上同時去調用非同步的方法printNumbers()
。
爲了測試這個,寫一個main
方法來作測試:
public class TestSynchronization { public static void main(String[] args) { NonSynchronizedMethod nonSynchronizedMethod = new NonSynchronizedMethod(); ThreadOne threadOne = new ThreadOne(nonSynchronizedMethod); threadOne.setName("ThreadOne"); ThreadTwo threadTwo = new ThreadTwo(nonSynchronizedMethod); threadTwo.setName("ThreadTwo"); threadOne.start(); threadTwo.start(); } }
運行上面的代碼,咱們會獲得下面的結果:
Starting to print Numbers for ThreadOne Starting to print Numbers for ThreadTwo ThreadTwo 0 ThreadTwo 1 ThreadTwo 2 ThreadTwo 3 ThreadTwo 4 Completed printing Numbers for ThreadTwo ThreadOne 0 ThreadOne 1 ThreadOne 2 ThreadOne 3 ThreadOne 4 Completed printing Numbers for ThreadOne
雖然ThreadOne
先開始執行的,可是ThreadTwo
先結束的。
當咱們再次運行上面的程序的時候,咱們會獲得一個不一樣的結果:
Starting to print Numbers for ThreadOne Starting to print Numbers for ThreadTwo ThreadOne 0 ThreadTwo 0 ThreadOne 1 ThreadTwo 1 ThreadOne 2 ThreadTwo 2 ThreadOne 3 ThreadOne 4 ThreadTwo 3 Completed printing Numbers for ThreadOne ThreadTwo 4 Completed printing Numbers for ThreadTwo
這些輸出徹底是偶然的,徹底不可預測。每次運行都會給咱們一個不一樣的輸出。由於能夠有更多的線程,咱們可能會遇到問題。在實際場景中,在訪問某種類型的共享資源(如文件或其餘類型的IO)時,這一點尤其重要,而不是僅僅打印到控制檯。
下面咱們採用同步的方法,使用synchronized
關鍵字:
public synchronized void printNumbers() { System.out.println("Starting to print Numbers for " + Thread.currentThread().getName()); for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } System.out.println("Completed printing Numbers for " + Thread.currentThread().getName()); }
代碼中只是給方法添加了一個synchronized
關鍵字,沒有其它的改動。如今咱們運行上面的代碼,獲得以下所示的結果:
Starting to print Numbers for ThreadOne ThreadOne 0 ThreadOne 1 ThreadOne 2 ThreadOne 3 ThreadOne 4 Completed printing Numbers for ThreadOne Starting to print Numbers for ThreadTwo ThreadTwo 0 ThreadTwo 1 ThreadTwo 2 ThreadTwo 3 ThreadTwo 4 Completed printing Numbers for ThreadTwo
在這裏,咱們看到即便兩個線程同時運行,只有一個線程一次進入synchronized
方法,在這種狀況下是ThreadOne
。一旦完成執行,ThreadTwo
就能夠執行printNumbers()
方法
多線程的主要目的是儘量並行地執行任意數量的任務。可是,同步限制了必須執行同步方法或塊的線程的並行性。
可是,咱們能夠嘗試經過在同步範圍內保留儘量少的代碼來減小以同步方式執行的代碼量。可能有許多場景,不是在整個方法上同步,而是能夠在方法中同步幾行代碼。
咱們可使用synchronized
塊來包含代碼的那部分而不是整個方法。也就是說對於須要同步的代碼塊進行同步,而不是對整個方法進行同步。
因爲在同步塊內部執行的代碼量較少,所以每一個線程都會更快地釋放鎖定。結果,其餘線程花費更少的時間等待鎖定而且代碼吞吐量大大增長。
讓咱們修改前面的例子,只同步for循環打印數字序列,實際上,它是咱們示例中應該同步的惟一代碼部分:
public class SynchronizedBlockExample { public void printNumbers() { System.out.println("Starting to print Numbers for " + Thread.currentThread().getName()); synchronized (this) { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } System.out.println("Completed printing Numbers for " + Thread.currentThread().getName()); } }
運行結果:
Starting to print Numbers for ThreadOne Starting to print Numbers for ThreadTwo ThreadOne 0 ThreadOne 1 ThreadOne 2 ThreadOne 3 ThreadOne 4 Completed printing Numbers for ThreadOne ThreadTwo 0 ThreadTwo 1 ThreadTwo 2 ThreadTwo 3 ThreadTwo 4 Completed printing Numbers for ThreadTwo
儘管ThreadTwo
在ThreadOne
完成其任務以前「開始」打印數字彷佛使人擔心,這只是由於咱們在中止ThreadTwo
鎖以前,容許線程經過System.out.println("Completed printing Numbers for " + Thread.currentThread().getName())
語句。
這很好,由於咱們只想同步每一個線程中的數字序列。咱們能夠清楚地看到兩個線程只是經過同步for
循環以正確的順序打印數字。
在這個例子中,咱們看到了如何在Java
中使用synchronized
關鍵字來實現多個線程之間的同步。咱們還經過例子瞭解了什麼時候可使用synchronized
方法和塊。
做者:Chandan Singh
譯者:lee