Java并行程序

并行程序基礎

本文為《實戰java高并發程序設計》電子筆記,供個人查閱及裝逼,不具有參考性。
https://legacy.gitbook.com/book/jiapengcai/effective-java/details

1.2 幾個重要的概念

  • 并發偏重于多個任務交替執行,而多個任務之間有可能還是串行的。而并行是真正意義上的“同時執行
  • 臨界區
  • 阻塞(Blocking)和非阻塞(Non-Blocking)
    阻塞:一個線程占用了臨界區的資源,其他所有需要這個資源的線程就必須在這個臨界區中進行等待。等待會導致線程掛起,這種情況就是阻塞。
    非阻塞:沒有一個線程可以妨礙其他線程執行。所有的線程都會嘗試不斷前向執行
  • 死鎖(Deadlock)、饑餓(Starvation)和活鎖(Livelock)

1.3 并發級別

由于臨界區的存在,多線程之間的并發必須受到控制。根據控制并發的策略,我們可以把并發的級別進行分類,大致上可以分為阻塞、無饑餓、無障礙、無鎖、無等待幾種。

  • 阻塞(Blocking)
    在其他線程釋放資源之前,當前線程無法繼續執行。當我們使用synchronized關鍵字,或者重入鎖時,我們得到的就是阻塞的線程。

無論是synchronized或者重入鎖,都會試圖在執行后續代碼前,得到臨界區的鎖,如果得不到,線程就會被掛起等待,直到占有了所需資源為止。

  • 無饑餓(Starvation-Free)
    不同線程的優先級相等
  • 無障礙(Obstruction-Free)
    一種最弱的非阻塞調度。兩個線程無障礙運行,他們不會因為臨界區的問題導致一方掛起。但數據出現異常,則立即回滾自己的修改,確保數據正確。
  • 無鎖(Lock-Free)
    無鎖的并行都是無障礙的。在無鎖的情況下,所有的線程都能嘗試對臨界區進行訪問,但不同的是,無鎖的并發保證必然有一個線程能夠在有限步內完成操作離開臨界區。
  • 無等待(Wait-Free)
    無鎖只要求有一個線程可以在有限步內完成操作,而無等待則在無鎖的基礎上更進一步進行擴展。它要求所有的線程都必須在有限步內完成,這樣就不會引起饑餓問題。

一種典型的無等待結構就是RCU(Read-Copy-Update)。它的基本思想是,對數據的讀可以不加控制。因此,所有的讀線程都是無等待的,它們既不會被鎖定等待也不會引起任何沖突。但在寫數據的時候,先取得原始數據的副本,接著只修改副本數據(這就是為什么讀可以不加控制),修改完成后,在合適的時機回寫數據。

1.4關于并行的兩個重要定律

1.5 JMM

我們需要在深入了解并行機制的前提下,再定義一種規則,保證多個線程間可以有效地、正確地協同工作。而JMM也就是為此而生的。
JMM的關鍵技術點都是圍繞著多線程的原子性、可見性和有序性來建立的

  • 原子性(Atomicity)
    原子性是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。
  • 可見性(Visibility)
    出現一個線程的修改不會立即被其他線程察覺的情況

2.2 線程的基本操作

1.新建線程
通過繼承Thread類或實現Runnable接口

start()方法就新建一個線程并讓這個線程執行run()方法

2.停止線程
一般無需手動關閉線程

Thread.stop()方法在結束線程時,會直接終止線程,并且會立即釋放這個線程所持有的鎖。而這些鎖恰恰是用來維持對象一致性的。故stop方法不使用

3.線程中斷

public void Thread.interrupt()               // 中斷線程
public boolean Thread.isInterrupted()        // 判斷是否被中斷
public static boolean Thread.interrupted()   // 判斷是否被中斷,并清除當前中斷狀態
  • Thead.sleep()方法:讓當前線程休眠若干時間

Thread.sleep()方法會讓當前線程休眠若干時間,它會拋出一個InterruptedException中斷異常。InterruptedException不是運行時異常,也就是說程序必須捕獲并且處理它,當線程在sleep()休眠時,如果被中斷,這個異常就會產生

01 public static void main(String[] args) throws InterruptedException {
02     Thread t1=new Thread(){
03         @Override
04         public void run(){
05             while(true){
06                 if(Thread.currentThread().isInterrupted()){
07                     System.out.println("Interruted!");
08                     break;
09                 }
10                 try {
11                     Thread.sleep(2000);
12                 } catch (InterruptedException e) {
13                     System.out.println("Interruted When Sleep");
14                     //設置中斷狀態
15                     Thread.currentThread().interrupt();
16                 }
17                 Thread.yield();
18             }
19         }
20     };
21     t1.start();
22     Thread.sleep(2000);
23     t1.interrupt();
24 }

Thread.sleep()方法由于中斷而拋出異常,此時,它會清除中斷標記,如果不加處理,那么在下一次循環開始時,就無法捕獲這個中斷,故在異常處理中,再次設置中斷標記位。

4.等待(wait)和通知(notify)
這兩個方法并不是在Thread類中的,而是輸出Object類。這也意味著任何對象都可以調用這兩個方法。

線程A中,調用了obj.wait()方法,那么線程A就會停止繼續執行,而轉為等待狀態。線程A會一直等到其他線程調用了obj.notify()方法為止。這時,obj對象就儼然成為多個線程之間的有效通信手段。

image.png

注意:
必須在包含synchronzied的語句中才可以調用 wait方法

image.png

6.等待線程結束(join)和謙讓(yield)

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

第一個join()方法表示無限等待,它會一直阻塞當前線程,直到目標線程執行完畢。第二個方法給出了一個最大等待時間,超過最大時間線程就繼續執行。

