Java 多執行緒面試問題彙總

吳峻申發表於2014-06-06

寫在前面:

這篇文章是我最近看15個頂級Java多執行緒面試題及回答這篇帖子,根據文中所列問題在網上找的答案彙總。或許某些解答不盡如人意,歡迎大家來補充和指正。另外感謝這篇帖子的翻譯者趙峰以及所有在網路上分享問題答案的朋友們~~

1. 有T1、T2、T3三個執行緒,如何怎樣保證T2在T1執行完後執行,T3在T2執行完後執行?

使用join方法。

join方法的功能是使非同步執行的執行緒變成同步執行。即呼叫執行緒例項的start方法後,該方法會立即返回,如果呼叫start方法後,需要使用一個由這個執行緒計算得到的值,就必須使用join方法。如果不使用join方法,就不能保證當執行到start方法後面的某條語句時,這個執行緒一定會執行完。而使用join方法後,直到這個執行緒退出,程式才會往下執行。

2.Java中的Lock介面,比起synchronized,優勢在哪裡?

如果需要實現一個高效的快取,它允許多個使用者讀,但只允許一個使用者寫,以此來保持它的完整性,如何實現?

Lock介面最大的優勢是為讀和寫分別提供了鎖。

讀寫鎖ReadWriteLock擁有更加強大的功能,它可細分為讀鎖和解鎖。

讀鎖可以允許多個進行讀操作的執行緒同時進入,但不允許寫程式進入;寫鎖只允許一個寫程式進入,在這期間任何程式都不能再進入。(完全符合題目中允許多個使用者讀和一個使用者寫的條件)

要注意的是每個讀寫鎖都有掛鎖和解鎖,最好將每一對掛鎖和解鎖操作都用try、finally來套入中間的程式碼,這樣就會防止因異常的發生而造成死鎖得情況。

下面是一個示例程式:

import java.util.Random;
import java.util.concurrent.locks.*;
public class ReadWriteLockTest {
 public static void main(String[] args) {
  final TheData myData=new TheData();  //這是各執行緒的共享資料
  for(int i=0;i<3;i++){ //開啟3個讀執行緒
   new Thread(new Runnable(){
    @Override
    public void run() {
     while(true){
      myData.get();
     }
    }
   }).start();
  }
  for(int i=0;i<3;i++){ //開啟3個寫執行緒
   new Thread(new Runnable(){
    @Override
    public void run() {
     while(true){
      myData.put(new Random().nextInt(10000));
     }
    }
   }).start();
  }
 }
}
class TheData{
 private Object data=null;
 private ReadWriteLock rwl=new ReentrantReadWriteLock();
 public void get(){
  rwl.readLock().lock();  //讀鎖開啟,讀執行緒均可進入
  try { //用try finally來防止因異常而造成的死鎖
   System.out.println(Thread.currentThread().getName()+"is ready to read");
   Thread.sleep(new Random().nextInt(100));
   System.out.println(Thread.currentThread().getName()+"have read date"+data);
  } catch (InterruptedException e) {
   e.printStackTrace();
  } finally{
   rwl.readLock().unlock(); //讀鎖解鎖
  }
 }
 public void put(Object data){
  rwl.writeLock().lock();  //寫鎖開啟,這時只有一個寫執行緒進入
  try {
   System.out.println(Thread.currentThread().getName()+"is ready to write");
   Thread.sleep(new Random().nextInt(100));
   this.data=data;
   System.out.println(Thread.currentThread().getName()+"have write date"+data);
  } catch (InterruptedException e) {
   e.printStackTrace();
  } finally{
   rwl.writeLock().unlock(); //寫鎖解鎖
  }
 }
}

3. java中wait和sleep方法有何不同?

最大的不同是在等待時wait會釋放鎖,而sleep一直持有鎖。Wait通常被用於執行緒間互動,sleep通常被用於暫停執行。

其它不同有:

  • sleep是Thread類的靜態方法,wait是Object方法。
  • wait,notify和notifyAll只能在同步控制方法或者同步控制塊裡面使用,而sleep可以在任何地方使用
  • sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常

4.如何用Java實現阻塞佇列?

首先,我們要明確阻塞佇列的定義:

阻塞佇列(BlockingQueue)是一個支援兩個附加操作的佇列。這兩個附加的操作是:在佇列為空時,獲取元素的執行緒會等待佇列變為非空。當佇列滿時,儲存元素的執行緒會等待佇列可用。 阻塞佇列常用於生產者和消費者的場景,生產者是往佇列裡新增元素的執行緒,消費者是從佇列裡拿元素的執行緒。阻塞佇列就是生產者存放元素的容器,而消費者也只從容器裡拿元素。

注:有關生產者——消費者問題,可查閱維基百科網址:

http://zh.wikipedia.org/wiki/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E9%97%AE%E9%A2%98

和百度百科網址:

http://baike.baidu.com/view/10800629.htm

阻塞佇列的一個簡單實現:

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);
  }
}

在enqueue和dequeue方法內部,只有佇列的大小等於上限(limit)或者下限(0)時,才呼叫notifyAll方法。如果佇列的大小既不等於上限,也不等於下限,任何執行緒呼叫enqueue或者dequeue方法時,都不會阻塞,都能夠正常的往佇列中新增或者移除元素。

5.編寫Java程式碼,解決生產者——消費者問題。

生產者——消費者問題是研究多執行緒程式時繞不開的經典問題之一,它描述是有一塊緩衝區作為倉庫,生產者可以將產品放入倉庫,消費者則可以從倉庫中取走產品。

使用問題4中阻塞佇列實現程式碼來解決。但此不是唯一解決方案。

解決生產者/消費者問題的方法可分為兩類:

  • 採用某種機制保護生產者和消費者之間的同步;
  • 在生產者和消費者之間建立一個管道。

第一種方式有較高的效率,並且易於實現,程式碼的可控制性較好,屬於常用的模式。第二種管道緩衝區不易控制,被傳輸資料物件不易於封裝等,實用性不強。因此建議使用第一種方式來實現。

同步的核心問題在於:如何保證同一資源被多個執行緒併發訪問時的完整性?

常用的同步方法是採用訊號或加鎖機制,保證資源在任意時刻至多被一個執行緒訪問。Java語言在多執行緒程式設計上實現了完全物件化,提供了對同步機制的良好支援。

在Java中一共有四種方法支援同步,其中前三個是同步方法,一個是管道方法。管道方法不建議使用,阻塞佇列方法在問題4已有描述,現只提供前兩種實現方法。

  • wait()/notify()方法
  • await()/signal()方法
  • BlockingQueue阻塞佇列方法
  • PipedInputStream/PipedOutputStream

生產者類:

public class Producer extends Thread { // 每次生產的產品數量
     private int num;

     // 所在放置的倉庫
     private Storage storage;

     // 建構函式,設定倉庫
     public Producer(Storage storage) {
          this.storage = storage;
     }

     // 執行緒run函式
     public void run() {
          produce(num);
     }

     // 呼叫倉庫Storage的生產函式
     public void produce(int num) {
          storage.produce(num);
     }

     public int getNum() {
          return num;
     }

