在java多線程程式設計中,我們經常會遇見這樣的一種情況,在我們輸入相同的資料,有時候輸出的資料是正确,有時候輸出的結果卻是錯誤的,這種計算結果的正确性與時間有關的現象就被稱之為競态。
1.競态産生的原因
在多線程程式設計時會出現多個線程共同通路一個變量,這樣的變量即我們所說的共享變量或者共享資源;競态産生的條件之一就是在于多個線程同時通路相同的變量并進行讀寫操作,當其中一個線程需要根據某個變量的狀态來相應執行某個操作的之前,該變量很可能已經被其它線程修改,這個時候就産生的競态。下邊我們看個例子
public class Statistics {
private int count = 0;
private void add(int i){
count = count + i;
System.out.println("線程"+Thread.currentThread().getId()+":"+count);
}
public static class StatisticsRunnable implements Runnable{
private Statistics statistics;
public StatisticsRunnable( Statistics statistics){
this.statistics = statistics;
}
@Override
public void run() {
for(int i=0;i<10;i++){
this.statistics.add(1);
}
}
}
public static void main(String[] args) {
Statistics statistics = new Statistics();
Thread t1 = new Thread(new StatisticsRunnable(statistics));
Thread t2 = new Thread(new StatisticsRunnable(statistics));
t1.start();
t2.start();
}
}
運作結果:
線程12:2
線程11:2
線程12:3
線程11:4
線程12:5
線程11:6
線程12:7
線程11:8
線程11:10
線程12:9
線程11:11
線程12:12
線程11:13
線程12:14
線程11:15
線程12:16
線程11:17
線程12:18
線程11:19
線程12:20
按照正常邏輯,我們的統計類一直在增量加1,每個線程所列印的count是不會一樣的,但是結果确并不是這樣,出現了有時候結果是我們所想的,有時候卻出現了兩個線程列印相同的結果。這就是我所謂的競态了,導緻競态的常見因素是多個線程在沒有采取任何控制措施的情況下并發的更新、讀取同一個共享變量。我反過來看例子,線上程11加完1之後,準備列印count,這個時候線程12并發的更新了count,導緻線程實際列印的是線程12已經加1之後的count,出現了和想象不一樣的結果,當我們加大循環次數,再多次運作上面的例子,我們還能發現最終的統計結果和我們想象有一定的差距,這種情況是兩個線程交錯的更新count,會出現一個線程的結果重新覆寫已經加1的count,這樣就少統計了。是以競态往往伴随着讀取髒資料的問題,即一個線程讀取到一個過時的資料,丢失更新問題,即一個線程所做的更新沒有展現在後續其他線程對該資料的讀取上。
需要注意的競态不一定就導緻計算結果的不正确,它隻是不排除計算結果有時候正确,有時候錯誤,這也是我們多線程程式設計需要非常注意的,可能我們自測時并發量小,出現的結果都是正确的,一旦到線上高并發的情況時就出現了錯誤的結果,是以當多線程程式設計時如果出現共享變量一定需要注意是否會出現競态。
2.競态的模式
在競态的典型案例中,常常有兩個競态模式:read-modify-write(讀-改-寫)和check-then-act(檢測後行動)。read-modify-write(讀-改-寫)這個操作分為這幾個步驟:一個線程讀取了一個共享變量的值,然後根據這個值做一下計算,最後在更新該共享變量的值,在這個操作中如果我們沒有采取任何控制措施,那麼就可能出現競态;一起分析一下這個過程,我們在讀取一個值後,剛準備用這個值,但是這個時候這個值被别的線程改變了,同樣的我們計算出結果需要更新這個結果,但是這個時候出現直接覆寫了别的線程更新過的值,這個過程出現了“髒讀”和“更新丢失”的問題。check-then-act(檢測後行動)這個操作的步驟:讀取某個共享變量的值,根據該變量的值決定下一步的動作是什麼時,在這個過程中,我們決定下一步準備怎麼做的時候可能這個共享變量的值被别的線程更新了,出現了下一步的操作就不是我們想要的操作了,這樣也出現了競态。多線程程式設計中可以套用這兩種模式,在使用一個共享變量時是否會出現以上兩種模式的情況,就可以分析出是否會出現競态。
2.競态的解決方法
競态的産生往往是操作共享變量産生的,是以當多個線程需要操作共享資源的時候,它們需要以某種順序來確定該共享變量在某一時刻隻能被一個線程使用,也就是說,當線程A需要使用共享變量,如果該共享變量正被線程B使用,同步機制就會讓線程A一直等待下去,直到線程B結束對該共享變量的使用,線程A才能使用。比如我們在add的方法上加上
synchronized
關鍵字之後在運作則結果一直正确的。 synchronized關鍵字會使其修改的方法在任時刻隻能被一個線程執行,該方法涉及的共享變量在任意時刻隻能有一個線程通路(讀,寫),進而避免了這個方法交錯執行的而導緻的幹擾,這樣就消除了競态。
public class Statistics {
private int count = 0;
private synchronized void add(int i){
count = count + i;
System.out.println("線程"+Thread.currentThread().getId()+":"+count);
}
public static class StatisticsRunnable implements Runnable{
private Statistics statistics;
public StatisticsRunnable( Statistics statistics){
this.statistics = statistics;
}
@Override
public void run() {
for(int i=0;i<100;i++){
this.statistics.add(1);
}
}
}
public static void main(String[] args) {
Statistics statistics = new Statistics();
Thread t1 = new Thread(new StatisticsRunnable(statistics));
Thread t2 = new Thread(new StatisticsRunnable(statistics));
t1.start();
t2.start();
}
}
還有一種解決競态的方法在多個線程通路共享變量時對共享變量的操作加鎖,使共享變量在任意時刻隻能有一個線程通路(讀,寫),也就避免了這個方法交錯執行的而導緻的幹擾。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Statistics {
private Lock lock = new ReentrantLock();
private int count = 0;
private synchronized void add(int i){
try{
lock.lock();
count = count + i;
System.out.println("線程"+Thread.currentThread().getId()+":"+count);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static class StatisticsRunnable implements Runnable{
private Statistics statistics;
public StatisticsRunnable( Statistics statistics){
this.statistics = statistics;
}
@Override
public void run() {
for(int i=0;i<100;i++){
this.statistics.add(1);
}
}
}
public static void main(String[] args) {
Statistics statistics = new Statistics();
Thread t1 = new Thread(new StatisticsRunnable(statistics));
Thread t2 = new Thread(new StatisticsRunnable(statistics));
t1.start();
t2.start();
}
}
當然鎖是一種重量級的操作,我們使用鎖需要注意很過地方,這個些在接下來的文章中我都會講到了,從這個兩個例子我們可以看出避免競态的實質就是,使共享變量在任意時刻隻能有一個線程通路(讀,寫),滿足我們上一篇文章的多線程式設計的原子性、可見性、有序性,就可以很好的避免競态了