1. 程式人生 > 程式設計 >淺談java多執行緒程式設計

淺談java多執行緒程式設計

一、多執行緒的優缺點

多執行緒的優點:

1)資源利用率更好
2)程式設計在某些情況下更簡單
3)程式響應更快

多執行緒的代價:

1)設計更復雜
雖然有一些多執行緒應用程式比單執行緒的應用程式要簡單,但其他的一般都更復雜。在多執行緒訪問共享資料的時候,這部分程式碼需要特別的注意。執行緒之間的互動往往非常複雜。不正確的執行緒同步產生的錯誤非常難以被發現,並且重現以修復。

2)上下文切換的開銷
當CPU從執行一個執行緒切換到執行另外一個執行緒的時候,它需要先儲存當前執行緒的本地的資料,程式指標等,然後載入另一個執行緒的本地資料,程式指標等,最後才開始執行。這種切換稱為“上下文切換”(“context switch”)。CPU會在一個上下文中執行一個執行緒,然後切換到另外一個上下文中執行另外一個執行緒。上下文切換並不廉價。如果沒有必要,應該減少上下文切換的發生。

二、建立java多執行緒

1、建立Thread的子類

建立Thread子類的一個例項並重寫run方法,run方法會在呼叫start()方法之後被執行。例子如下:

public class MyThread extends Thread {
  public void run(){
   System.out.println("MyThread running");
  }
}

MyThread myThread = new MyThread();
myTread.start();

也可以如下建立一個Thread的匿名子類:

Thread thread = new Thread(){
  public void run(){
   System.out.println("Thread Running");
  }
};
thread.start();

2、實現Runnable介面

第二種編寫執行緒執行程式碼的方式是新建一個實現了java.lang.Runnable介面的類的例項,例項中的方法可以被執行緒呼叫。下面給出例子:

public class MyRunnable implements Runnable {
  public void run(){
  System.out.println("MyRunnable running");
  }
}

Thread thread = new Thread(new MyRunnable());
thread.start();

同樣,也可以建立一個實現了Runnable介面的匿名類,如下所示:

Runnable myRunnable = new Runnable(){
  public void run(){
   System.out.println("Runnable running");
  }
}
Thread thread = new Thread(myRunnable);
thread.start();

三、執行緒安全

在同一程式中執行多個執行緒本身不會導致問題,問題在於多個執行緒訪問了相同的資源。如同一記憶體區(變數,陣列,或物件)、系統(資料庫,web services等)或檔案。實際上,這些問題只有在一或多個執行緒向這些資源做了寫操作時才有可能發生,只要資源沒有發生變化,多個執行緒讀取相同的資源就是安全的。

當兩個執行緒競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。導致競態條件發生的程式碼區稱作臨界區。

如果一個資源的建立,使用,銷燬都在同一個執行緒內完成,且永遠不會脫離該執行緒的控制,則該資源的使用就是執行緒安全的。

四、java同步塊

Java中的同步塊用synchronized標記。同步塊在Java中是同步在某個物件上。所有同步在一個物件上的同步塊在同時只能被一個執行緒進入並執行操作。所有其他等待進入該同步塊的執行緒將被阻塞,直到執行該同步塊中的執行緒退出。

有四種不同的同步塊:

  1. 例項方法
  2. 靜態方法
  3. 例項方法中的同步塊
  4. 靜態方法中的同步塊

例項方法同步:

 public synchronized void add(int value){
this.count += value;
 }

Java例項方法同步是同步在擁有該方法的物件上。這樣,每個例項其方法同步都同步在不同的物件上,即該方法所屬的例項。只有一個執行緒能夠在例項方法同步塊中執行。如果有多個例項存在,那麼一個執行緒一次可以在一個例項同步塊中執行操作。一個例項一個執行緒。

靜態方法同步:

public static synchronized void add(int value){
 count += value;
 }

靜態方法的同步是指同步在該方法所在的類物件上。因為在Java虛擬機器中一個類只能對應一個類物件,所以同時只允許一個執行緒執行同一個類中的靜態同步方法。

例項方法中的同步塊:

public void add(int value){
  synchronized(this){
    this.count += value;
  }
 }

注意Java同步塊構造器用括號將物件括起來。在上例中,使用了“this”,即為呼叫add方法的例項本身。在同步構造器中用括號括起來的物件叫做監視器物件。上述程式碼使用監視器物件同步,同步例項方法使用呼叫方法本身的例項作為監視器物件。一次只有一個執行緒能夠在同步於同一個監視器物件的Java方法內執行。

下面兩個例子都同步他們所呼叫的例項物件上,因此他們在同步的執行效果上是等效的。

public class MyClass {

  public synchronized void log1(String msg1,String msg2){
   log.writeln(msg1);
   log.writeln(msg2);
  }

  public void log2(String msg1,String msg2){
   synchronized(this){
     log.writeln(msg1);
     log.writeln(msg2);
   }
  }
 }

靜態方法中的同步塊:

public class MyClass {
  public static synchronized void log1(String msg1,String msg2){
    log.writeln(msg1);
    log.writeln(msg2);
  }

  public static void log2(String msg1,String msg2){
    synchronized(MyClass.class){
     log.writeln(msg1);
     log.writeln(msg2);
    }
  }
 }