     public void setNum(int num) {
          this.num = num;
     }

     public Storage getStorage() {
          return storage;
     }

     public void setStorage(Storage storage) {
          this.storage = storage;
     }
}

消費者類:

public class Consumer extends Thread { // 每次消費的產品數量
     private int num;

     // 所在放置的倉庫
     private Storage storage;

     // 建構函式,設定倉庫
     public Consumer(Storage storage) {
          this.storage = storage;
     }

     // 執行緒run函式
     public void run() {
          consume(num);
     }

     // 呼叫倉庫Storage的生產函式
     public void consume(int num) {
          storage.consume(num);
     }

     // get/set方法
     public int getNum() {
          return num;
     }

     public void setNum(int num) {
          this.num = num;
     }

     public Storage getStorage() {
          return storage;
     }

     public void setStorage(Storage storage) {
          this.storage = storage;
     }
}

倉庫類:(wait()/notify()方法)

public class Storage { // 倉庫最大儲存量
     private final int MAX_SIZE = 100;

     // 倉庫儲存的載體
     private LinkedList<Object> list = new LinkedList<Object>();

     // 生產num個產品
     public void produce(int num) {
          // 同步程式碼段
          synchronized (list) {
               // 如果倉庫剩餘容量不足
               while (list.size() + num > MAX_SIZE) {
                    System.out.print("【要生產的產品數量】:" + num);
                    System.out.println(" 【庫存量】:" + list.size() + " 暫時不能執行生產任務!");

                    try {
                         list.wait();// 由於條件不滿足,生產阻塞
                    } catch (InterruptedException e) {
                         e.printStackTrace();
                    }
               }

               // 生產條件滿足情況下,生產num個產品
               for (int i = 1; i <= num; ++i) {
                    list.add(new Object());
               }

               System.out.print("【已經生產產品數】:" + num);
               System.out.println(" 【現倉儲量為】:" + list.size());

               list.notifyAll();
          }
     }

     // 消費num個產品
     public void consume(int num) {
          // 同步程式碼段
          synchronized (list) {
               // 如果倉庫儲存量不足
               while (list.size() < num) {
                    System.out.print("【要消費的產品數量】:" + num);
                    System.out.println(" 【庫存量】:" + list.size() + " 暫時不能執行生產任務!");

                    try {
                         // 由於條件不滿足,消費阻塞
                         list.wait();
                    } catch (InterruptedException e) {
                         e.printStackTrace();
                    }
               }

               // 消費條件滿足情況下,消費num個產品
               for (int i = 1; i <= num; ++i) {
                    list.remove();
               }

               System.out.print("【已經消費產品數】:" + num);
               System.out.println(" 【現倉儲)量為】:" + list.size());

               list.notifyAll();
          }
     }

     // get/set方法
     public LinkedList<Object> getList() {
          return list;
     }

     public void setList(LinkedList<Object> list) {
          this.list = list;
     }

     public int getMAX_SIZE() {
          return MAX_SIZE;
     }
}

倉庫類:(await()/signal()方法)

public class Storage { // 倉庫最大儲存量
     // 倉庫最大儲存量
     private final int MAX_SIZE = 100;

     // 倉庫儲存的載體
     private LinkedList<Object> list = new LinkedList<Object>();

     // 鎖
     private final Lock lock = new ReentrantLock();

     // 倉庫滿的條件變數
     private final Condition full = lock.newCondition();

     // 倉庫空的條件變數
     private final Condition empty = lock.newCondition();

