溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java中如何實現并發編程

發布時間:2021-06-17 14:20:18 來源:億速云 閱讀:264 作者:Leah 欄目:編程語言

本篇文章為大家展示了Java中如何實現并發編程,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

1. 進程

一個進程有其專屬的運行環境,一個進程通常有一套完整、私有的運行時資源;尤其是每個進程都有其專屬的內存空間。
通常情況下,進程等同于運行的程序或者應用,然而很多情況下用戶看到的一個應用實際上可能是多個進程協作的。為了達到進程通信的目的,主要的操作系統都實現了Inter Process Communication(IPC)資源,例如pipe和sockets,IPC不僅能支持同一個系統中的進程通信,還能支持跨系統進程通信。

2. 線程

線程通常也被叫做輕量級進程,進程線程都提供執行環境,但是創建一個線程需要的資源更少,線程在進程中,每個進程至少有一條線程,線程共享進程的資源,包括內存空間和文件資源,這種機制會使得處理更高效但是也存在很多問題。
多線程運行是Java的一個主要特性,每個應用至少包含一個線程或者更多。從應用程序角度來講,我們從一條叫做主線程的線程開始,主線程可以創建別的其他的線程。

線程生命周期

一個線程的生命周期包含了一下幾種狀態

1、新建狀態

該狀態線程已經被創建,但未進入運行狀態,我們可以通過start()方法來調用線程使其進入可執行狀態。

2、可執行狀態/就緒狀態

在該狀態下,線程在排隊等待任務調度器對其進行調度執行。

3、運行狀態

在該狀態下,線程獲得了CPU的使用權并在CPU中運行,在這種狀態下我們可以通過yield()方法來使得該線程讓出時間片給自己或者其他線程執行,若讓出了時間片,則進入就緒隊列等待調度。

4、阻塞狀態

在阻塞狀態下,線程不可運行,并且被異除出等待隊列,沒有機會進行CPU執行,在以下情況出現時線程會進入阻塞狀態

  • 調用suspend()方法

  • 調用sleep()方法

  • 調用wait()方法

  • 等待IO操作

線程可以從阻塞狀態重回就緒狀態等待調度,如IO操作完畢后。

5、終止狀態

當線程執行完畢或被終止執行后便會進入終止狀態,進入終止狀態后線程將無法再被調度執行,徹底喪失被調度的機會。

線程對象

每一條線程都有一個關聯的Thread對象,在并發編程中Java提供了兩個基本策略來使用線程對象

  • 直接控制線程的創建和管理,在需要創建異步任務時直接通過實例化Thread來創建和使用線程。

  • 或者將抽象好的任務傳遞給一個任務執行器 executor

1. 定義和開始一條線程

在創建一個線程實例時需要提供在線程中執行的代碼,有兩種方式可以實現。

提供一個Runnable對象,Runnable接口定義了一個run方法,我們將要在線程中執行的方法放到run方法內部,再將Runnable對象傳遞給一個Thread構造器,代碼如下。

public class ThreadObject {
  public static void main(String args[]) {
    new Thread(new HelloRunnable()).start();
  }
}
// 實現Runnable接口
class HelloRunnable implements Runnable {
  @Override
  public void run() {
    System.out.println("Say hello to world!!!");
  }
}

繼承Thread,Thread類自身實現了Runnable接口,但是其run方法什么都沒做,由我們自己根據需求去擴展。

public class ThreadObject {
  public static void main(String args[]) {
    new HelloThread().start();
  }
}
// 繼承Thread,擴展run方法
class HelloThread extends Thread {
  public void run() {
    System.out.println("Say hello to world!!!");
  }
}

兩種實現方式的選取根據業務場景和Java中單繼承,多實現的特性來綜合考量。

2. 利用Sleep暫停線程執行

sleep()方法會使線程進入阻塞隊列,進入阻塞隊列后,線程會將CPU時間片讓給其他線程執行,sleep()有兩個重載方法sleep(long millis)和sleep(long millis, int nanos)當到了指定的休眠時間后,線程將會重新進入就緒隊列等待調度管理器進行調度

