Java 多線程面試問題彙總

寫在前面:

這篇文章是我最近看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鎖定;

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

避免死鎖的一個通用的經驗法則是:當幾個線程都要訪問共享資源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關鍵字,實際上是以這個成員方法所在的對象本身作爲對象鎖。此例中,即對td1td2這兩個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字段進行排序。這樣就可以找到佔用處理器的線程了。

原文地址:http://www.ituring.com.cn/article/111835
發佈了18 篇原創文章 · 獲贊 152 · 訪問量 125萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章