一個計數器
對於普通的變量,在涉及多線程操作時,會遇到經典的線程安全問題。考慮如下代碼:
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