public static void main(String args[]) throws InterruptedException {
  for (int i = 0; i < 4; i++) {
    System.out.println("print number "+ i);
    // 將主線程暫停4秒后執行,4秒后重新獲得調度執行的機會
    Thread.sleep(4*1000);
  }
}

3. 中斷

當一個線程被中斷后就代表這個線程再無法繼續執行,將放棄所有在執行的任務,程序可以自己決定如何處理中斷請求,但通常都是終止執行。

在Java中與中斷相關的有Thread.interrupt()、Thread.isInterrupted()、Thread.interrupted()三個方法

Thread.interrupt()為設置中斷的方法,該方法會將線程狀態設置為確認中斷狀態,但程序并不會立馬中斷執行只是設置了狀態,而Thread.isInterrupted()、Thread.interrupted()這兩個方法可以用于捕獲中斷狀態,區別在于Thread.interrupted()會重置中斷狀態。

4. Join

join方法允許一條線程等待另一條線程執行完畢,例如t是一條線程,若調用t.join()方法,則當前線程會等待t線程執行完畢后再執行。

線程同步 Synchronization

各線通信方式

  • 共享對象的訪問權限 如. A和B線程都有訪問和操作某一個對象的權限

  • 共享 對象的引用對象的訪問權限 如. A和B線程都能訪問C對象,C對象引用了D對象,則A和B能通過C訪問D對象

這種通信方式使得線程通訊變得高效,但是也帶來一些列的問題例如線程干擾和內存一致性錯誤。那些用于防止出現這些類型的錯誤出現的工具或者策略就叫做同步。

1. 線程干擾 Thread Interference

線程干擾是指多條線同時操作某一個引用對象時造成計算結果與預期不符,彼此之間相互干擾。如例

public class ThreadInterference{
  public static void main(String args[]) throws InterruptedException {
    Counter ctr = new Counter();
    // 累加線程
    Thread incrementThread = new Thread(()->{
      for(int i = 0; i<10000;i++) {
        ctr.increment();
      }
    }); 
    // 累減線程
    Thread decrementThread = new Thread(()->{
      for(int i = 0; i<10000;i++) {
        ctr.decrement();
      }
    }); 
    incrementThread.start();
    decrementThread.start();
    incrementThread.join();
    decrementThread.join();
    System.out.println(String.format("最終執行結果:%d", ctr.get()));
  }
}
class Counter{
  private int count = 0;
  // 自增
  public void increment() {
    ++this.count;
  }
  // 自減
  public void decrement() {
    --this.count;
  }
  public int get() {
    return this.count;
  }
}

理論上來講,如果按照正常的思路理解,一個累加10000次一個累減10000次最終結果應該是0 ,但實際結果卻是每次運行結果都不一致,產生這個結果的原因便是線程之間相互干擾。

我們可以把自增和自減操作拆解為以下幾個步驟

  • 獲取count變量當前值

  • 自增/自減 獲取到的值

  • 將結果保存回count變量

當多個線程同時對count進行操作時,便可能產生如下這一種狀態

  • 線程A : 獲取count

  • 線程B : 獲取count

  • 線程A: 自增,結果 為 1

  • 線程B: 自減,結果為 -1

  • 線程A: 將結果1 保存到count; 當前count = 1

  • 線程B: 將結果-1 保存到count; 當前count = -1

當線程以上面所示的順序執行時,線程B就會覆蓋掉線程A的結果,當然這只是其中一種情況。

2. 內存一致性錯誤 Memory Consistency Errors

當不同的線程對應相同數據具有不一致的視圖時,會發生內存一致性錯誤,詳細信息參見 JVM內存模型

3. 同步方法

Java提供了兩種同步的慣用方法:同步方法 synchronized methods 、同步語句 synchronized statements 。要使方法變成同步方法只需要在方法聲明時加入synchronized關鍵字,如

class Counter{
  private int count = 0;
  // 自增
  public synchronized void increment() {
    ++this.count;
  }
  // 自減
  public synchronized void decrement() {
    --this.count;
  }
  public synchronized int get() {
    return this.count;
  }
}

聲明為同步方法之后將會使得對象產生如下所述的影響

  • 首先,不可以在同一對象上多次調用同步方法來交錯執行,同步聲明使得同一個時間只能有一條線程調用該對象的同步方法,當一條線程已經在調用同步方法時,其他線程會被阻塞block,無法調用該對象的所有同步方法。

  • 其次,當同步方法調用結束時,會自動與同一對象的任何后續調用方法建立一個happens-before關聯,這保證對對象狀態的更改對所有線程可見。

