天天看點

Java多線程面試問題彙總(深入了解篇)

非常不錯的一篇文章,有新的收獲:http://www.ituring.com.cn/article/177182

寫在前面

這篇文章是根據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)是一個支援兩個附加操作的隊列。這兩個附加的操作是:在隊列為空時,擷取元素的線程會等待隊列變為非空。當隊列滿時,存儲元素的線程會等待隊列可用。 阻塞隊列常用于生産者和消費者的場景,生産者是往隊列裡添加元素的線程,消費者是從隊列裡拿元素的線程。阻塞隊列就是生産者存放元素的容器,而消費者也隻從容器裡拿元素。

注:有關生産者——消費者問題,可查閱維基百科和百度百科。

阻塞隊列的一個簡單實作:

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編寫的會導緻死鎖的程式?

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

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

導緻死鎖的根源在于不适當地運用“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,達到忽略對象操作的效果。

6. 什麼是原子操作,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;
          }
     }
}
           

7. 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++,實際上也是由多個原子操作組成:

假如多個線程同時執行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消耗更多資源。

8. 什麼是競争條件?如何發現和解決競争?

兩個線程同步操作同一個對象,使這個對象的最終狀态不明——叫做競争條件。競争條件可以在任何應該由程式員保證原子操作的,而又忘記使用synchronized的地方。

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

Java有兩種鎖可供選擇:

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

9. 為什麼調用start()方法時會執行run()方法,而不能直接調用run()方法?

調用start()方法時,将會建立新的線程,并且執行在run()方法裡的代碼。但如果直接調用 run()方法,它不會建立新的線程也不會執行調用線程的代碼。

10. Java中怎樣喚醒一個阻塞的線程?

如果是IO阻塞,建立線程時,加一個數量的門檻值,超過該值後則不再建立。或者為每個線程設定标志變量标志該線程是否已經束,三就是直接加入線程組去管理。

如果線程因為調用 wait()、sleep()、或者join()方法而導緻的阻塞,你可以中斷線程,并且通過抛出InterruptedException來喚醒它。

11. Java中CycliBarriar和CountdownLatch有什麼差別?

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

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

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

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

1. CyclicBarrier可以多次使用,CountDownLatch隻能用一次(為0後不可變)

2. Barrier是等待指定數量線程到達再繼續處理;Latch是等待指定事件變為指定狀态後發生再繼續處理,對于CountDown就是計數減為0的事件,但你也可以實作或使用其他Latch,就不是這個事件了...

3. Barrier是等待指定數量任務完成,Latch是等待其他任務完成指定狀态的改變再繼續

12. 什麼是不可變對象,它對寫并發應用有什麼幫助?

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

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

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

  • 字元串常量池的需要

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

如下面的代碼所示,将會在堆記憶體中隻建立一個實際String對象.

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

示意圖如下所示:

Java多線程面試問題彙總(深入了解篇)

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

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

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

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

  • 允許String對象緩存HashCode

Java中String對象的哈希碼被頻繁地使用, 比如在hashMap 等容器中。

字元串不變性保證了hash碼的唯一性,是以可以放心地進行緩存.這也是一種性能優化手段,意味着不必每次都去計算新的哈希碼. 在String類的定義中有如下代碼:

  • 安全性

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

假如有如下的代碼:

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

13. 多線程環境中遇到的常見問題是什麼?如何解決?

多線程和并發程式中常遇到的有Memory-interface、競争條件、死鎖、活鎖和饑餓。

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

競争條件見第8題

死鎖見第5題

活鎖和饑餓:

活鎖(英文 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(),并需讓它從隊列中移除。

14. 在java中綠色線程和本地線程差別?

綠色線程執行使用者級别的線程,且一次隻使用一個OS線程。本地線程用的是OS線程系統,在每個JAVA線程中使用一個OS線程。在執行java時,可通過使用-green或 -native标志來選擇所用線程是綠色還是本地。

15. 線程與程序的差別?

線程是指程序内的一個執行單元,也是程序内的可排程實體.

與程序的差別:

  • 位址空間:程序内的一個執行單元;程序至少有一個線程;它們共享程序的位址空間;而程序有自己獨立的位址空間;
  • 資源擁有:程序是資源配置設定和擁有的機關,同一個程序内的線程共享程序的資源
  • 線程是處理器排程的基本機關,但程序不是.
  • 二者均可并發執行.

程序和線程都是由作業系統所體會的程式運作的基本單元,系統利用該基本單元實作系統對應用的并發性。程序和線程的差別在于:

簡而言之,一個程式至少有一個程序,一個程序至少有一個線程。線程的劃分尺度小于程序,使得多線程程式的并發性高。

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

從邏輯角度來看,多線程的意義在于一個應用程式中,有多個執行部分可以同時執行。但作業系統并沒有将多個線程看做多個獨立的應用,來實作程序的排程和管理以及資源配置設定。這就是程序和線程的重要差別。

程序是具有一定獨立功能的程式關于某個資料集合上的一次運作活動,程序是系統進行資源配置設定和排程的一個獨立機關.

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

一個線程可以建立和撤銷另一個線程;同一個程序中的多個線程之間可以并發執行.

16. 什麼是多線程中的上下文切換?

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

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

17. 死鎖與活鎖的差別,死鎖與饑餓的差別?

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

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

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

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

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

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

死鎖與饑餓的差別?見第13題

18. Java中用到的線程排程算法是什麼?

計算機通常隻有一個CPU,在任意時刻隻能執行一條機器指令,每個線程隻有獲得CPU的使用權才能執行指令. 所謂多線程的并發運作,其實是指從宏觀上看,各個線程輪流獲得CPU的使用權,分别執行各自的任務.在運作池中,會有多個處于就緒狀态的線程在等待CPU,JAVA虛拟機的一項任務就是負責線程的排程,線程排程是指按照特定機制為多個線程配置設定CPU的使用權

java虛拟機采用搶占式排程模型,是指優先讓可運作池中優先級高的線程占用CPU,如果可運作池中的線程優先級相同,那麼就随機選擇一個線程,使其占用CPU。處于運作狀态的線程會一直運作,直至它不得不放棄CPU。

一個線程會因為以下原因而放棄CPU。

  • java虛拟機讓目前線程暫時放棄CPU,轉到就緒狀态,使其它線程獲得運作機會。
  • 目前線程因為某些原因而進入阻塞狀态
  • 線程結束運作

需要注意的是,線程的排程不是跨平台的,它不僅僅取決于java虛拟機,還依賴于作業系統。在某些作業系統中,隻要運作中的線程沒有遇到阻塞,就不會放棄CPU;

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

調整各個線程的優先級

  • 讓處于運作狀态的線程調用Thread.sleep()方法
  • 讓處于運作狀态的線程調用Thread.yield()方法
  • 讓處于運作狀态的線程調用另一個線程的join()方法

19.在Java中什麼是線程排程?

見上題

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

捕捉異常有兩種方法。

  • 把線程的錯誤捕捉到,往上抛
  • 通過線程池工廠,把異常捕捉到,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());
              }
         }
    }
               

21. 什麼是線程組,為什麼在Java中不推薦使用?

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

允許線程通路有關自己的線程組的資訊,但是不允許它通路有關其線程組的父線程組或其他任何線程組的資訊。線程組的目的就是對線程進行管理。

線程組為什麼不推薦使用

節省頻繁建立和銷毀線程的開銷,提升線程使用效率。

衍生問題:線程組和線程池的差別在哪裡?

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

22. 為什麼使用Executor架構比使用應用建立和管理線程好?

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

這樣會帶來兩個問題:

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

二,對于生命周期短暫的任務,頻繁地建立和消亡線程并不是明智的選擇。因為建立和消亡線程的開銷可能會大于使用多線程帶來的性能好處。

一種更加合理的使用多線程的方法是使用線程池(Thread Pool)。 java.util.concurrent 提供了一個靈活的線程池實作:Executor 架構。這個架構可以用于異步任務執行,而且支援很多不同類型的任務執行政策。它還為任務送出和任務執行之間的解耦提供了标準的方法,為使用 Runnable 描述任務提供了通用的方式。 Executor的實作還提供了對生命周期的支援和hook 函數,可以添加如統計收集、應用程式管理機制和螢幕等擴充。

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

23. 在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 裡等待執行。

24. 如何在Windows和Linux上查找哪個線程使用的CPU時間最長?

其實就是找CPU占有率最高的那個線程

Windows

任務管理器裡面看,如下圖:

Java多線程面試問題彙總(深入了解篇)

Linux

可以用下面的指令将 cpu 占用率高的線程找出來:

這個指令首先指定參數’H',顯示線程相關的資訊,格式輸出中包含:

然後再用%cpu字段進行排序。這樣就可以找到占用處理器的線程了。