介紹
在Java
併發系列的文章中,這個是第二篇文章。在前面的一篇文章中,我們學習了Java
中的Executor
池和Excutors
的各種類別。
在這篇文章中,我們會學習synchronized
關鍵字以及我們在多線程的環境中如何使用。
synchronized關鍵字是什麼?
在一個多線程的環境中,多個線程同時訪問相同的資源的情況是存在的。例如,兩個線程試圖寫入同一個文本文件。它們之間沒有任何的同步,當兩個或多個線程對同一文件具有寫訪問權時,寫入該文件的數據可能會損壞。
同理,在JVM
中,每個線程在自己的棧上面都存儲了自己變量的一份複製。某些其他線程可能會更改這些變量的實際值。但是被更改後的值是不會被刷新到另外一個線程的本地複製中。
這可能導致程序執行錯誤和非確定性行爲。
爲了避免這種問題,Java
給我們提供了synchronized
關鍵字,其作用類似於對特定資源的鎖定。這個有助於實現線程之間的通信,只有一個線程可以訪問同步資源,而其它的線程都必須
等待直到可以訪問資源。
synchronized
關鍵字可以被用在下面一些不同的方式中,比如一個同步塊:
synchronized(someobject){
//thread-safe code here
}
對方法進行同步:
public synchronized void someMethod(){
//thread-safe code here
}
在JVM中synchronized是如何實現的
當一個線程試圖進入一個同步塊或者同步方法中的時候,它必須先獲得一個同步對象上的鎖。一次只可以有一個線程獲取鎖,並且執行塊中的代碼。
如果有一個線程要去訪問同步塊,那麼它必須等待,一直等待到當前線程執行完同步的代碼即可。噹噹前的線程執行完並且退出了同步塊,同步鎖也會自動的釋放掉。同時任何其他
正在等待的線程是可以獲取鎖,並且進入同步塊中。
- 對於一個
synchronized
塊來說,在synchronized
關鍵字後的括號中指定的對象上獲取鎖; - 對於一個同步的靜態方法,鎖定是在.class對象上獲取的;
- 對於同步實例方法來說,鎖定是在該類的當前實例上獲得的,即該實例;
同步方法
定義同步方法就像在返回類型之前簡單地包含關鍵字一樣簡單。讓我們定義一種方法,以順序方式打印1到5之間的數字。會有兩個線程來訪問這個方法,所以讓我們來看看在沒有
使用synchronized
關鍵字它們的運行情況和我們使用關鍵字來鎖住共享對象會發生什麼:
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()方法的自定義線程:
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
完成其任務之前“開始”打印數字似乎令人擔憂,但這只是因爲我們允許線程通過System.out.println(開始爲ThreadTwo打印Numbers)
語句,然後停止ThreadTwo
鎖。
這很好,因爲我們只想同步每個線程中的數字序列。我們可以清楚地看到兩個線程只是通過同步for
循環以正確的順序打印數字。
結論
在這個例子中,我們看到了如何在Java
中使用synchronized
關鍵字來實現多個線程之間的同步。我們還了解了何時可以使用synchronized
方法和塊來舉例說明。
說明
本譯文均採用java
語言編寫代碼。
原文:https://stackabuse.com/synchronized-keyword-in-java/
作者:Chandan Singh
譯者:lee