4. 內部鎖和同步

同步是圍繞對象內部實體構建的,API規范通常將此類實體稱之為監視器,內部鎖有兩個至關重要的作用

  • 強制對對象狀態的獨占訪問

  • 建立至關重要的happens-before關系

每個對象都有與其關聯的固有鎖,通常,需要對對象的字段進行獨占且一致的訪問前需要獲取對象的內部鎖,然后再使用完成時釋放內部鎖,線程在獲取后釋放前擁有該對象的內部鎖。只要線程擁有了內部鎖其他任何線程都無法獲取相同的鎖,其他線程在嘗試獲取鎖時將被阻塞。在線程釋放內部鎖時,該操作將會在該對象的任何后續操作間建立happens-before關系。

4.1 同步方法中的鎖

當線程調用同步方法時,線程會自動獲得該方法所屬對象得內部鎖,并且在方法返回時自動釋放,即使返回是由未捕獲異常導致。靜態同步方法的鎖不同于實例方法的鎖,靜態方法是圍繞該類進行控制而非該類的某一個實例。

4.2 同步語句

另外一個提供同步的方法是同步代語句,與同步方法不同的是,同步語句必須指定一個對象來提供內部鎖。

public class IntrinsicLock {
  private List<String> nameList = new LinkedList<String>();
  private String lastName;
  private int nameCount;

  public void addName(String name) {
    // 當多條線程對同一個實例對象的addName()方法操作時將會是同步的,提供鎖的對象為該實例對象本身
    synchronized(this) {
      lastName = name;
      nameCount++;
    }
    nameList.add(name);
  }
}

同步語句對細粒度同步提高并發性也很有用,比如我們需要對同一個對象的不同屬性進行同步修改我們可以通過如下代碼來提高細粒度同步控制下的并發。

public class IntrinsicLock {
  // 1. 該屬性需要基于同步的修改
  private String lastName;
  // 1. 該屬性也需要基于同步的修改
  private int count;
  
  // 該對象用于對lastName提供內部鎖
  private Object nameLock = new Object();
  // 該對象用于對nameCount提供內部鎖
  private Object countLock = new Object();
  
  public void addName(String name) {
    synchronized(nameLock) {
      lastName = name;
    }
  }
  public void increment() {
    synchronized(countLock) {
      count++;
    }
  }
}

這樣,對lastName的操作不會阻塞count屬性的自增操作,因為他們分別使用了不同的對象來提供鎖。若像上一個例子中使用this來提供鎖的話,則在調用addName()方法時increment()也被阻塞,反之亦然,這樣將會增加不必要的阻塞。

4.3 可重入同步

線程無法獲取另外一個線程已經擁有的鎖,但是線程可以多次獲取它已經擁有的鎖,允許線程多次獲取同一鎖可以實現可重入的同步,即同步方法或者同步代碼塊中又調用了由同一個對象提供鎖的其他同步方法時,該鎖可以多次被獲取

public class IntrinsicLock {
  private int count;
  public void decrement(String name) {
    synchronized(this) {
      count--;
      // 調用其他由同一個對象提供鎖的同步方法時,鎖可以重復獲取
      // 但只能由當前有用鎖的線程重復獲取
      increment();
    }
  }
  public void increment() {
    synchronized(this) {
      count++;
    }
  }
}

4.4 原子訪問

在編程中,原子操作指的是指所有操作一行性完成,原子操作不可能執行一半,要么全都執行,要么都不執行。在原子操作完成之前,其修改都是不可見的。在Java中以下操作是原子性的。

  • 讀寫大部分原始變量(除了long和double)

  • 讀寫所有使用volatile聲明的變量

原子操作的特性使得我們不必擔心線程干擾帶來的同步問題,但是原子操作依然會發生內存一致性錯誤。需要使用volatile聲明變量以有效防止內存一致性錯誤,因為寫volatile標記的變量時會與讀取該變量的后續操作建立happens-before關系,所以改變使用volatile標記變量時對其他線程總是可見的。也就是它不僅可以觀測最新的改變,也能觀測到尚未使其改變的操作。