這兩個方法不允許同時被執行緒訪問。如果第二個同步塊不是同步在MyClass.class這個物件上。那麼這兩個方法可以同時被執行緒訪問。

五、java執行緒通訊

執行緒通訊的目標是使執行緒間能夠互相傳送訊號。另一方面,執行緒通訊使執行緒能夠等待其他執行緒的訊號。

Java有一個內建的等待機制來允許執行緒在等待訊號的時候變為非執行狀態。java.lang.Object 類定義了三個方法,wait()、notify()和notifyAll()來實現這個等待機制。

一個執行緒一旦呼叫了任意物件的wait()方法,就會變為非執行狀態,直到另一個執行緒呼叫了同一個物件的notify()方法。為了呼叫wait()或者notify(),執行緒必須先獲得那個物件的鎖。也就是說,執行緒必須在同步塊裡呼叫wait()或者notify()。

以下為一個使用了wait()和notify()實現的執行緒間通訊的共享物件:

public class MyWaitNotify{

 MonitorObject myMonitorObject = new MonitorObject();
 boolean wasSignalled = false;

 public void doWait(){
  synchronized(myMonitorObject){
   while(!wasSignalled){
    try{
     myMonitorObject.wait();
     } catch(InterruptedException e){...}
   }
   //clear signal and continue running.
   wasSignalled = false;
  }
 }

 public void doNotify(){
  synchronized(myMonitorObject){
   wasSignalled = true;
   myMonitorObject.notify();
  }
 }
}

注意以下幾點:

1、不管是等待執行緒還是喚醒執行緒都在同步塊裡呼叫wait()和notify()。這是強制性的!一個執行緒如果沒有持有物件鎖,將不能呼叫wait(),notify()或者notifyAll()。否則,會丟擲IllegalMonitorStateException異常。

2、一旦執行緒呼叫了wait()方法,它就釋放了所持有的監視器物件上的鎖。這將允許其他執行緒也可以呼叫wait()或者notify()。

3、為了避免丟失訊號,必須把它們儲存在訊號類裡。如上面的wasSignalled變數。

4、假喚醒:由於莫名其妙的原因,執行緒有可能在沒有呼叫過notify()和notifyAll()的情況下醒來。這就是所謂的假喚醒(spurious wakeups)。為了防止假喚醒,儲存訊號的成員變數將在一個while迴圈裡接受檢查,而不是在if表示式裡。這樣的一個while迴圈叫做自旋鎖。

5、不要在字串常量或全域性物件中呼叫wait()。即上面MonitorObject不能是字串常量或是全域性物件。每一個MyWaitNotify的例項都擁有一個屬於自己的監視器物件,而不是在空字串上呼叫wait()/notify()。

六、java中的鎖

自Java 5開始,java.util.concurrent.locks包中包含了一些鎖的實現,因此你不用去實現自己的鎖了。

常用的一些鎖:

java.util.concurrent.locks.Lock;
java.util.concurrent.locks.ReentrantLock;
java.util.concurrent.locks.ReadWriteLock;
java.util.concurrent.locks.ReentrantReadWriteLock;

一個可重入鎖(reentrant lock)的簡單實現:

public class Lock {
  boolean isLocked = false;
  Thread lockedBy = null;
  int lockedCount = 0;

  public synchronized void lock() throws InterruptedException{
    Thread callingThread = Thread.currentThread();
    while(isLocked && lockedBy != callingThread){
      wait();
    }
    isLocked = true;
    lockedCount++;
    lockedBy = callingThread;
  }

  public synchronized void unlock(){
    if(Thread.currentThread() == this.lockedBy){
      lockedCount--;
      if(lockedCount == 0){
        isLocked = false;
        notify();
      }
    }
  }
}

注意的一點:在finally語句中呼叫unlock()

lock.lock();
try{
  //do critical section code,which may throw exception
} finally {
  lock.unlock();
}

七、java中其他同步方法

訊號量(Semaphore):java.util.concurrent.Semaphore

阻塞佇列(Blocking Queue):java.util.concurrent.BlockingQueue

public class BlockingQueue {
  private List queue = new LinkedList();
  private int limit = 10;

  public BlockingQueue(int limit) {
    this.limit = limit;
  }

  public synchronized void enqueue(Object item) throws InterruptedException {
    while (this.queue.size() == this.limit) {
      wait();
    }
    if (this.queue.size() == 0) {
      notifyAll();
    }
    this.queue.add(item);
  }

  public synchronized Object dequeue() throws InterruptedException {
    while (this.queue.size() == 0) {
      wait();
    }
    if (this.queue.size() == this.limit) {
      notifyAll();
    }
    return this.queue.remove(0);
  }
}

八、java中的執行緒池

Java通過Executors提供四種執行緒池,分別為:

newCachedThreadPool

建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。

newFixedThreadPool

建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。

newScheduledThreadPool

建立一個大小無限制的執行緒池。此執行緒池支援定時以及週期性執行任務。

newSingleThreadExecutor

建立一個單執行緒的執行緒池。此執行緒池支援定時以及週期性執行任務。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

執行緒池簡單用法:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
  public static void main(String[] args) {
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
      final int index = i;
      cachedThreadPool.execute(new Runnable() {
        public void run() {
          System.out.println(index);
        }
      });
    }
  }
}

以上就是淺談java多執行緒程式設計的詳細內容,更多關於java多執行緒的資料請關注我們其它相關文章!