一個計數器
對於普通的變量,在涉及多線程操作時,會遇到經典的線程安全問題。考慮如下代碼:
private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);
Thread[] threads = new Thread[TEST_THREAD_COUNT];
for (int i = 0; i < TEST_THREAD_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
++counter;
System.out.println("Thread " + Thread.currentThread().getId() + " / Counter : " + counter);
latch.countDown();
}
});
threads[i].start();
}
try {
latch.await();
System.out.println("Main Thread " + " / Counter : " + counter);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
多次執行這段程序,我們會發現最後counter的值會出現98,99等值,而不是預想中的100。
...
...
Thread 100 / Counter : 90
Thread 101 / Counter : 91
Thread 102 / Counter : 92
Thread 103 / Counter : 93
Thread 104 / Counter : 95
Thread 105 / Counter : 95
Thread 106 / Counter : 96
Thread 107 / Counter : 97
Thread 108 / Counter : 98
Thread 109 / Counter : 99
Main Thread / Counter : 99
這個問題發生的原因是++counter不是一個原子性操作。當要對一個變量進行計算的時候,CPU需要先從內存中將該變量的值讀取到高速緩存中,再去計算,計算完畢後再將變量同步到主內存中。這在多線程環境中就會遇到問題,試想一下,線程A從主內存中複製了一個變量a=3到工作內存,並且對變量a進行了加一操作,a變成了4,此時線程B也從主內存中複製該變量到它自己的工作內存,它得到的a的值還是3,a的值不一致了(這裏工作內存就是高速緩存)。
同步
java有個sychronized關鍵字,它能後保證同一個時刻只有一條線程能夠執行被關鍵字修飾的代碼,其他線程就會在隊列中進行等待,等待這條線程執行完畢後,下一條線程才能對執行這段代碼。
它的修飾對象有以下幾種:
- 修飾一個代碼塊,被修飾的代碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象;
- 修飾一個方法,被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的對象是調用這個方法的對象;
- 修飾一個靜態的方法,其作用的範圍是整個靜態方法,作用的對象是這個類的所有對象;
- 修飾一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的對象是這個類的所有對象。
現在我們開始使用我們的新知識,調整以上代碼,在run()上添加sychronized關鍵字。
private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);
Thread[] threads = new Thread[TEST_THREAD_COUNT];
for (int i = 0; i < TEST_THREAD_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public synchronized void run() {
++counter;
System.out.println("Thread " + Thread.currentThread().getId() + " / Counter : " + counter);
latch.countDown();
}
});
threads[i].start();
}
try {
latch.await();
System.out.println("Main Thread " + " / Counter : " + counter);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
多次執行新代碼,我們依舊發現結果不正確:
...
...
Thread 98 / Counter : 87
Thread 97 / Counter : 86
Thread 99 / Counter : 89
Thread 100 / Counter : 89
Thread 101 / Counter : 90
Thread 102 / Counter : 91
Thread 104 / Counter : 95
Thread 108 / Counter : 97
Thread 106 / Counter : 96
Thread 105 / Counter : 95
Thread 103 / Counter : 95
Thread 109 / Counter : 98
Thread 107 / Counter : 97
Main Thread / Counter : 98
這裏的原因在於synchronized是鎖定當前實例對象的代碼塊。也就是當多條線程操作同一個實例對象的同步方法是時,只有一條線程可以訪問,其他線程都需要等待。這裏Runnable實例有多個,所以鎖就不起作用。
我們繼續修改代碼,使得Runnable實例只有一個:
private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;
private final static CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);
static class MyRunnable implements Runnable {
@Override
public synchronized void run() {
++counter;
System.out.println("Thread " + Thread.currentThread().getId() + " / Counter : " + counter);
latch.countDown();
}
}
public static void main(String[] args) {
Thread[] threads = new Thread[TEST_THREAD_COUNT];
MyRunnable myRun = new MyRunnable();
for (int i = 0; i < TEST_THREAD_COUNT; i++) {
threads[i] = new Thread(myRun);
threads[i].start();
}
try {
latch.await();
System.out.println("Main Thread " + " / Counter : " + counter);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
現在我們發現多次執行代碼後,最後結果都是100。
我們可以給counter變量添加volatile關鍵字(這裏它對於結果沒有影響)。
當一個變量被定義為volatile之後,它對所有的線程就具有了可見性,也就是説當一個線程修改了該變量的值,所有的其它線程都可以立即知道。通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
sychronized的優缺點
synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生。另外在資源競爭不是很激烈的情況下,偶爾會有同步的情形下,synchronized是很合適的。原因在於,編譯程序通常會盡可能的進行優化synchronized,另外可讀性非常好,不管用沒用過5.0多線程包的程序員都能理解。但是當同步競爭非常激烈的時候,synchronized的性能一下子會下降幾十倍。還有一個最大的問題就是多線程競爭一個鎖時,其餘未得到鎖的線程只能不停的嘗試獲得鎖,而不能中斷。這種情況下就會造成大量的競爭線程性能的下降。
Atomic
針對synchronized的一系列缺點,JDK5提供了Lock類,目的是為同步機制進行改善。Lock和synchronized有一點非常大的不同,採用synchronized不需要用户手動的去釋放鎖,當synchronized方法或者代碼塊執行完畢之後,系統會自動的讓線程釋放對鎖的佔有,而Lock則必須要用户去手動釋放鎖,如果沒有主動的釋放鎖,就會可能導致出現死鎖的現象。不過這篇文章這裏不討論Lock類。
在Java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本數據類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數),減法操作(減一個數)進行了封裝,保證這些操作是原子性操作。
我們這裏使用AtomicInteger類
private static final int TEST_THREAD_COUNT = 100;
private static AtomicInteger at = new AtomicInteger(0);
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);
Thread[] threads = new Thread[TEST_THREAD_COUNT];
for (int i = 0; i < TEST_THREAD_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
int value = at.incrementAndGet();
System.out.println("Thread " + Thread.currentThread().getId() + " / Counter : " + value);
latch.countDown();
}
});
threads[i].start();
}
try {
latch.await();
System.out.println("Main Thread " + " / Counter : " + at.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}