5. 死鎖

死鎖是描述一種兩條或多條線程相互等待(阻塞)的場景,如下例子所示

public class DeadLock {
  static class Friend {
    String name;
    public Friend(String name) {
      super();
      this.name = name;
    }
    public String getName() {
      return name;
    }
    public synchronized void call(Friend friend) {
      System.out.println(String.format("%s被%s呼叫...", name,friend.getName()));
      friend.callBack(this);
    }
    public synchronized void callBack(Friend friend) {
      System.out.println(String.format("%s呼叫%s...", friend.getName(),name));
    }
  }
  
  public static void main(String args[]) {
    final Friend zhangSan = new Friend("張三");
    final Friend liSi = new Friend("李四");
    new Thread(new Runnable() {
      public void run() { zhangSan.call(liSi); }
    }).start();
    new Thread(new Runnable() {
      public void run() { liSi.call(zhangSan); }
    }).start();
  }
}

如果張三呼叫李四的同時,李四呼叫張三,那么他們會永遠等待對方,線程永遠阻塞。

6. 饑餓和活鎖

相對死鎖而言,饑餓和活鎖問題要少得多,但是也應注意。

6.1 饑餓

饑餓是一種描述線程無法定期訪問共享資源,程序無法取得正常執行的一種場景,比如一個同步方法執行時間很長,但是多條線程爭搶且頻繁的執行,那么將會有大量線程無法在正常的情況下獲得使用權,造成大量阻塞和積壓,我們使用饑餓來描述這種并發場景。

6.2 活鎖

活鎖是一種描述線程在執行同步方法的過程中依賴其他外部資源,而該部分獲取緩慢而無保障造成無法進一步執行的的場景,相對于死鎖,活鎖是有機會進一步執行的,只是執行過程緩慢,造成部分資源被 正在等待其他資源的線程占用。

7. 保護塊/守護塊

通常,線程會根據其需要來協調其操作。最常用的協調方式便是通過守護塊的方式,用一個代碼塊來輪詢一個一條件,只有到該條件滿足時,程序才繼續執行。要實現這個功能通常有幾個要遵循的步驟,先給出一個并不是那么好的例子請勿在生產代碼使用以下示例

public void guardedJoy() {
  // 這是一個簡單的輪詢守護塊,但是極其消耗資源
  // 請勿在生產環境中使用此類代碼,這是一個不好的示例
  while(!joy) {}
  System.out.println("Joy has been achieved!");
}

這個例子中,只有當別的線程講joy變量設置為true時,程序才會繼續往下執行,在理論上該方法確實能實現守護的功能,利用簡單的輪詢,一直等待條件滿足后,才繼續往下執行,這是這種輪詢方式是極其消耗資源的,因為輪詢會一直占用CPU資源。別的線程便無法獲得CPU進行處理。

一個更為有效的守護方式是調用Object.wait方法來暫停線程執行,暫停后線程會被阻塞,讓出CPU時間片給其他線程使用,直到其他線程發出一個某些條件已經滿足的通知事件后,該線程會被喚醒重新執行,即使其他線程完成的條件并非它等的哪一個條件。更改上面的代碼

public synchronized void guardedJoy() {
  // 正確的例子,該守護快每次被其他線程喚醒之后只會輪詢一次,
  while(!joy) {
    try{
      wait();
    }catch(Exception e) {}
  }
  System.out.println("Joy has been achieved!");
}

為什么這個版本的守護塊需要同步的?假設d是一個我們調用wait方法的對象,當線程調用d.wait()方法時線程必須擁有對象d的內部鎖,否則將會拋出異常。在一個同步方法內部調用wait()方法是一個簡單的獲取對象內部鎖的方式。當wait()方法被調用后,當前線程會釋放內部鎖并暫停執行,在將來的某一刻,其他線程將會獲得d的內部鎖,并調用d.notifyAll()方法,來喚醒由對象d.wait()方法暫停執行的線程。

public synchronized notifyJoy() {
  joy = true;
  // 喚醒所有被wait()方法暫停的線程
  notifyAll();
}

上述內容就是Java中如何實現并發編程,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女