public volatile static int i=0;
    public static class AddThread extends Thread{
        @Override
        public void run() {
            for(i=0;i<10000000;i++);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AddThread at=new AddThread();
        at.start();
        at.join();
        //主線程等待子線程執行后再執行
        System.out.println(i);
    }

join()的本質是讓調用線程wait()在當前線程對象實例

public static native void yield();
//讓出當前占用cpu,但接下來會繼續參與搶奪

2.3volatile與Java內存模型(JMM)

當你用volatile去申明一個變量時,就等于告訴了虛擬機,這個變量極有可能會被某些程序或者線程修改。為了確保這個變量被修改后,應用程序范圍內的所有線程都能夠“看到”這個改動,虛擬機就必須采用一些特殊的手段,保證這個變量的可見性等特點。

  • volatile并不能代替鎖,它也無法保證一些復合操作的原子性。比如下面的例子,通過volatile是無法保證i++的原子性操作的:
01 static volatile int i=0;
02 public static class PlusTask implements Runnable{
03     @Override
04     public void run() {
05         for(int k=0;k<10000;k++)
06             i++;
07     }
08 }
09
10 public static void main(String[] args) throws InterruptedException {
11     Thread[] threads=new Thread[10];
12     for(int i=0;i<10;i++){
13         threads[i]=new Thread(new PlusTask());
14         threads[i].start();
15     }
16     for(int i=0;i<10;i++){
17         threads[i].join();
18     }
19
20     System.out.println(i);
21    }

執行上述代碼,如果第6行i++是原子性的,那么最終的值應該是100000(10個線程各累加10000次)。但實際上,上述代碼的輸出總是會小于100000。

  • volatile能保證數據的可見性和有序性

2.6 線程優先級

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

2.7 線程安全的概念與synchronized

volatile并不能真正的保證線程安全。它只能確保一個線程修改了數據后,其他線程能夠看到這個改動。但當兩個線程同時修改某一個數據時,卻依然會產生沖突

01 public class AccountingVol implements Runnable{
02     static AccountingVol instance=new AccountingVol();
03     static volatile int i=0;
04     public static void increase(){
05         i++;
06     }
07     @Override
08     public void run() {
09         for(int j=0;j<10000000;j++){
10             increase();
11         }
12     }
13     public static void main(String[] args) throws InterruptedException {
14         Thread t1=new Thread(instance);
15         Thread t2=new Thread(instance);
16         t1.start();t2.start();
17         t1.join();t2.join();
18         System.out.println(i);
19     }
20 }

很多時候,i的最終值會小于20000000。這就是因為兩個線程同時對i進行寫入時,其中一個線程的結果會覆蓋另外一個(雖然這個時候i被聲明為volatile變量)。線程1和線程2同時讀取i為0,并各自計算得到i=1,并先后寫入這個結果,因此,雖然i++被執行了2次,但是實際i的值只增加了1。

要從根本上解決這個問題,我們就必須保證多個線程在對i進行操作時完全同步,使用關鍵字synchronized來實現這個功能

關鍵字synchronized的作用是實現線程間的同步。它的工作是對同步的代碼加鎖,使得每一次,只能有一個線程進入同步塊,從而保證線程間的安全性

關鍵字synchronized可以有多種用法。這里做一個簡單的整理。

  • 指定加鎖對象:對給定對象加鎖,進入同步代碼前要獲得給定對象的鎖。
  • 直接作用于實例方法:相當于對當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。
  • 直接作用于靜態方法:相當于對當前類加鎖,進入同步代碼前要獲得當前類的鎖。
01 public class AccountingSync2 implements Runnable{
02     static AccountingSync2 instance=new AccountingSync2();
03     static int i=0;
04     public synchronized void increase(){
05         i++;
06     }
07     @Override
08     public void run() {
09         for(int j=0;j<10000000;j++){
10             increase();
11         }
12     }
13     public static void main(String[] args) throws InterruptedException {
14         Thread t1=new Thread(instance);
15         Thread t2=new Thread(instance);
16         t1.start();t2.start();
17         t1.join();t2.join();
18         System.out.println(i);
19     }
20 }

這兩個線程都指向同一個Runnable接口實例(instance對象),這樣才能保證兩個線程在工作時,能夠關注到同一個對象鎖上去,從而保證線程安全。
一個錯誤的示例

01 public class AccountingSyncBad implements Runnable{
02     static int i=0;
03     public synchronized void increase(){
04         i++;
05     }
06     @Override
07     public void run() {
08         for(int j=0;j<10000000;j++){
09             increase();
10         }
11     }
12     public static void main(String[] args) throws InterruptedException {
13         Thread t1=new Thread(new AccountingSyncBad());
14         Thread t2=new Thread(new AccountingSyncBad());
15         t1.start();t2.start();
16         t1.join();t2.join();
17         System.out.println(i);
18     }
19 }

但我們只要簡單地修改上述代碼,就能使其正確執行。那就是使用synchronized的第三種用法,將其作用于靜態方法。將increase()方法修改如下:
這樣,即使兩個線程指向不同的Runnable對象,但由于方法塊需要請求的是當前類的鎖,而非當前實例,因此,線程間還是可以正確同步。

public static synchronized void increase(){
    i++;
}

被synchronized限制的多個線程是串行執行的

2.8 程序中的幽靈:隱蔽的錯誤

  • 并發下的ArrayList

注意:改進的方法很簡單,使用線程安全的Vector代替ArrayList即可。

  • 并發下詭異的HashMap

最簡單的解決方案就是使用ConcurrentHashMap代替HashMap。

  • 錯誤的加鎖
    比如加在不可變的int上

JDK并發包

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容