     // 生產num個產品
     public void produce(int num) {
          // 獲得鎖
          lock.lock();

          // 如果倉庫剩餘容量不足
          while (list.size() + num > MAX_SIZE) {
               System.out.print("【要生產的產品數量】:" + num);
               System.out.println(" 【庫存量】:" + list.size() + " 暫時不能執行生產任務!");

               try {
                    // 由於條件不滿足,生產阻塞
                    full.await();
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
          }

          // 生產條件滿足情況下,生產num個產品
          for (int i = 1; i <= num; ++i) {
               list.add(new Object());
          }

          System.out.print("【已經生產產品數】:" + num);
          System.out.println(" 【現倉儲量為】:" + list.size());

          // 喚醒其他所有執行緒
          full.signalAll();
          empty.signalAll();

          // 釋放鎖
          lock.unlock();
     }

     // 消費num個產品
     public void consume(int num) {
          // 獲得鎖
          lock.lock();

          // 如果倉庫儲存量不足
          while (list.size() < num) {
               System.out.print("【要消費的產品數量】:" + num);
               System.out.println(" 【庫存量】:" + list.size() + " 暫時不能執行生產任務!");

               try {
                    // 由於條件不滿足,消費阻塞
                    empty.await();
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
          }

          // 消費條件滿足情況下,消費num個產品
          for (int i = 1; i <= num; ++i) {
               list.remove();
          }

          System.out.print("【已經消費產品數】:" + num);
          System.out.println(" 【現倉儲)量為】:" + list.size());

          // 喚醒其他所有執行緒
          full.signalAll();
          empty.signalAll();

          // 釋放鎖
          lock.unlock();
     }

     // set/get方法
     public int getMAX_SIZE() {
          return MAX_SIZE;
     }

     public LinkedList<Object> getList() {
          return list;
     }

     public void setList(LinkedList<Object> list) {
          this.list = list;
     }
}

6. 如何解決一個用Java編寫的會導致死鎖的程式?

Java執行緒死鎖問題往往和一個被稱之為哲學家就餐的問題相關聯。

注:有關哲學家就餐的問題,可查閱維基百科網址:

http://zh.wikipedia.org/wiki/%E5%93%B2%E5%AD%A6%E5%AE%B6%E5%B0%B1%E9%A4%90%E9%97%AE%E9%A2%98

和百度百科網址:

http://baike.baidu.com/link?url=V-QPP4G1a1PDO1krV6GreFQSp7AQl-KhAP8WGzXw4zl7eeevz3vn07MJMf8SmXfz36CtkDQXMh8kZ36_Fwnfxq

導致死鎖的根源在於不適當地運用“synchronized”關鍵詞來管理執行緒對特定物件的訪問。

“synchronized”關鍵詞的作用是,確保在某個時刻只有一個執行緒被允許執行特定的程式碼塊,因此,被允許執行的執行緒首先必須擁有對變數或物件的排他性的訪問權。當執行緒訪問物件 時,執行緒會給物件加鎖,而這個鎖導致其它也想訪問同一物件的執行緒被阻塞,直至第一個執行緒釋放它加在物件上的鎖。由於這個原因,在使用“synchronized”關鍵詞時,很容易出現兩個執行緒互相等待對方做出某個動作的情形。

死鎖程式例子

public class Deadlocker implements Runnable {
     public int flag = 1;
     static Object o1 = new Object(), o2 = new Object();

     public void run() {
          System.out.println("flag=" + flag);
          if (flag == 1) {
               synchronized (o1) {
                    try {
                         Thread.sleep(500);
                    } catch (Exception e) {
                         e.printStackTrace();
                    }
                    synchronized (o2) {
                         System.out.println("1");
                    }
               }
          }

          if (flag == 0) {
               synchronized (o2) {
                    try {
                         Thread.sleep(500);
                    } catch (Exception e) {
                         e.printStackTrace();
                    }
                    synchronized (o1) {
                         System.out.println("0");
                    }
               }
          }
     }

     public static void main(String[] args) {
          Deadlocker td1 = new Deadlocker();
          Deadlocker td2 = new Deadlocker();
          td1.flag = 1;
          td2.flag = 0;
          Thread t1 = new Thread(td1);
          Thread t2 = new Thread(td2);
          t1.start();
          t2.start();
     }
}

說明:

 當類的物件flag=1時(T1),先鎖定O1,睡眠500毫秒,然後鎖定O2;

 而T1在睡眠的時候另一個flag=0的物件(T2)執行緒啟動,先鎖定O2,睡眠500毫秒,等待T1釋放O1;

 T1睡眠結束後需要鎖定O2才能繼續執行,而此時O2已被T2鎖定;

 T2睡眠結束後需要鎖定O1才能繼續執行,而此時O1已被T1鎖定;

 T1、T2相互等待,都需要對方鎖定的資源才能繼續執行,從而死鎖。

避免死鎖的一個通用的經驗法則是:當幾個執行緒都要訪問共享資源A、B、C時,保證使每個執行緒都按照同樣的順序去訪問它們,比如都先訪問A,再訪問B和C。

如把 Thread t2 = new Thread(td2); 改成 Thread t2 = new Thread(td1);

還有一種方法是對物件進行synchronized,加大鎖定的粒度,如上面的例子中使得程式鎖定當前物件,而不是逐步鎖定當前物件的兩個子物件o1和o2。這樣就在t1鎖定o1之後, 即使發生休眠,當前物件仍然被t1鎖定,t2不能打斷t1去鎖定o2,等t1休眠後再鎖定o2,獲取資源,執行成功。然後釋放當前物件t2,接著t1繼續執行。

程式碼如下:

public class Deadlocker implements Runnable {
     public int flag = 1;
     static Object o1 = new Object(), o2 = new Object();

     public synchronized void run() {
          System.out.println("flag=" + flag);
          if (flag == 1) {
               try {
                    Thread.sleep(500);
               } catch (Exception e) {
                    e.printStackTrace();
               }

               System.out.println("1");
          }
          if (flag == 0) {
               try {
                    Thread.sleep(500);
               } catch (Exception e) {
                    e.printStackTrace();
               }
               System.out.println("0");
          }
     }

     public static void main(String[] args) {
          Deadlocker td1 = new Deadlocker();
          Deadlocker td2 = new Deadlocker();
          td1.flag = 1;
          td2.flag = 0;
          Thread t1 = new Thread(td1);
          Thread t2 = new Thread(td2);
          t1.start();
          t2.start();
     }
}
程式碼修改成public synchronized void run(){..},去掉子物件鎖定。對於一個成員方法加synchronized關鍵字,實際上是以這個成員方法所在的物件本身作為物件鎖。此例中,即對td1,td2這兩個Deadlocker 物件進行加鎖。

第三種解決死鎖的方法是使用實現Lock介面的重入鎖類(ReentrantLock),程式碼如下:

public class Deadlocker implements Runnable {
     public int flag = 1;
     static Object o1 = new Object(), o2 = new Object();
     private final Lock lock = new ReentrantLock();

     public boolean checkLock() {
          return lock.tryLock();
     }

     public void run() {
          if (checkLock()) {
               try {
                    System.out.println("flag=" + flag);
                    if (flag == 1) {
                         try {
                              Thread.sleep(500);
                         } catch (Exception e) {
                              e.printStackTrace();
                         }

                         System.out.println("1");
                    }
                    if (flag == 0) {
                         try {
                              Thread.sleep(500);
                         } catch (Exception e) {
                              e.printStackTrace();
                         }
                         System.out.println("0");
                    }
               } finally {
                    lock.unlock();
               }
          }
     }

     public static void main(String[] args) {
          Deadlocker td1 = new Deadlocker();
          Deadlocker td2 = new Deadlocker();
          td1.flag = 1;
          td2.flag = 0;
          Thread t1 = new Thread(td1);
          Thread t2 = new Thread(td2);
          t1.start();
          t2.start();
     }
}

說明:

程式碼行lock.tryLock()是測試物件操作是否已在執行中,如果已在執行中則不再執行此物件操作,立即返回false,達到忽略物件操作的效果。

7. 什麼是原子操作,Java中的原子操作是什麼?

所謂原子操作是指不會被執行緒排程機制打斷的操作;這種操作一旦開始,就一直執行到結束,中間切換到另一個執行緒。

java中的原子操作介紹:

jdk1.5的包為java.util.concurrent.atomic

這個包裡面提供了一組原子類。其基本特性就是在多執行緒環境下,當有多個執行緒同時執行這些類的例項包含的方法時,具有排他性。

即當某個執行緒進入方法,執行其中的指令時,不會被其他執行緒打斷,而別的執行緒就像鎖一樣,一直等到該方法執行完成,才由JVM從等待佇列中選擇另一個執行緒進入,這只是一種邏輯上的理解。實際上是藉助硬體的相關指令來實現的,但不會阻塞執行緒(synchronized 會把別的等待的執行緒掛,或者說只是在硬體級別上阻塞了)。

其中的類可以分成4組

  • AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • AtomicIntegerArray,AtomicLongArray
  • AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  • AtomicMarkableReference,AtomicStampedReference,AtomicReferenceArray

Atomic類的作用

  • 使得讓對單一資料的操作,實現了原子化
  • 使用Atomic類構建複雜的,無需阻塞的程式碼
  • 訪問對2個或2個以上的atomic變數(或者對單個atomic變數進行2次或2次以上的操作)通常認為是需要同步的,以達到讓這些操作能被作為一個原子單元。

AtomicBoolean , AtomicInteger, AtomicLong, AtomicReference 這四種基本型別用來處理布林,整數,長整數,物件四種資料。

  • 建構函式(兩個建構函式)

    • 預設的建構函式:初始化的資料分別是false,0,0,null

    • 帶參建構函式:引數為初始化的資料

  • set( )和get( )方法:可以原子地設定和獲取atomic的資料。類似於volatile,保證資料會在主存中設定或讀取

  • getAndSet( )方法

    • 原子的將變數設定為新資料,同時返回先前的舊資料
    • 其本質是get( )操作,然後做set( )操作。儘管這2個操作都是atomic,但是他們合併在一起的時候,就不是atomic。在Java的源程式的級別上,如果不依賴synchronized的機制來完成這個工作,是不可能的。只有依靠native方法才可以。
  • compareAndSet( ) 和weakCompareAndSet( )方法

    • 這兩個方法都是conditional modifier方法。這2個方法接受2個引數,一個是期望資料(expected),一個是新資料(new);如果atomic裡面的資料和期望資料一致,則將新資料設定給atomic的資料,返回true,表明成功;否則就不設定,並返回false。
  • 對於AtomicInteger、AtomicLong還提供了一些特別的方法。getAndIncrement( )、incrementAndGet( )、getAndDecrement( )、decrementAndGet ( )、addAndGet( )、getAndAdd( )以實現一些加法,減法原子操作。(注意 --i、++i不是原子操作,其中包含有3個操作步驟:第一步,讀取i;第二步,加1或減1;第三步:寫回記憶體)

例子-使用AtomicReference建立執行緒安全的堆疊

public class LinkedStack<T> {
     private AtomicReference<Node<T>> stacks = new AtomicReference<Node<T>>();

     public T push(T e) {
          Node<T> oldNode, newNode;
          while (true) { //這裡的處理非常的特別,也是必須如此的。
               oldNode = stacks.get();
               newNode = new Node<T>(e, oldNode);
               if (stacks.compareAndSet(oldNode, newNode)) {
                    return e;
               }
          }
     }

     public T pop() {
          Node<T> oldNode, newNode;
          while (true) {
               oldNode = stacks.get();
               newNode = oldNode.next;
               if (stacks.compareAndSet(oldNode, newNode)) {
                    return oldNode.object;
               }
          }
     }

     private static final class Node<T> {
          private T object;
          private Node<T> next;

          private Node(T object, Node<T> next) {
               this.object = object;
               this.next = next;
          }
     }
}

8. Java中的volatile關鍵字是什麼作用?怎樣使用它?在Java中它跟synchronized方法有什麼不同?

volatile在多執行緒中是用來同步變數的。 執行緒為了提高效率,將某成員變數(如A)拷貝了一份(如B),執行緒中對A的訪問其實訪問的是B。只在某些動作時才進行A和B的同步。因此存在A和B不一致的情況。

volatile就是用來避免這種情況的。volatile告訴jvm, 它所修飾的變數不保留拷貝,直接訪問主記憶體中的(也就是上面說的A) 變數。

一個變數宣告為volatile,就意味著這個變數是隨時會被其他執行緒修改的,因此不能將它cache線上程memory中。以下例子展現了volatile的作用:

public class StoppableTask extends Thread {
     private volatile boolean pleaseStop;

     public void run() {
          while (!pleaseStop) {
               // do some stuff...
          }
     }

     public void tellMeToStop() {
          pleaseStop = true;
     }
}

假如pleaseStop沒有被宣告為volatile,執行緒執行run的時候檢查的是自己的副本,就不能及時得知其他執行緒已經呼叫tellMeToStop()修改了pleaseStop的值。

Volatile一般情況下不能代替sychronized,因為volatile不能保證操作的原子性,即使只是i++,實際上也是由多個原子操作組成:

read i; inc; write i,

假如多個執行緒同時執行i++,volatile只能保證他們操作的i是同一塊記憶體,但依然可能出現寫入髒資料的情況。如果配合Java 5增加的atomic wrapper classes,對它們的increase之類的操作就不需要sychronized。

volatile和synchronized的不同是最容易解釋清楚的。volatile是變數修飾符,而synchronized則作用於一段程式碼或方法;看如下三句get程式碼:

  int i1;
     volatile int i2;
     int i3;

     int geti1() {
          return i1;
     }

     int geti2() {
          return i2;
     }

     synchronized int geti3() {
          return i3;
     }

得到儲存在當前執行緒中i1的數值。多個執行緒有多個i1變數拷貝,而且這些i1之間可以互不相同。換句話說,另一個執行緒可能已經改變了它執行緒內的 i1值,而這個值可以和當前執行緒中的i1值不相同。事實上,Java有個思想叫“主”記憶體區域,這裡存放了變數目前的“準確值”。每個執行緒可以有它自己的 變數拷貝,而這個變數拷貝值可以和“主”記憶體區域裡存放的不同。因此實際上存在一種可能:“主”記憶體區域裡的i1值是1,執行緒1裡的i1值是2,執行緒2裡 的i1值是3——這線上程1和執行緒2都改變了它們各自的i1值,而且這個改變還沒來得及傳遞給“主”記憶體區域或其他執行緒時就會發生。

而 geti2()得到的是“主”記憶體區域的i2數值。用volatile修飾後的變數不允許有不同於“主”記憶體區域的變數拷貝。換句話說,一個變數經 volatile修飾後在所有執行緒中必須是同步的;任何執行緒中改變了它的值,所有其他執行緒立即獲取到了相同的值。理所當然的,volatile修飾的變數存取時比一般變數消耗的資源要多一點,因為執行緒有它自己的變數拷貝更為高效。

既然volatile關鍵字已經實現了執行緒間資料同步,又要 synchronized幹什麼呢?它們之間有兩點不同。首先,synchronized獲得並釋放監視器——如果兩個執行緒使用了同一個物件鎖,監視器能強制保證程式碼塊同時只被一個執行緒所執行——這是眾所周知的事實。但是,synchronized也同步記憶體:事實上,synchronized在“ 主”記憶體區域同步整個執行緒的記憶體。因此,執行geti3()方法做了如下幾步:

1.執行緒請求獲得監視this物件的物件鎖(假設未被鎖,否則執行緒等待直到鎖釋放)

2.執行緒記憶體的資料被消除,從“主”記憶體區域中讀入

3.程式碼塊被執行

4,對於變數的任何改變現在可以安全地寫到“主”記憶體區域中(不過geti3()方法不會改變變數值)

5.執行緒釋放監視this物件的物件鎖

因此volatile只是線上程記憶體和“主”記憶體間同步某個變數的值,而synchronized通過鎖定和解鎖某個監視器同步所有變數的值。顯然synchronized要比volatile消耗更多資源。

9. 什麼是競爭條件?如何發現和解決競爭?

兩個執行緒同步操作同一個物件,使這個物件的最終狀態不明——叫做競爭條件。競爭條件可以在任何應該由程式設計師保證原子操作的,而又忘記使用synchronized的地方。

唯一的解決方案就是加鎖。

Java有兩種鎖可供選擇:

  • 物件或者類(class)的鎖。每一個物件或者類都有一個鎖。使用synchronized關鍵字獲取。 synchronized加到static方法上面就使用類鎖,加到普通方法上面就用物件鎖。除此之外synchronized還可以用於鎖定關鍵區域塊(Critical Section)。 synchronized之後要制定一個物件(鎖的攜帶者),並把關鍵區域用大括號包裹起來。synchronized(this){// critical code}。
  • 顯示構建的鎖(java.util.concurrent.locks.Lock),呼叫lock的lock方法鎖定關鍵程式碼。

10.如何使用thread dump?如何分析Thread dump?

Thread Dump是非常有用的診斷Java應用問題的工具,每一個Java虛擬機器都有及時生成顯示所有執行緒在某一點狀態的thread-dump的能力。雖然各個 Java虛擬機器列印輸出格式上略微有一些不同,但是Thread dumps出來的資訊包含執行緒;執行緒的執行狀態、標識和呼叫的堆疊;呼叫的堆疊包含完整的類名,所執行的方法,如果可能的話還有原始碼的行數。

SUN
JVM 產生ThreadDumpSolaris OS

 <ctrl>-’\’ (Control-Backslash)
 kill -QUIT <PID>

HP-UX/UNIX/Linux

Kill -3 <PID>

Windows

直接對MSDOS視窗的程式按Ctrl-break

有些Java應用伺服器是在控制檯上執行,如Weblogic,為了方便獲取threaddump資訊,在weblogic啟動的時候,會將其標準輸出重定向到一個檔案, 用"nohup ./startWebLogic .sh > log.out &"命令,執行"kill -3 <pid>",Thread dump就會輸出到log.out裡。

Tomcat的Thread Dump會輸出到命令列控制檯或者logs的catalina.out檔案裡。為了反映執行緒狀態的動態變化,需要接連做三次以上thread dump,每次間隔10-20s。

IBM JVM 產生Thread Dump

在AIX上用IBM的JVM,記憶體溢位時預設地會產生javacore檔案(關於cpu的)和heapdump檔案(關於記憶體的)。 如果沒有,則參照下列方法:

  1. 在server啟動前設定下面環境變數(可以加在啟動指令碼中)

    export IBM_HEAPDUMP=true
    
    
    export IBM_HEAP_DUMP=true
    
    
    export IBM_HEAPDUMP_OUTOFMEMORY=true
    
    
    export IBM_HEAPDUMPDIR=<directory path>
    
  2. 用set命令檢查引數設定,確保沒有設定DISABLE_JAVADUMP,然後啟動server

  3. 執行kill -3 命令可以生成javacore檔案和heapdump檔案

拿到java thread dump後,你要做的就是查詢"waiting for monitor entry"的thread,如果大量thread都在等待給同一個地址上鎖(因為對於Java,一個物件只有一把鎖),這說明很可能死鎖發生了。比如:

"service-j2ee" prio=5 tid=0x024f1c28 nid=0x125 waiting for monitor entry
[62a3e000..62a3f690]
[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
com.sun.enterprise.resource.IASNonSharedResourcePool.internalGetResource(IASNonS
haredResourcePool.java:625)
[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: - waiting to
lock <0x965d8110> (a com.sun.enterprise.resource.IASNonSharedResourcePool)
[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
com.sun.enterprise.resource.IASNonSharedResourcePool.getResource(IASNonSharedRes
ourcePool.java:520)
................

為了確定問題,常常需要在隔兩分鐘後再次收集一次thread dump,如果得到的輸出相同,仍然是大量thread都在等待給同一個地址上鎖,那麼肯定是死鎖了。

如何找到當前持有鎖的執行緒是解決問題的關鍵。方法是搜尋thread dump,查詢"locked <0x965d8110>", 找到持有鎖的執行緒。

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: "Thread-20" daemon prio=5 tid=0x01394f18
nid=0x109 runnable [6716f000..6716fc28]

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
java.net.SocketInputStream.socketRead0(Native Method)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
java.net.SocketInputStream.read(SocketInputStream.java:129)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at oracle.net.ns.Packet.receive(Unknown Source)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.net.ns.DataPacket.receive(Unknown Source)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.net.ns.NetInputStream.getNextPacket(Unknown Source)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.net.ns.NetInputStream.read(Unknown Source)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.net.ns.NetInputStream.read(Unknown Source)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.net.ns.NetInputStream.read(Unknown Source)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.jdbc.ttc7.MAREngine.unmarshalUB1(MAREngine.java:929)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.jdbc.ttc7.MAREngine.unmarshalSB1(MAREngine.java:893)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.jdbc.ttc7.Ocommoncall.receive(Ocommoncall.java:106)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.jdbc.ttc7.TTC7Protocol.logoff(TTC7Protocol.java:396)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: - locked <0x954f47a0> (a
oracle.jdbc.ttc7.TTC7Protocol)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
oracle.jdbc.driver.OracleConnection.close(OracleConnection.java:1518)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: - locked <0x954f4520> (a
oracle.jdbc.driver.OracleConnection)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
com.sun.enterprise.resource.JdbcUrlAllocator.destroyResource(JdbcUrlAllocator.java:122)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
com.sun.enterprise.resource.IASNonSharedResourcePool.destroyResource(IASNonSharedResourcePool.java:872)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
com.sun.enterprise.resource.IASNonSharedResourcePool.resizePool(IASNonSharedResourcePool.java:1086)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: - locked <0x965d8110> (a
com.sun.enterprise.resource.IASNonSharedResourcePool)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
com.sun.enterprise.resource.IASNonSharedResourcePool$Resizer.run(IASNonSharedResourcePool.java:1178)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
java.util.TimerThread.mainLoop(Timer.java:432)

[27/Jun/2006:10:03:08] WARNING (26140): CORE3283: stderr: at
java.util.TimerThread.run(Timer.java:382)

在這個例子裡,持有鎖的執行緒在等待Oracle返回結果,卻始終等不到響應,因此發生了死鎖。

如果持有鎖的執行緒還在等待給另一個物件上鎖,那麼還是按上面的辦法順藤摸瓜,直到找到死鎖的根源為止。 另外,在thread dump裡還會經常看到這樣的執行緒,它們是等待一個條件而主動放棄鎖的執行緒。例如:

"Thread-1" daemon prio=5 tid=0x014e97a8 nid=0x80 in Object.wait() [68c6f000..68c6fc28]
at java.lang.Object.wait(Native Method) - waiting on <0x95b07178> (a java.util.LinkedList)
at com.iplanet.ias.util.collection.BlockingQueue.remove(BlockingQueue.java:258)
- locked <0x95b07178> (a java.util.LinkedList) at com.iplanet.ias.util.threadpool.FastThreadPool$ThreadPoolThread.run(FastThreadPool.java:241)
at java.lang.Thread.run(Thread.java:534)

有時也會需要分析這類執行緒,尤其是執行緒等待的條件。

其實,Java thread dump並不只用於分析死鎖,其它Java應用執行時古怪的行為都可以用thread dump來分析。

在Java SE 5裡,增加了jstack的工具,也可以獲取thread dump。在Java SE 6裡, 通過jconsole的圖形化工具也可以方便地查詢涉及object monitors 和java.util.concurrent.locks死鎖。

參考文章:http://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html

11. 為什麼呼叫start()方法時會執行run()方法,而不能直接呼叫run()方法?

呼叫start()方法時,將會建立新的執行緒,並且執行在run()方法裡的程式碼。但如果直接呼叫 run()方法,它不會建立新的執行緒也不會執行呼叫執行緒的程式碼。

12. Java中怎樣喚醒一個阻塞的執行緒?

如果是IO阻塞,建立執行緒時,加一個數量的閾值,超過該值後則不再建立。或者為每個執行緒設定標誌變數標誌該執行緒是否已經束,三就是直接加入執行緒組去管理。

如果執行緒因為呼叫 wait()、sleep()、或者join()方法而導致的阻塞,你可以中斷執行緒,並且通過丟擲InterruptedException來喚醒它。

13. Java中CycliBarriar和CountdownLatch有什麼區別?

CountdownLatch: 一個執行緒(或者多個),等待另外N個執行緒完成某個事情之後才能執行。

CycliBarriar: N個執行緒相互等待,任何一個執行緒完成之前,所有的執行緒都必須等待。

這樣應該就清楚一點了,對於CountDownLatch來說,重點是那個“一個執行緒”, 是它在等待,而另外那N的執行緒在把“某個事情”做完之後可以繼續等待,也可以終止。

而對於CyclicBarrier來說,重點是那N個執行緒,他們之間任何一個沒有完成,所有的執行緒都必須等待。

  1. CyclicBarrier可以多次使用,CountDownLatch只能用一次(為0後不可變)
  2. Barrier是等待指定數量執行緒到達再繼續處理;Latch是等待指定事件變為指定狀態後發生再繼續處理,對於CountDown就是計數減為0的事件,但你也可以實現或使用其他Latch,就不是這個事件了...
  3. Barrier是等待指定數量任務完成,Latch是等待其他任務完成指定狀態的改變再繼續

14. 什麼是不可變物件,它對寫併發應用有什麼幫助?

不可變物件(英語:Immutable object)是一種物件,在被創造之後,它的狀態就不可以被改變。

由於它不可更改,併發時不需要其他額外的同步保證,故相比其他的鎖同步等方式的併發效能要好。

衍生問題:為什麼String是不可變的?

  • 字串常量池的需要

字串常量池(String pool, String intern pool, String保留池) 是Java堆記憶體中一個特殊的儲存區域, 當建立一個String物件時,假如此字串值已經存在於常量池中,則不會建立一個新的物件,而是引用已經存在的物件。

如下面的程式碼所示,將會在堆記憶體中只建立一個實際String物件.

String s1 = "abcd";  
String s2 = "abcd";  

示意圖如下所示: enter image description here

假若字串物件允許改變,那麼將會導致各種邏輯錯誤,比如改變一個物件會影響到另一個獨立物件. 嚴格來說,這種常量池的思想,是一種優化手段.

請思考: 假若程式碼如下所示,s1和s2還會指向同一個實際的String物件嗎?

String s1= "ab" + "cd";  
String s2= "abc" + "d";  

也許這個問題違反新手的直覺, 但是考慮到現代編譯器會進行常規的優化, 所以他們都會指向常量池中的同一個物件. 或者,你可以用 jd-gui 之類的工具檢視一下編譯後的class檔案.

  • 允許String物件快取HashCode

Java中String物件的雜湊碼被頻繁地使用, 比如在hashMap 等容器中。

字串不變性保證了hash碼的唯一性,因此可以放心地進行快取.這也是一種效能優化手段,意味著不必每次都去計算新的雜湊碼. 在String類的定義中有如下程式碼:

private int hash;//用來快取HashCode  
  • 安全性

String被許多的Java類(庫)用來當做引數,例如 網路連線地址URL,檔案路徑path,還有反射機制所需要的String引數等, 假若String不是固定不變的,將會引起各種安全隱患。

假如有如下的程式碼:

 boolean connect(String s) {
      if (!isSecure(s)) {
           throw new SecurityException();
      }
      // 如果在其他地方可以修改String,那麼此處就會引起各種預料不到的問題/錯誤
      causeProblem(s);
 }

15. 多執行緒環境中遇到的常見問題是什麼?如何解決?

多執行緒和併發程式中常遇到的有Memory-interface、競爭條件、死鎖、活鎖和飢餓。

Memory-interface(暫無資料)[X]

競爭條件見第9題

死鎖見第6題

活鎖和飢餓:

活鎖(英文 livelock)

概念:指事物1可以使用資源,但它讓其他事物先使用資源;事物2可以使用資源,但它也讓其他事物先使用資源,於是兩者一直謙讓,都無法使用資源。活鎖有一定機率解開。而死鎖(deadlock)是無法解開的。

解決:避免活鎖的簡單方法是採用先來先服務的策略。當多個事務請求封鎖同一資料物件時,封鎖子系統按請求封鎖的先後次序對事務排隊,資料物件上的鎖一旦釋放就批准申請佇列中第一個事務獲得鎖。

飢餓

概念:是指如果事務T1封鎖了資料R,事務T2又請求封鎖R,於是T2等待。T3也請求封鎖R,當T1釋放了R上的封鎖後,系統首先批准了T3的請 求,T2仍然等待。然後T4又請求封鎖R,當T3釋放了R上的封鎖之後,系統又批准了T4的請求......T2可能永遠等待,這就是飢餓。

解決: 公平鎖: 每一個呼叫lock()的執行緒都會進入一個佇列,當解鎖後,只有佇列裡的第一個執行緒被允許鎖住Farlock例項,所有其它的執行緒都將處於等待狀態,直到他們處於佇列頭部。

程式碼示例 公平鎖類:

public class FairLock {
     private boolean isLocked = false;
     private Thread lockingThread = null;
     private List<QueueObject> waitingThreads = new ArrayList<QueueObject>();

     public void lock() throws InterruptedException {
          QueueObject queueObject = new QueueObject();
          boolean isLockedForThisThread = true;
          synchronized (this) {
               waitingThreads.add(queueObject);
          }
          while (isLockedForThisThread) {
               synchronized (this) {
                    isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject;
                    if (!isLockedForThisThread) {
                         isLocked = true;
                         waitingThreads.remove(queueObject);
                         lockingThread = Thread.currentThread();
                         return;
                    }
               }
               try {
                    queueObject.doWait();
               } catch (InterruptedException e) {
                    synchronized (this) {
                         waitingThreads.remove(queueObject);
                    }
                    throw e;
               }
          }
     }

     public synchronized void unlock() {
          if (this.lockingThread != Thread.currentThread()) {
               throw new IllegalMonitorStateException("Calling thread has not locked this lock");
          }
          isLocked = false;
          lockingThread = null;
          if (waitingThreads.size() > 0) {
               waitingThreads.get(0).doNotify();
          }
     }
}

佇列物件類:

public class QueueObject {
     private boolean isNotified = false;

     public synchronized void doWait() throws InterruptedException {
          while (!isNotified) {
               this.wait();
          }
          this.isNotified = false;
     }

     public synchronized void doNotify() {
          this.isNotified = true;
          this.notify();
     }

     public boolean equals(Object o) {
          return this == o;
     }
}

說明:

首先lock()方法不再宣告為synchronized,取而代之的是對必需同步的程式碼,在synchronized中進行巢狀。 FairLock新建立一個QueueObject的例項,並對每個呼叫lock()的執行緒進行入佇列。呼叫unlock()的執行緒將從佇列頭部獲取QueueObject,並對其呼叫doNotify(),用以喚醒在該物件上等待的執行緒。通過這種方式,在同一時間僅有一個等待執行緒獲得喚醒,而不是所有的等待執行緒。這也是實現了FairLock公平性。

注意,在同一個同步塊中,鎖狀態依然被檢查和設定,以避免出現滑漏條件。還有,QueueObject實際是一個semaphore。doWait()和doNotify()方法在QueueObject中儲存著訊號。這樣做以避免一個執行緒在呼叫queueObject.doWait()之前被另一個呼叫unlock()並隨之呼叫 queueObject.doNotify()的執行緒重入,從而導致訊號丟失。queueObject.doWait()呼叫放置在 synchronized(this)塊之外,以避免被monitor巢狀鎖死,所以只要沒有執行緒在lock方法的 synchronized(this)塊中執行,另外的執行緒都可以被解鎖。

最後,注意到queueObject.doWait()在try – catch塊中是怎樣呼叫的。在InterruptedException丟擲的情況下,執行緒得以離開lock(),並需讓它從佇列中移除。

16. 在java中綠色執行緒和本地執行緒區別?

綠色執行緒執行使用者級別的執行緒,且一次只使用一個OS執行緒。 本地執行緒用的是OS執行緒系統,在每個JAVA執行緒中使用一個OS執行緒。 在執行java時,可通過使用-green或 -native標誌來選擇所用執行緒是綠色還是本地。

17. 執行緒與程式的區別?

執行緒是指程式內的一個執行單元,也是程式內的可排程實體.

與程式的區別:

  • 地址空間:程式內的一個執行單元;程式至少有一個執行緒;它們共享程式的地址空間;而程式有自己獨立的地址空間;

  • 資源擁有:程式是資源分配和擁有的單位,同一個程式內的執行緒共享程式的資源

  • 執行緒是處理器排程的基本單位,但程式不是.

  • 二者均可併發執行.

程式和執行緒都是由作業系統所體會的程式執行的基本單元,系統利用該基本單元實現系統對應用的併發性。程式和執行緒的區別在於:

簡而言之,一個程式至少有一個程式,一個程式至少有一個執行緒。執行緒的劃分尺度小於程式,使得多執行緒程式的併發性高。

另外,程式在執行過程中擁有獨立的記憶體單元,而多個執行緒共享記憶體,從而極大地提高了程式的執行效率。 執行緒在執行過程中與程式還是有區別的。每個獨立的執行緒有一個程式執行的入口、順序執行序列和程式的出口。但是執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。

從邏輯角度來看,多執行緒的意義在於一個應用程式中,有多個執行部分可以同時執行。但作業系統並沒有將多個執行緒看做多個獨立的應用,來實現程式的排程和管理以及資源分配。這就是程式和執行緒的重要區別。

程式是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程式是系統進行資源分配和排程的一個獨立單位.

執行緒是程式的一個實體,是CPU排程和分派的基本單位,它是比程式更小的能獨立執行的基本單位.執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程式的其他的執行緒共享程式所擁有的全部資源.

一個執行緒可以建立和撤銷另一個執行緒;同一個程式中的多個執行緒之間可以併發執行.

18. 什麼是多執行緒中的上下文切換?

作業系統管理很多程式的執行。有些程式是來自各種程式、系統和應用程式的單獨程式,而某些程式來自被分解為很多程式的應用或程式。當一個程式從核心中移出, 另一個程式成為活動的,這些程式之間便發生了上下文切換。作業系統必須記錄重啟程式和啟動新程式使之活動所需要的所有資訊。這些資訊被稱作上下文,它描述 了程式的現有狀態。當程式成為活動的,它可以繼續從被搶佔的位置開始執行。

當執行緒被搶佔時,就會發生執行緒之間的上下文切換。如果執行緒屬於相同的程式,它們共享相同的地址空間,因為執行緒包含在它們所屬於的程式的地址空間內。這樣,程式需要恢復的多數資訊對於執行緒而言是不需要的。儘管程式和它的執行緒共享了很多內容,但最為重要的是其地址空間和資源,有些資訊對於執行緒而言是本地且唯一 的,而執行緒的其他方面包含在程式的各個段的內部。

19. 死鎖與活鎖的區別,死鎖與飢餓的區別?

死鎖: 是指兩個或兩個以上的程式在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生 了死鎖,這些永遠在互相等待的程式稱為死鎖程式。 由於資源佔用是互斥的,當某個程式提出申請資源後,使得有關程式在無外力協助下,永遠分配不到必需的資源而無法繼續執行,這就產生了一種特殊現象:死鎖。

雖然程式在執行過程中,可能發生死鎖,但死鎖的發生也必須具備一定的條件,死鎖的發生必須具備以下四個必要條件。

  • 互斥條件:指程式對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個程式佔用。如果此時還有其它程式請求資源,則請求者只能等待,直至佔有資源的程式用畢釋放。
  • 請求和保持條件:指程式已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程式佔有,此時請求程式阻塞,但又對自己已獲得的其它資源保持不放。
  • 不剝奪條件:指程式已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
  • 環路等待條件:指在發生死鎖時,必然存在一個程式——資源的環形鏈,即程式集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。

活鎖:指事物1可以使用資源,但它讓其他事物先使用資源;事物2可以使用資源,但它也讓其他事物先使用資源,於是兩者一直謙讓,都無法使用資源。

活鎖有一定機率解開。而死鎖(deadlock)是無法解開的。

避免活鎖的簡單方法是採用先來先服務的策略。當多個事務請求封鎖同一資料物件時,封鎖子系統按請求封鎖的先後次序對事務排隊,資料物件上的鎖一旦釋放就批准申請佇列中第一個事務獲得鎖。

死鎖與飢餓的區別?見第15題

20. Java中用到的執行緒排程演算法是什麼?

計算機通常只有一個CPU,在任意時刻只能執行一條機器指令,每個執行緒只有獲得CPU的使用權才能執行指令. 所謂多執行緒的併發執行,其實是指從巨集觀上看,各個執行緒輪流獲得CPU的使用權,分別執行各自的任務.在執行池中,會有多個處於就緒狀態的執行緒在等待CPU,JAVA虛擬機器的一項任務就是負責執行緒的排程,執行緒排程是指按照特定機制為多個執行緒分配CPU的使用權

java虛擬機器採用搶佔式排程模型,是指優先讓可執行池中優先順序高的執行緒佔用CPU,如果可執行池中的執行緒優先順序相同,那麼就隨機選擇一個執行緒,使其佔用CPU。處於執行狀態的執行緒會一直執行,直至它不得不放棄CPU。

一個執行緒會因為以下原因而放棄CPU。

  • java虛擬機器讓當前執行緒暫時放棄CPU,轉到就緒狀態,使其它執行緒獲得執行機會。
  • 當前執行緒因為某些原因而進入阻塞狀態
  • 執行緒結束執行

需要注意的是,執行緒的排程不是跨平臺的,它不僅僅取決於java虛擬機器,還依賴於作業系統。在某些作業系統中,只要執行中的執行緒沒有遇到阻塞,就不會放棄CPU;

在某些作業系統中,即使執行緒沒有遇到阻塞,也會執行一段時間後放棄CPU,給其它執行緒執行的機會。 java的執行緒排程是不分時的,同時啟動多個執行緒後,不能保證各個執行緒輪流獲得均等的CPU時間片。 如果希望明確地讓一個執行緒給另外一個執行緒執行的機會,可以採取以下辦法之一。

調整各個執行緒的優先順序

  • 讓處於執行狀態的執行緒呼叫Thread.sleep()方法

  • 讓處於執行狀態的執行緒呼叫Thread.yield()方法

  • 讓處於執行狀態的執行緒呼叫另一個執行緒的join()方法

21.在Java中什麼是執行緒排程?

見上題

22. 線上程中,怎麼處理不可捕捉異常?

捕捉異常有兩種方法。

  • 把執行緒的錯誤捕捉到,往上拋
  • 通過執行緒池工廠,把異常捕捉到,uncaughtException往log4j寫錯誤日誌

示例程式碼:

public class TestThread implements Runnable {
     public void run() {
          throw new RuntimeException("throwing runtimeException.....");
     }
}

當執行緒程式碼丟擲執行級別異常之後,執行緒會中斷。主執行緒不受這個影響,不會處理這個,而且根本不能捕捉到這個異常,仍然繼續執行自己的程式碼。

  • 方法1)程式碼示例:

      public class TestMain {             
          public static void main(String[] args) {
              try {
                   TestThread t = new TestThread();
                   ExecutorService exec = Executors.newCachedThreadPool();
                   Future future = exec.submit(t);
                   exec.shutdown();
                   future.get();//主要是這句話起了作用,呼叫get()方法,異常重丟擲,包裝在ExecutorException
              } catch (Exception e) {//這裡可以把執行緒的異常繼續丟擲去
                   System.out.println("Exception Throw:" + e.getMessage());
              }
         }
    }
    
  • 方法2)程式碼示例:

    public class HandlerThreadFactory implements ThreadFactory {
         public Thread newThread(Runnable runnable) {
              Thread t = new Thread(runnable);
              MyUncaughtExceptionHandler myUncaughtExceptionHandler = new MyUncaughtExceptionHandler();
              t.setUncaughtExceptionHandler(myUncaughtExceptionHandler);
              return t;
         }
    }
    
    
    public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler  {
           public void uncaughtException(Thread t, Throwable e) {
              System.out.println("write logger here:" + e);
         }
    }
    
    
    public class TestMain {   
        public static void main(String[] args) {
              try {
                   TestThread t = new TestThread();
                   ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
                   exec.execute(t);
              } catch (Exception e) {
                   System.out.println("Exception Throw:" + e.getMessage());
              }
         }
    }
    

23. 什麼是執行緒組,為什麼在Java中不推薦使用?

ThreadGroup執行緒組表示一個執行緒的集合。此外,執行緒組也可以包含其他執行緒組。執行緒組構成一棵樹,在樹中,除了初始執行緒組外,每個執行緒組都有一個父執行緒組。

允許執行緒訪問有關自己的執行緒組的資訊,但是不允許它訪問有關其執行緒組的父執行緒組或其他任何執行緒組的資訊。執行緒組的目的就是對執行緒進行管理。

執行緒組為什麼不推薦使用

節省頻繁建立和銷燬執行緒的開銷,提升執行緒使用效率。

衍生問題:執行緒組和執行緒池的區別在哪裡?

一個執行緒的週期分為:建立、執行、銷燬三個階段。處理一個任務時,首先建立一個任務執行緒,然後執行任務,完了,銷燬執行緒。而執行緒處於執行狀態的時候,才是真的在處理我們交給它的任務,這個階段才是有效執行時間。所以,我們希望花在建立和銷燬執行緒的資源越少越好。如果不銷燬執行緒,而這個執行緒又不能被其他的任務呼叫,那麼就會出現資源的浪費。為了提高效率,減少建立和銷燬執行緒帶來時間和空間上的浪費,出現了執行緒池技術。這種技術是在開始就建立一定量的執行緒,批量處理一類任務,等待任務的到來。任務執行完畢後,執行緒又可以執行其他的任務。等不再需要執行緒的時候,就銷燬。這樣就省去了頻繁建立和銷燬執行緒的麻煩。

24. 為什麼使用Executor框架比使用應用建立和管理執行緒好?

大多數併發應用程式是以執行任務(task)為基本單位進行管理的。通常情況下,我們會為每個任務單獨建立一個執行緒來執行。

這樣會帶來兩個問題:

一,大量的執行緒(>100)會消耗系統資源,使執行緒排程的開銷變大,引起效能下降;

二,對於生命週期短暫的任務,頻繁地建立和消亡執行緒並不是明智的選擇。因為建立和消亡執行緒的開銷可能會大於使用多執行緒帶來的效能好處。

一種更加合理的使用多執行緒的方法是使用執行緒池(Thread Pool)。 java.util.concurrent 提供了一個靈活的執行緒池實現:Executor 框架。這個框架可以用於非同步任務執行,而且支援很多不同型別的任務執行策略。它還為任務提交和任務執行之間的解耦提供了標準的方法,為使用 Runnable 描述任務提供了通用的方式。 Executor的實現還提供了對生命週期的支援和hook 函式,可以新增如統計收集、應用程式管理機制和監視器等擴充套件。

線上程池中執行任務執行緒,可以重用已存在的執行緒,免除建立新的執行緒。這樣可以在處理多個任務時減少執行緒建立、消亡的開銷。同時,在任務到達時,工作執行緒通常已經存在,用於建立執行緒的等待時間不會延遲任務的執行,因此提高了響應性。通過適當的調整執行緒池的大小,在得到足夠多的執行緒以保持處理器忙碌的同時,還可以防止過多的執行緒相互競爭資源,導致應用程式線上程管理上耗費過多的資源。

25. 在Java中Executor和Executors的區別?

Executor是介面,是用來執行 Runnable 任務的;它只定義一個方法- execute(Runnable command);執行 Ruannable 型別的任務。

Executors是類,提供了一系列工廠方法用於建立執行緒池,返回的執行緒池都實現了ExecutorService介面。

Executors幾個重要方法:

callable(Runnable task): 將 Runnable 的任務轉化成 Callable 的任務

newSingleThreadExecutor(): 產生一個ExecutorService物件,這個物件只有一個執行緒可用來執行任務,若任務多於一個,任務將按先後順序執行。

newCachedThreadPool(): 產生一個ExecutorService物件,這個物件帶有一個執行緒池,執行緒池的大小會根據需要調整,執行緒執行完任務後返回執行緒池,供執行下一次任務使用。

newFixedThreadPool(int poolSize): 產生一個ExecutorService物件,這個物件帶有一個大小為 poolSize 的執行緒池,若任務數量大於 poolSize ,任務會被放在一個 queue 裡順序執行。

newSingleThreadScheduledExecutor(): 產生一個ScheduledExecutorService物件,這個物件的執行緒池大小為 1 ,若任務多於一個,任務將按先後順序執行。

newScheduledThreadPool(int poolSize): 產生一個ScheduledExecutorService物件,這個物件的執行緒池大小為 poolSize ,若任務數量大於 poolSize ,任務會在一個 queue 裡等待執行。

26. 如何在Windows和Linux上查詢哪個執行緒使用的CPU時間最長?

其實就是找CPU佔有率最高的那個執行緒

Windows

工作管理員裡面看,如下圖:

enter image description here

Linux

可以用下面的命令將 cpu 佔用率高的執行緒找出來:

$ ps H -eo user,pid,ppid,tid,time,%cpu,cmd –sort=%cpu

這個命令首先指定引數’H',顯示執行緒相關的資訊,格式輸出中包含:

user,pid,ppid,tid,time,%cpu,cmd

然後再用%cpu欄位進行排序。這樣就可以找到佔用處理器的執行緒了。

相關文章