多線程併發編程

 1、學習架構圖html

2、建立線程java

(一)直接建立方式:程序員

1.建立一個類繼承Thread,並重現run方法,而後調用start()啓動線程;面試

class hello extends Thread {
    public void run() {
        for (int i = 0; i < 7; i++) {
            if (count > 0) {
                System.out.println("count= " + count--);
            }
        }
    }
    public static void main(String[] args) {
        hello h1 = new hello();
        hello h2 = new hello();
        hello h3 = new hello();
        h1.start();
        h2.start();
        h3.start();
    }
 
    private int count = 5;
}

或者直接在須要調用的方法體內新建Thread,並重載run方法算法

public void testThread(final int i){
        Thread thread=new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println(i);
            }
        });
        thread.start();
    }

2.實現Runnable方法,並實現run接口;再調用start()進行線程啓動;數據庫

class MyThread implements Runnable{
 
    private int ticket = 5;  //5張票
 
    public void run() {
        for (int i=0; i<=20; i++) {
            if (this.ticket > 0) {
                System.out.println(Thread.currentThread().getName()+ "正在賣票"+this.ticket--);
            }
        }
    }
}
public class lzwCode {
     
    public static void main(String [] args) {
        MyThread my = new MyThread();
        new Thread(my, "1號窗口").start();
        new Thread(my, "2號窗口").start();
        new Thread(my, "3號窗口").start();
    }
}

說明:其實Thread也是實現Runnable接口的。這種操做模式其實就是代理模式。編程

實現Runnable接口比繼承Thread類所具備的優點:緩存

1):適合多個相同的程序代碼的線程去處理同一個資源安全

2):能夠避免java中的單繼承的限制性能優化

3):增長程序的健壯性,代碼能夠被多個線程共享,代碼和數據獨立。

方法說明:

A、睡眠操做,經過調用sleep方法來實現;通過睡眠時間後,線程從不可運行狀態自動恢復爲可運行狀態;

B、掛起操做,經過調用suspend方法來實現;必須調用恢復(resume)方法纔可將線程從不可運行狀態恢復爲可運行狀態;

C、等待操做,經過調用wait方法來實現;必須調用notify或notifyAll方法纔可將線程從不可運行狀態恢復爲可運行狀態;

退讓操做,經過調用yield方法來實現;

終止操做,經過調用stop方法來實現。

前三種方法調用便可將線程從可運行狀態轉變爲不可運行狀態;

不可運行狀態恢復爲可運行狀態的三種形式:

A、自動恢復,調用sleep()方法進入的不可運行狀態;

B、用resume方法進行恢復,對應可運行狀態方法爲suspend();

C、通知方法(notify或notifyAll)進行恢復,對應可運行狀態方法爲wait();

線程消亡狀態:調用(stop)方法終止線程;

(二)線程池管理建立方式(減小頻繁建立線程帶來的資源消耗)

Executors類下面包括如下幾種建立線程池方法:

好比:ExecutorService pool= Executors.newCachedThreadPool();

pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("線程中執行代碼");
    }
});

newCachedThreadPool():建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
newFixedThreadPool(int nThreads):建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newSingleThreadExecutor():建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序無界隊列方式(FIFO, LIFO, 優先級)執行。
new ScheduledThreadPool(int corePoolSize):建立一個線程池,它可安排在給定延遲後運行命令或者按期地執行。
適合於:定時或者延遲的任務,在異步操做中須要 超時回調的場景

(三)fork-join方式(分而治之)

特色:多線程框架、分而治之、工做密取(本身任務完成了,就主動從別的線程中提取任務進行完成)

在Java的Fork/Join框架中,使用兩個類完成上述操做

1.ForkJoinTask:咱們要使用Fork/Join框架,首先須要建立一個ForkJoin任務。該類提供了在任務中執行fork和join的機制。一般狀況下咱們不須要直接集成ForkJoinTask類,只須要繼承它的子類,Fork/Join框架提供了兩個子類:

a.RecursiveAction:用於沒有返回結果的任務,異步,調用方式:execute()

b.RecursiveTask:用於有返回結果的任務,同步,調用方式:invoke();

2.ForkJoinPool:ForkJoinTask須要經過ForkJoinPool來執行

例子:求1+2+3+4的結果

public class CountTask extends RecursiveTask<Integer>{

    private static final int THREAD_HOLD = 2;

    private int start;
    private int end;

    public CountTask(int start,int end){
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        //若是任務足夠小就計算
        boolean canCompute = (end - start) <= THREAD_HOLD;
        if(canCompute){
            for(int i=start;i<=end;i++){
                sum += i;
            }
        }else{
            int middle = (start + end) / 2;
            CountTask left = new CountTask(start,middle);
            CountTask right = new CountTask(middle+1,end);
            //執行子任務
            left.fork();
            right.fork();
            //獲取子任務結果
            int lResult = left.join();
            int rResult = right.join();
            sum = lResult + rResult;
        }
        return sum;
    }

    public static void main(String[] args){
        ForkJoinPool pool = new ForkJoinPool();
        CountTask task = new CountTask(1,4);
        Future<Integer> result = pool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

3、知識點

(一)關鍵字volatile 和synchronize

volatile所修飾的變量不保留拷貝,直接訪問主內存中的(強制線程從主內存中取變量而不是拷貝);它具有可見性,而不具有原子性;java的內存模型,每一個線程都會產生一個獨立的工做內存(從主內存中產生副本);一個變量聲明爲volatile就不會被複制;好比兩個線程同時複製主內存一個變量,都加1;結果就多是2或者3,這就表明非原子性;volatile保證寫入後當即同步到全部線程可見;但未更新時,若是被其餘線程獲取了,那就保證不了原子性了;單獨用volatile不足以實現計數器、互斥鎖等功能;使用場景:單例模式

synchronized當用來修飾一個方法或者一個代碼塊的時候,可以保證在同一時刻最多隻有一個線程執行該段代碼;其餘訪問該同步代碼塊的線程將會被阻塞;synchronized是同時具有原子性和可見性的;

synchronize和volatile關鍵字能防止指令的重排序(還包括好比join或者CountDownLatch等等都能防止指令重排序)

區別:

1.volatile是變量修飾符,而synchronized則做用於一段代碼或方法。

 2.volatile只是在線程內存和「主」內存間同步某個變量的值;而synchronized經過鎖定和解鎖某個監視器同步全部變量的值。顯然synchronized要比volatile消耗更多資源。 

模式 #1:狀態標誌

也許實現 volatile 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。

volatile boolean shutdownRequested;
...
public void shutdown() { 
    shutdownRequested = true; 
}
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

線程1執行doWork()的過程當中,可能有另外的線程2調用了shutdown,因此boolean變量必須是volatile。

而若是使用 synchronized 塊編寫循環要比使用 volatile 狀態標誌編寫麻煩不少。因爲 volatile 簡化了編碼,而且狀態標誌並不依賴於程序內任何其餘狀態,所以此處很是適合使用 volatile。

模式 #2:一次性安全發佈

private volatile static Singleton instace;   
public static Singleton getInstance(){   
    //第一次null檢查     
    if(instance == null){            
        synchronized(Singleton.class) {    //1     
            //第二次null檢查       
            if(instance == null){          //2  
                instance = new Singleton();//3  
            }  
        }           
    }  
    return instance; 
}

若是不用volatile,則由於內存模型容許所謂的「無序寫入」,可能致使失敗。——某個線程可能會得到一個未徹底初始化的實例。

模式 #4:容器模式

不少框架爲易變數據的持有者(例如 HttpSession)提供了容器,可是放入這些容器中的對象必須是線程安全的。

(二)鎖(會阻塞線程)

鎖從宏觀上分類,分爲悲觀鎖與樂觀鎖

樂觀鎖:讀不上鎖,寫加鎖,適用於讀頻繁,寫不多場景;java中的樂觀鎖基本都是經過CAS操做實現的,CAS是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。

悲觀鎖:寫頻繁,每次讀都認爲變了;故要在讀寫上都加上鎖;java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如RetreenLock。

前面提到了java的4種鎖,他們分別是重量級鎖、自旋鎖、輕量級鎖和偏向鎖;

ReentrantLock與synchronize區別

很明顯Synchronized的使用比較方便簡潔,而且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock須要手工聲明來加鎖和釋放鎖,爲了不忘記手工釋放鎖形成死鎖,因此最好在finally中聲明釋放鎖。鎖的細粒度和靈活度:很明顯ReenTrantLock優於Synchronized

使用synchronized。若是Thread1不釋放,Thread2將一直等待,不能被中斷

使用ReentrantLock。若是Thread1不釋放,Thread2等待了很長時間之後,能夠中斷等待,轉而去作別的事情。

private static Lock lock = new ReentrantLock();   //若是lock不加static關鍵字時,那麼每個線程將鎖住的是具體的對象,

(三)併發集合

java.util.concurrent包中包含的併發集合類以下:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet

(1)ConcurrentHashMap在線程安全的基礎上提供了更好的寫併發能力,但同時下降了對讀一致性的要求;

ConcurrentHashMap採用了分段鎖(sendment)的設計,只有在同一個分段內才存在競態關係,不一樣的分段鎖之間沒有鎖競爭。相比於對整個Map加鎖的設計,分段鎖大大的提升了高併發環境下的處理能力。但同時,因爲不是對整個Map加鎖,致使一些須要掃描整個Map的方法(如size(), containsValue())須要使用特殊的實現,另一些方法(如clear())甚至放棄了對一致性的要求(ConcurrentHashMap是弱一致性的;

問題:ConcurrentHashMap和Hashtable之間的區別?

那麼Hashtable和ConcurrentHashMap之間的區別是什麼,能夠在多線程環境中使用,但一旦Hashtable的大小變得至關大的性能下降,由於迭代它必須被鎖定更長的時間。
因爲ConcurrentHashMap引入了分段的概念,因此它只有一部分被鎖定才能提供線程安全性. 
總而言之,ConcurrentHashMap僅鎖定Map的某些部分,而Hashtable在執行迭代時鎖定完整映射。 

ConcurrentHashMap 不容許空值或空值同步,可是HashMap容許一個空鍵。

CopyOnWrite容器即寫時複製的容器。通俗的理解是當咱們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,而後新的容器裏添加元素,添加完元素以後,再將原容器的引用指向新的容器。這樣作的好處是咱們能夠對CopyOnWrite容器進行併發的讀,而不須要加鎖,由於當前容器不會添加任何元素。因此CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不一樣的容器。

(2)CopyOnWriteArrayList、CopyOnWriteArraySet

CopyOnWrite容器即寫時複製的容器。通俗的理解是當咱們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,而後新的容器裏添加元素,添加完元素以後,再將原容器的引用指向新的容器。這樣作的好處是咱們能夠對CopyOnWrite容器進行併發的讀,而不須要加鎖,由於當前容器不會添加任何元素。因此CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不一樣的容器。

(四)原子操做(在多線程中能保證原子性)
         AtomicInteger          AtomicLong      AtomicBoolean

i++;    //不是原子操做,此處含有三步(取得i的值,i+1,把i+1後的結果賦值給i)結果將有異常。當一條線程執行到i+1時,尚未賦值,另外一線程i+1並賦值。
AtomicInteger flag = new AtomicInteger();
ai.incrementAndGet();    //能夠用原子方式更新的 int 值。以原子方式將當前值加 1.它只有一步操做,隨便哪一個線程執行都將只有這一步。

(五)併發工具類

1.等待多線程完成的CountDownLatch

假若有這樣一個需求:咱們須要解析一個Excel裏多個sheet的數據,此時能夠考慮使用多線程,每一個線程解析一個sheet裏的數據,等到全部的sheet都解析完以後,程序須要提示解析完成(或者彙總結果)。在這個需求中,要實現主線程等待全部線程完成sheet的解析操做,最簡單的作法是使用join()方法;使用CountDownLatch也能完成;

join方法:

AnalylisExcel implements Runnable{run(//實現解析excel)};

Thread[] threads=new Thread[threadCount];
        for(int i=0;i<threadCount;i++){
            threads[i]=new Thread(new AnalylisExcel("Parser-"+i));
        } 
        for(int i=0;i<threadCount;i++){
            threads[i].start();
        } 
        for(int i=0;i<threadCount;i++){
            threads[i].join();
        } 

使用CountDownLatch

AnalylisExcel implements Runnable{

CountDownLatch countDown=new CountDownLatch(threadCount);

run(//實現解析excel;

countDown.countDown();//注意這裏

)};

Thread[] threads=new Thread[threadCount];
        for(int i=0;i<threadCount;i++){
            threads[i]=new Thread(new AnalylisExcel("Parser-"+i));
        } 
        for(int i=0;i<threadCount;i++){
            threads[i].start();
        } 
countDown.await();//將join改成使用CountDownLatch

2.CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要作的事情是,讓一組線程到達一個屏障(也能夠叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,全部被屏障攔截的線程纔會繼續運行。

問題:CyclicBarrier和CountDownLatch的區別

CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器能夠使用reset()方法重置。因此CyclicBarrier能處理更爲複雜的業務場景。例如,若是計算髮生錯誤,能夠重置計數器,並讓線程從新執行一次。

3.Semaphore能夠用於作流量控制,特別是公用資源有限的應用場景,好比數據庫鏈接。假若有一個需求,要讀取幾萬個文件的數據,由於都是IO密集型任務,咱們能夠啓動幾十個線程併發地讀取,可是若是讀到內存後,還須要存儲到數據庫中,而數據庫的鏈接數只有10個,這時咱們必須控制只有10個線程同時獲取數據庫鏈接保存數據,不然會報錯沒法獲取數據庫鏈接。這個時候,就能夠使用Semaphore來作流量控制,

public class SemaphoreTest { 
    private static final int THREAD_COUNT = 30;
    private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
    private static Semaphore s = new Semaphore(10);
    
    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try { 
                        s.acquire();
                        System.out.println("save data");
                        s.release();
                    } catch (InterruptedException e) {
                    }
                }
            });
        }
        threadPool.shutdown();
    }
}

4.Exchanger(交換者)是一個用於線程間協做的工具類。Exchanger用於進行線程間的數據交換。

private static final Exchanger<String> exgr = new Exchanger<String>();

4、問題彙總

1、sleep() 和 wait() 有什麼區別?

二者最大區別是wait會釋放鎖,而sleep不會釋放鎖,只是暫停做用;

    sleep是線程類(Thread)的方法,致使此線程暫停執行指定時間,給執行機會給其餘線程,可是監控狀態依然保持,到時後會自動恢復。調用sleep不會釋放對象鎖。

    wait是Object類的方法,致使此線程進入等待此對象的等待鎖定池,只有針對此對象發出notify方法(或notifyAll)後本線程才進入對象鎖定池準備得到對象鎖進入運行狀態。調用wait方法致使本線程放棄對象鎖;

2、當一個線程進入一個對象的一個synchronized方法後,其它線程是否可進入此對象的其它方法?

不能,一個對象的一個synchronized方法只能由一個線程訪問。

3、多線程有哪些狀態?

新建、就緒(start方法調用後)、運行、睡眠(sleep方法)、等待(wait方法)、掛起、恢復、阻塞、死亡。

線程同步解決問題

方式一:同步代碼塊

如下是避免同一時間多個線程同時運行致使訂票異常的方法:

    class hello implements Runnable {
         public void run() {
             for ( int i= 0 ;i< 10 ;++i){
                 synchronized ( this ) {
                     if (count> 0 ){
                         try {
                             Thread.sleep( 1000 );
                         } catch (InterruptedException e){
                             e.printStackTrace();
                         }
                         System.out.println(count--);
                     }
                 }
             }
         }
     
         public static void main(String[] args) {
             hello he= new hello();
             Thread h1= new Thread(he);
             Thread h2= new Thread(he);
             Thread h3= new Thread(he);
             h1.start();
             h2.start();
             h3.start();
         }
         private int count= 5 ;
    }


方法二:

【同步方法】

也能夠採用同步方法。

語法格式爲synchronized 方法返回類型方法名(參數列表){

    // 其餘代碼

}

class hello implements Runnable {  
    public void run() {  
        for (int i = 0; i < 10; ++i) {  
            sale();  
        }  
    }    
    public synchronized void sale() {  
        if (count > 0) {  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println(count--);  
        }  
    }  
    public static void main(String[] args) {  
        hello he = new hello();  
        Thread h1 = new Thread(he);  
        Thread h2 = new Thread(he);  
        Thread h3 = new Thread(he);  
        h1.start();  
        h2.start();  
        h3.start(); 
    } 
    private int count = 5; 
}

4.什麼是線程?

線程是操做系統可以進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運做單位。程序員能夠經過它進行多處理器編程,你能夠使用多線程對運算密 集型任務提速。

5. 線程和進程有什麼區別?

線程是進程的子集,一個進程能夠有不少線程,每條線程並行執行不一樣的任務。不一樣的進程使用不一樣的內存空間,而全部的線程共享一片相同的內存空間。別把它和棧內存搞混,每一個線程都擁有單獨的棧內存用來存儲本地數據。

6.如何在Java中實現線程?

在語言層面有兩種方式。一是繼承 java.lang.Thread 類,並實現Runnable中的run()方法;二 是直接調用Runnable接口來重寫run()方法實現線程。

7.用Runnable仍是Thread?

Thread是一個類,在其餘類中沒有繼承才能夠調用;而Runnable是一個接口;一個類中能夠實現多個接口;若是你知道Java不支持類的多重繼承,但容許你調用多個接口。因此若是你要繼承其餘類,固然是調用Runnable接口了。

8.Thread 類中的start() 和 run() 方法有什麼區別?

start()方法被用來啓動新建立的線程,並且start()內部調用了 run()方法,當你直接調用run()方法的時候,只會是在原來的線程中調用,沒有新的線程啓動,start() 方法纔會啓動新線程。

9.什麼是線程安全?Vector是一個線程安全類嗎? (詳見這裏)

若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也 和預期的是同樣的,就是線程安全的。一個線程安全的計數器類的同一個實例對象在被多個線程使用的狀況下也不會出現計算失誤。很顯然你能夠將集合類分紅兩組,線程安全和非線程安全的。Vector 是用同步方法來實現線程安全的, 而和它類似的ArrayList不是線程安全的。

10.Java中如何中止一個線程?

Java提供了很豐富的API但沒有爲中止線程提供API。JDK 1.0原本有一些像stop(), suspend() 和 resume()的控制方法可是因爲潛在的死鎖威脅所以在後續的JDK版本中他們被棄用了,以後Java API的設計者就沒有提供一個兼容且線程安全的方法來中止一個線程。當run() 或者 call() 方法執行完的時候線程會自動結束,若是要手動結束一個線程,你能夠用volatile 布爾變量來退出run()方法的循環或者是取消任務來中斷線程。

11.一個線程運行時發生異常會怎樣?

這是我在一次面試中遇到的一個很刁鑽的Java面試題, 簡單的說,若是異常沒有被捕獲該線程將會中止執行。Thread.UncaughtExceptionHandler是用於處理未捕獲異常形成線程忽然中 斷狀況的一個內嵌接口。當一個未捕獲異常將形成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來 查詢線程的UncaughtExceptionHandler並將線程和異常做爲參數傳遞給handler的uncaughtException()方法 進行處理。

12.如何在兩個線程間共享數據?

你能夠經過共享對象來實現這個目的,或者是使用像阻塞隊列這樣併發的數據結構。用wait和notify方法實現了生產者消費者模型。經過在線程之間共享對象就能夠了,而後經過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞隊列BlockingQueue就是爲線程之間共享數據而設計的

13. Java中notify 和 notifyAll有什麼區別?

notify()方法不能喚醒某個具體的線程,因此只有一個線程在等待的時候它纔有用武之地。而notifyAll()喚醒全部線程並容許他們爭奪鎖確保了至少有一個線程能繼續運行。

14.爲何wait, notify 和 notifyAll這些方法不在thread類裏面?

一個很明顯的緣由是JAVA提供的鎖是對象級的而不是線程級的,每一個對象都有鎖,通 過線程得到。若是線程須要等待某些鎖那麼調用對象中的wait()方法就有意義了。若是wait()方法定義在Thread類中,線程正在等待的是哪一個鎖就不明顯了。簡單的說,因爲wait,notify和notifyAll都是鎖級別的操做,因此把他們定義在Object類中由於鎖屬於對象。

15.爲何wait和notify方法要在同步塊中調用?

主要是由於Java API強制要求這樣作,若是你不這麼作,你的代碼會拋出IllegalMonitorStateException異常。還有一個緣由是爲了不wait和notify之間產生競態條件。

16.wait()方法和notify()/notifyAll()方法在放棄對象監視器時有什麼區別

wait()方法和notify()/notifyAll()方法在放棄對象監視器的時候的區別在於:wait()方法當即釋放對象監視器,notify()/notifyAll()方法則會等待線程剩餘代碼執行完畢纔會放棄對象監視器

17.Java中堆和棧有什麼不一樣?

爲何把這個問題歸類在多線程和併發面試題裏?由於棧是一塊和線程緊密相關的內存區域。每一個線程都有本身的棧內存,用於存儲本地變量,方法參數和棧調用, 一個線程中存儲的變量對其它線程是不可見的。而堆是全部線程共享的一片公用內存區域。對象都在堆裏建立,爲了提高效率線程會從堆中弄一個緩存到本身的棧, 若是多個線程使用該變量就可能引起問題,這時volatile 變量就能夠發揮做用了,它要求線程從主存中讀取變量的值。
18.有三個線程T1,T2,T3,怎麼確保它們按順序執行?

在多線程中有多種方法讓線程按特定順序執行,你能夠用線程類的join()方法在一個線程中啓動另外一個線程,另一個線程完成該線程繼續執行。爲了確保三個線程的順序你應該先啓動最後一個(T3調用T2,T2調用T1),這樣T1就會先完成而T3最後完成。

19.什麼是線程池? 爲何要使用它?

建立線程要花費昂貴的資源和時間,若是任務來了才建立線程那麼響應時間會變長,並且一個進程能建立的線程數有限。爲了不這些問題,在程序啓動的時候就創 建若干線程來響應處理,它們被稱爲線程池,裏面的線程叫工做線程。

20.爲何你應該在循環中檢查等待條件?

處於等待狀態的線程可能會收到錯誤警報和僞喚醒,若是不在循環中檢查等待條件,程序就會在沒有知足結束條件的狀況下退出。所以,當一個等待線程醒來時,不能認爲它原來的等待狀態仍然是有效的,在notify()方法調用以後和等待線程醒來以前這段時間它可能會改變。這就是在循環中使用wait()方法效果更好的緣由。

21. Java中什麼是競態條件? 舉個例子說明。

競態條件會致使程序在併發狀況下出現一些bugs。多線程對一些資源的競爭的時候就會產生競態條件,若是首先要執行的程序競爭失敗排到後面執行了,那麼整個程序就會出現一些不肯定的bugs。這種bugs很難發現並且會重複出現,由於線程間的隨機競爭。一個例子:好比生產和消費兩個進程,正常順序是先執行生產,再有消費,若是消費先執行了,那整個環節就出現問題了。

22.Java中活鎖和死鎖有什麼區別?

活鎖和死鎖相似,不一樣之處在於處於活鎖的線程或進程的狀態是不斷改變的,活鎖能夠認爲是一種特殊的飢餓。一個現實的活鎖例子是兩我的在狹小的走廊碰到,兩我的都試着避讓對方好讓彼此經過,可是由於避讓的方向都同樣致使最後誰都不能經過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態能夠改變可是卻不能繼續執行。

23.怎麼檢測一個線程是否擁有鎖?

我一直不知道咱們居然能夠檢測一個線程是否擁有鎖,直到我參加了一次電話面試。在java.lang.Thread中有一個方法叫holdsLock(),它返回true若是當且僅當當前線程擁有某個具體對象的鎖。。

24.你如何在Java中獲取線程堆棧?

對於不一樣的操做系統,有多種方法來得到Java進程的線程堆棧。當你獲取線程堆棧時,JVM會把全部線程的狀態存到日誌文件或者輸出到控制檯。在 Windows你能夠使用Ctrl + Break組合鍵來獲取線程堆棧,Linux下用kill -3命令。你也能夠用jstack這個工具來獲取,它對線程id進行操做,你能夠用jps這個工具找到id。

25. JVM中哪一個參數是用來控制線程的堆棧大小的?

這個問題很簡單, -Xss參數用來控制線程的堆棧大小。你能夠查看JVM配置列表來了解這個參數的更多信息。

26.如何避免死鎖?

Java多線程中的死鎖
死鎖是指兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。這是一個嚴重的問題,由於死鎖會讓你的程序掛起沒法完成任務,死鎖的發生必須知足如下四個條件:

·         互斥條件:一個資源每次只能被一個進程使用。

·         請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。

·         不剝奪條件:進程已得到的資源,在末使用完以前,不能強行剝奪。

·         循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。

避免死鎖最簡單的方法就是阻止循環等待條件,將系統中全部的資源設置標誌位、排序,規定全部的進程申請資源必須以必定的順序(升序或降序)作操做來避免死鎖。

27.Java內存模型是什麼?

Java內存模型規定和指引Java程序在不一樣的內存架構、CPU和操做系統間有肯定性地行爲。它在多線程的狀況下尤爲重要。Java內存模型對一個線程 所作的變更能被其它線程可見提供了保證,它們之間是先行發生關係。這個關係定義了一些規則讓程序員在併發編程時思路更清晰。好比,先行發生關係確保了:

·         線程內的代碼可以按前後順序執行,這被稱爲程序次序規則。

·         對於同一個鎖,一個解鎖操做必定要發生在時間上後發生的另外一個鎖定操做以前,也叫作管程鎖定規則。

·         前一個對volatile的寫操做在後一個volatile的讀操做以前,也叫volatile變量規則。

·         一個線程內的任何操做必需在這個線程的start()調用以後,也叫做線程啓動規則。

·         一個線程的全部操做都會在線程終止以前,線程終止規則。

·         一個對象的終結操做必需在這個對象構造完成以後,也叫對象終結規則。

·         可傳遞性

28.Java中的volatile 變量是什麼?

volatile是一個特殊的修飾符,只有成員變量才能使用它。在Java併發程序缺乏同步類的狀況下,多線程對成員變量的操做對其它線程是透明的。volatile變量能夠保證下一個讀取操做會在前一個寫操做以後發生,就是上一題的volatile變量規則。

29.什麼是ThreadLocal變量?

ThreadLocal是Java裏一種特殊的變量。每一個線程都有一個ThreadLocal就是每一個線程都擁有了本身獨立的一個變量,它是爲建立代價高昂的對象獲取線程安全的好方法,好比你能夠用ThreadLocal讓SimpleDateFormat變成線程安全的,由於那個類建立代價高昂且每次調用都須要建立不一樣的實例因此不值得在局部範圍使用它,若是爲每一個線程提供一個本身獨有的變量拷貝,將大大提升效率。首先,經過複用減小了代價高昂的對象的建立個數。其次,你在沒有使用高代價的同步或者不變性的狀況下得到了線程安全。線程局部變量的另外一個不錯的例子是 ThreadLocalRandom類,它在多線程環境中減小了建立代價高昂的Random對象的個數。

30. 什麼是FutureTask?

在Java併發程序中FutureTask表示一個能夠取消的異步運算。它有啓動和取消運算、查詢運算是否完成和取回運算結果等方法。只有當運算完成的時 候結果才能取回,若是運算還沒有完成get方法將會阻塞。一個FutureTask對象能夠對調用了Callable和Runnable的對象進行包裝,由 於FutureTask也是調用了Runnable接口因此它能夠提交給Executor來執行。

31. Java中interrupted 和 isInterruptedd方法的區別?

interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除然後者不會。簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態清零。

32.Java中的同步集合與併發集合有什麼區別?

同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在Java1.5以前程序員們只有同步集合來用且在多線程 併發的時候會致使爭用,阻礙了系統的擴展性。Java5介紹了併發集合像ConcurrentHashMap,不只提供線程安全還用鎖分離和內部分區等現 代技術提升了可擴展性。

33.如何寫代碼來解決生產者消費者問題?

在現實中你解決的許多線程問題都屬於生產者消費者模型,就是一個線程生產任務供其它線程進行消費,你必須知道怎麼進行線程間通訊來解決這個問題。比較低級的辦法是用wait和notify來解決這個問題,比較讚的辦法是用Semaphore 或者 BlockingQueue來實現生產者消費者模型。

34.生產者消費者模型的做用是什麼

這個問題很理論,可是很重要:

1)經過平衡生產者的生產能力和消費者的消費能力來提高整個系統的運行效率,這是生產者消費者模型最重要的做用

2)解耦,這是生產者消費者模型附帶的做用,解耦意味着生產者和消費者之間的聯繫少,聯繫越少越能夠獨自發展而不須要收到相互的制約

35.Java中synchronized 和 ReentrantLock 區別?

synchronized是和if、else、for、while同樣的關鍵字,ReentrantLock是類,這是兩者的本質區別。既然ReentrantLock是類,那麼它就提供了比synchronized更多更靈活的特性,能夠被繼承、能夠有方法、能夠有各類各樣的類變量,ReentrantLock比synchronized的擴展性體如今幾點上:

(1)ReentrantLock能夠對獲取鎖的等待時間進行設置,這樣就避免了死鎖

(2)ReentrantLock能夠獲取各類鎖的信息

(3)ReentrantLock能夠靈活地實現多路通知

另外,兩者的鎖機制其實也是不同的。ReentrantLock底層調用的是Unsafe的park方法加鎖,synchronized操做的應該是對象頭中mark word,這點我不能肯定。

36.Thread類中的yield方法有什麼做用?

Yield方法能夠暫停當前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜態方法並且只保證當前線程放棄CPU佔用而不能保證使其它線程必定能佔用CPU,執行yield()的線程有可能在進入到暫停狀態後立刻又被執行。點擊這裏查看更多yield方法的相關內容。

37.Java中ConcurrentHashMap的併發度是什麼?

ConcurrentHashMap把實際map劃分紅若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度得到的,它是 ConcurrentHashMap類構造函數的一個可選參數,默認值爲16,這樣在多線程狀況下就能避免爭用。欲瞭解更多併發度和內部大小調整請閱讀我 的文章How ConcurrentHashMap works in Java

38.Java線程池中submit() 和 execute()方法有什麼區別?

execute()方法其實是Executor中聲明的方法,這個方法是ThreadPoolExecutor的核心方法,經過這個方法能夠向線程池提交一個任務,交由線程池去執行。submit()方法實際上也是調用execute(),只不過它利用了Future來獲取任務執行結果

39.什麼是阻塞式方法?

阻塞式方法是指程序會一直等待該方法完成期間不作其餘事情,ServerSocket的accept()方法就是一直等待客戶端鏈接。這裏的阻塞是指調用 結果返回以前,當前線程會被掛起,直到獲得結果以後纔會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。更多詳細信息請點擊這裏

40.如何在Java中建立Immutable對象?

這個問題看起來和多線程沒什麼關係, 但不變性有助於簡化已經很複雜的併發程序。Immutable對象能夠在沒有同步的狀況下共享,下降了對該對象進行併發訪問時的同步化開銷。但是Java 沒有@Immutable這個註解符,要建立不可變類,要實現下面幾個步驟:經過構造方法初始化全部成員、對變量不要提供setter方法、將全部的成員 聲明爲私有的,這樣就不容許直接訪問這些成員、在getter方法中,不要直接返回對象自己,而是克隆對象,並返回對象的拷貝。個人文章how to make an object Immutable in Java有詳細的教程,看完你能夠充滿自信。

41.Java中的ReadWriteLock是什麼?

首先明確一下,不是說ReentrantLock很差,只是ReentrantLock某些時候有侷限。若是使用ReentrantLock,可能自己是爲了防止線程A在寫數據、線程B在讀數據形成的數據不一致,但這樣,若是線程C在讀數據、線程D也在讀數據,讀數據是不會改變數據的,沒有必要加鎖,可是仍是加鎖了,下降了程序的性能。

由於這個,才誕生了讀寫鎖ReadWriteLock。ReadWriteLock是一個讀寫鎖接口,ReentrantReadWriteLock是ReadWriteLock接口的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨佔的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間纔會互斥,提高了讀寫的性能。

42.volatile 變量和 atomic 變量有什麼不一樣?

這是個有趣的問題。首先,volatile 變量和 atomic 變量看起來很像,但功能卻不同。Volatile變量能夠確保先行關係,即寫操做會發生在後續的讀操做以前, 但它並不能保證原子性。例如用volatile修飾count變量那麼 count++ 操做就不是原子性的。而AtomicInteger類提供的atomic方法可讓這種操做具備原子性如getAndIncrement()方法會原子性 的進行增量操做把當前值加一,其它數據類型和引用變量也能夠進行類似操做。

43. 若是同步塊內的線程拋出異常會發生什麼?

這個問題坑了不少Java程序員,若你能想到鎖是否釋放這條線索來回答還有點但願答對。不管你的同步塊是正常仍是異常退出的,裏面的線程都會釋放鎖,因此對比鎖接口我更喜歡同步塊,由於它不用我花費精力去釋放鎖,該功能能夠在finally block裏釋放鎖實現。

44.單例模式的雙檢鎖是什麼?

這個問題在Java面試中常常被問到,可是面試官對回答此問題的滿意度僅爲50%。一半的人寫不出雙檢鎖還有一半的人說不出它的隱患和Java1.5是如 何對它修正的。它實際上是一個用來建立線程安全的單例的老方法,當單例實例第一次被建立時它試圖用單個鎖進行性能優化,可是因爲太過於複雜在JDK1.4中 它是失敗的,我我的也不喜歡它。不管如何,即使你也不喜歡它可是仍是要了解一下,由於它常常被問到。你能夠查看how double checked locking on Singleton works這篇文章得到更多信息。

45.寫出3條你遵循的多線程最佳實踐

a.給你的線程起個有意義的名字。這樣能夠方便找bug或追蹤。OrderProcessor, QuoteProcessor or TradeProcessor 這種名字比 Thread-1. Thread-2 and Thread-3 好多了,

b.避免鎖定和縮小同步的範圍:鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區。所以相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。

c.多用同步類少用wait 和 notify
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 這些同步類簡化了編碼操做,而用wait和notify很難實現對複雜控制流的控制。

d.多用併發集合少用同步集合
這是另一個容易遵循且受益巨大的最佳實踐,併發集合比同步集合的可擴展性更好,因此在併發編程時使用併發集合效果更好。若是下一次你須要用到map,你應該首先想到用ConcurrentHashMap。

46. 如何強制啓動一個線程?

這個問題就像是如何強制進行Java垃圾回收,目前尚未以爲方法,雖然你能夠使用System.gc()來進行垃圾回收,可是不保證能成功。在Java裏面沒有辦法強制啓動一個線程,它是被線程調度器控制着且Java沒有公佈相關的API。

47.Java中的fork join框架是什麼?

fork join框架是JDK7中出現的一款高效的工具,Java開發人員能夠經過它充分利用現代服務器上的多處理器。它是專門爲了那些能夠遞歸劃分紅許多子模塊 設計的,目的是將全部可用的處理能力用來提高程序的性能。fork join框架一個巨大的優點是它使用了工做竊取算法,能夠完成更多任務的工做線程能夠從其它線程中竊取任務來執行。

48.用Java實現阻塞隊列

這是一個相對艱難的多線程面試問題,它能達到不少的目的。第一,它能夠檢測侯選者是否能實際的用Java線程寫程序;第二,能夠檢測侯選者對併發場景的理解,而且你能夠根據這個問不少問題。若是他用wait()和notify()方法來實現阻塞隊列,你能夠要求他用最新的Java 5中的併發類來再寫一次

import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * 實現了阻塞的take和put方法的阻塞隊列
 *      分別用synchronized 和 wait/notify 實現
 * @author xuexiaolei
 * @version 2017年11月01日
 */
public class MyBlocingQueue<E> {
    private final List list;
    private final int limit;//有大小限制的
 
    public MyBlocingQueue(int limit) {
        this.limit = limit;
        this.list = new LinkedList<E>();
    }
 
    //這是用synchronized寫的,在list空或者滿的時候效率會低,由於會一直輪詢
//    public void put(E e){
//        while(true){
//            synchronized (list){
//                if (list.size() < limit) {
//                    System.out.println("list : " + list.toString());
//                    System.out.println("put : " + e);
//                    list.add(e);
//                    return;
//                }
//            }
//        }
//    }
//    public E take(){
//        while (true) {
//            synchronized (list) {
//                if (list.size() > 0){
//                    System.out.println("list : " + list.toString());
//                    E remove = (E) list.remove(0);
//                    System.out.println("take : " + remove);
//                    return remove;
//                }
//            }
//        }
//    }
 
    //用wait,notify寫的,在list空或者滿的時候效率會高一點,由於wait釋放鎖,而後等待喚醒
    public synchronized void put(E e){
        while (list.size() == limit){
            try {
                wait();
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
        System.out.println("list : " + list.toString());
        System.out.println("put : " + e);
        list.add(e);
        notifyAll();
    }
    public synchronized E take() {
        while (list.size() == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("list : " + list.toString());
        E remove = (E) list.remove(0);
        System.out.println("take : " + remove);
        notifyAll();
        return remove;
    }
 
 
    /********************    下面是測試區       *********************************/
    public static void main(String[] args) {
        final MyBlocingQueue<Integer> myBlocingQueue = new MyBlocingQueue(10);
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            exec.execute(new TestRunnable(myBlocingQueue));
        }
        exec.shutdown();
    }
 
    static class TestRunnable implements Runnable{
        private final MyBlocingQueue<Integer> myBlocingQueue;
 
        TestRunnable(MyBlocingQueue<Integer> myBlocingQueue) {
            this.myBlocingQueue = myBlocingQueue;
        }
 
        @Override
        public void run() {
            Random random = new Random();
            int r = random.nextInt(100);
            //生成隨機數,按照必定比率讀取或者放入,能夠更改!!!
            if (r < 30){
                myBlocingQueue.put(r);
            } else {
                myBlocingQueue.take();
            }
        }
    }
}

49.多線程有什麼用?

1)發揮多核CPU的優點

單核CPU上所謂的"多線程"那是假的多線程,同一時間處理器只會處理一段邏輯,只不過線程之間切換得比較快,看着像多個線程"同時"運行罷了。多核CPU上的多線程纔是真正的多線程,它能讓你的多段邏輯同時工做,多線程,能夠真正發揮出多核CPU的優點來,達到充分利用CPU的目的。

2)防止阻塞

從程序運行效率的角度來看,單核CPU不但不會發揮出多線程的優點,反而會由於在單核CPU上運行多線程致使線程上下文的切換,而下降程序總體的效率。可是單核CPU咱們仍是要應用多線程,就是爲了防止阻塞。試想,若是單核CPU使用單線程,那麼只要這個線程阻塞了,比方說遠程讀取某個數據吧,對端遲遲未返回又沒有設置超時時間,那麼你的整個程序在數據返回回來以前就中止運行了。多線程能夠防止這個問題,多條線程同時運行,哪怕一條線程的代碼執行讀取數據阻塞,也不會影響其它任務的執行。

3)便於建模

這是另一個沒有這麼明顯的優勢了。假設有一個大的任務A,單線程編程,那麼就要考慮不少,創建整個程序模型比較麻煩。可是若是把這個大的任務A分解成幾個小任務,任務B、任務C、任務D,分別創建程序模型,並經過多線程分別運行這幾個任務,那就簡單不少了。

50.Runnable接口和Callable接口的區別

有點深的問題了,也看出一個Java程序員學習知識的廣度。

Runnable接口中的run()方法的返回值是void,它作的事情只是純粹地去執行run()方法中的代碼而已;Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合能夠用來獲取異步執行的結果。

這實際上是頗有用的一個特性,由於多線程相比單線程更難、更復雜的一個重要緣由就是由於多線程充滿着未知性,某條線程是否執行了?某條線程執行了多久?某條線程執行的時候咱們指望的數據是否已經賦值完畢?沒法得知,咱們能作的只是等待這條多線程的任務執行完畢而已。而Callable+Future/FutureTask卻能夠獲取多線程運行的結果,能夠在等待時間太長沒獲取到須要的數據的狀況下取消該線程的任務,真的是很是有用。

51.CyclicBarrier和CountDownLatch的區別

兩個看上去有點像的類,都在java.util.concurrent下,均可以用來表示代碼運行到某個點上,兩者的區別在於:

1)CyclicBarrier的某個線程運行到某個點上以後,該線程即中止運行,直到全部的線程都到達了這個點,全部線程才從新運行;CountDownLatch則不是,某線程運行到某個點上以後,只是給某個數值-1而已,該線程繼續運行。

2)CyclicBarrier只能喚起一個任務,CountDownLatch能夠喚起多個任務。

3) CyclicBarrier可重用,CountDownLatch不可重用,計數值爲0該CountDownLatch就不可再用了。

52.什麼是線程安全

若是你的代碼在多線程下執行和在單線程下執行永遠都能得到同樣的結果,那麼你的代碼就是線程安全的

這個問題有值得一提的地方,就是線程安全也是有幾個級別的:

1)不可變

像String、Integer、Long這些,都是final類型的類,任何一個線程都改變不了它們的值,要改變除非新建立一個,所以這些不可變對象不須要任何同步手段就能夠直接在多線程環境下使用

2)絕對線程安全

無論運行時環境如何,調用者都不須要額外的同步措施。要作到這一點一般須要付出許多額外的代價,Java中標註本身是線程安全的類,實際上絕大多數都不是線程安全的,不過絕對線程安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet

3)相對線程安全

相對線程安全也就是咱們一般意義上所說的線程安全,像Vector這種,add、remove方法都是原子操做,不會被打斷,但也僅限於此,若是有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的狀況下都會出現ConcurrentModificationException,也就是fail-fast機制

4)線程非安全

這個就沒什麼好說的了,ArrayList、LinkedList、HashMap等都是線程非安全的類,點擊這裏瞭解爲何不安全。

53.不可變對象對多線程有什麼幫助

前面有提到過的一個問題,不可變對象保證了對象的內存可見性,對不可變對象的讀取不須要進行額外的同步手段,提高了代碼執行效率。

54.Java中如何獲取到線程dump文件

死循環、死鎖、阻塞、頁面打開慢等問題,打線程dump是最好的解決問題的途徑。所謂線程dump也就是線程堆棧,獲取到線程堆棧有兩步:

1)獲取到線程的pid,能夠經過使用jps命令,在Linux環境下還能夠使用ps -ef | grep java

2)打印線程堆棧,能夠經過使用jstack pid命令,在Linux環境下還能夠使用kill -3 pid

另外提一點,Thread類提供了一個getStackTrace()方法也能夠用於獲取線程堆棧。這是一個實例方法,所以此方法是和具體線程實例綁定的,每次獲取獲取到的是具體某個線程當前運行的堆棧。

55.一個線程若是出現了運行時異常會怎麼樣

若是這個異常沒有被捕獲的話,這個線程就中止執行了。另外重要的一點是:若是這個線程持有某個某個對象的監視器,那麼這個對象監視器會被當即釋放

避免頻繁地建立和銷燬線程,達到線程對象的重用。另外,使用線程池還能夠根據項目靈活地控制併發的數目。點擊這裏學習線程池詳解。

56.怎麼檢測一個線程是否持有對象監視器

 

我也是在網上看到一道多線程面試題才知道有方法能夠判斷某個線程是否持有對象監視器:Thread類提供了一個holdsLock(Object obj)方法,當且僅當對象obj的監視器被某條線程持有的時候纔會返回true,注意這是一個static方法,這意味着"某條線程"指的是當前線程

57.Linux環境下如何查找哪一個線程使用CPU最長

這是一個比較偏實踐的問題,這種問題我以爲挺有意義的。能夠這麼作:

(1)獲取項目的pid,jps或者ps -ef | grep java,這個前面有講過

(2)top -H -p pid,順序不能改變

這樣就能夠打印出當前的項目,每條線程佔用CPU時間的百分比。注意這裏打出的是LWP,也就是操做系統原生線程的線程號,我筆記本山沒有部署Linux環境下的Java工程,所以沒有辦法截圖演示,網友朋友們若是公司是使用Linux環境部署項目的話,能夠嘗試一下。

使用"top -H -p pid"+"jps pid"能夠很容易地找到某條佔用CPU高的線程的線程堆棧,從而定位佔用CPU高的緣由,通常是由於不當的代碼操做致使了死循環。

最後提一點,"top -H -p pid"打出來的LWP是十進制的,"jps pid"打出來的本地線程號是十六進制的,轉換一下,就能定位到佔用CPU高的線程的當前線程堆棧了。

58.Java編程寫一個會致使死鎖的程序

第一次看到這個題目,以爲這是一個很是好的問題。不少人都知道死鎖是怎麼一回事兒:線程A和線程B相互等待對方持有的鎖致使程序無限死循環下去。固然也僅限於此了,問一下怎麼寫一個死鎖的程序就不知道了,這種狀況說白了就是不懂什麼是死鎖,懂一個理論就完事兒了,實踐中碰到死鎖的問題基本上是看不出來的。

真正理解什麼是死鎖,這個問題其實不難,幾個步驟:

1)兩個線程裏面分別持有兩個Object對象:lock1和lock2。這兩個lock做爲同步代碼塊的鎖;

2)線程1的run()方法中同步代碼塊先獲取lock1的對象鎖,Thread.sleep(xxx),時間不須要太多,50毫秒差很少了,而後接着獲取lock2的對象鎖。這麼作主要是爲了防止線程1啓動一會兒就連續得到了lock1和lock2兩個對象的對象鎖

3)線程2的run)(方法中同步代碼塊先獲取lock2的對象鎖,接着獲取lock1的對象鎖,固然這時lock1的對象鎖已經被線程1鎖持有,線程2確定是要等待線程1釋放lock1的對象鎖的

這樣,線程1"睡覺"睡完,線程2已經獲取了lock2的對象鎖了,線程1此時嘗試獲取lock2的對象鎖,便被阻塞,此時一個死鎖就造成了。代碼就不寫了,佔的篇幅有點多,Java多線程7:死鎖這篇文章裏面有,就是上面步驟的代碼實現。

死鎖的案例。

59.怎麼喚醒一個阻塞的線程

若是線程是由於調用了wait()、sleep()或者join()方法而致使的阻塞,能夠中斷線程,而且經過拋出InterruptedException來喚醒它;若是線程遇到了IO阻塞,無能爲力,由於IO是操做系統實現的,Java代碼並無辦法直接接觸到操做系統。

60.什麼是多線程的上下文切換

多線程的上下文切換是指CPU控制權由一個已經正在運行的線程切換到另一個就緒並等待獲取CPU執行權的線程的過程。

61.若是你提交任務時,線程池隊列已滿,這時會發生什麼

這裏區分一下:

1)若是使用的是無界隊列LinkedBlockingQueue,也就是無界隊列的話,不要緊,繼續添加任務到阻塞隊列中等待執行,由於LinkedBlockingQueue能夠近乎認爲是一個無窮大的隊列,能夠無限存聽任務

2)若是使用的是有界隊列好比ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,會根據maximumPoolSize的值增長線程數量,若是增長了線程數量仍是處理不過來,ArrayBlockingQueue繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,默認是AbortPolicy

62.Java中用到的線程調度算法是什麼

搶佔式。一個線程用完CPU以後,操做系統會根據線程優先級、線程飢餓狀況等數據算出一個總的優先級並分配下一個時間片給某個線程執行。

63.Thread.sleep(0)的做用是什麼

這個問題和上面那個問題是相關的,我就連在一塊兒了。因爲Java採用搶佔式的線程調度算法,所以可能會出現某條線程經常獲取到CPU控制權的狀況,爲了讓某些優先級比較低的線程也能獲取到CPU控制權,能夠使用Thread.sleep(0)手動觸發一次操做系統分配時間片的操做,這也是平衡CPU控制權的一種操做。

64.什麼是自旋

不少synchronized裏面的代碼只是一些很簡單的代碼,執行時間很是快,此時等待的線程都加鎖多是一種不太值得的操做,由於線程阻塞涉及到用戶態和內核態切換的問題。既然synchronized裏面的代碼執行得很是快,不妨讓等待鎖的線程不要被阻塞,而是在synchronized的邊界作忙循環,這就是自旋。若是作了屢次忙循環發現尚未得到鎖,再阻塞,這樣多是一種更好的策略。

65.什麼是Java內存模型

1)Java內存模型將內存分爲了主內存和工做內存。類的狀態,也就是類之間共享的變量,是存儲在主內存中的,每次Java線程用到這些主內存中的變量的時候,會讀一次主內存中的變量,並讓這些內存在本身的工做內存中有一份拷貝,運行本身線程代碼的時候,用到這些變量,操做的都是本身工做內存中的那一份。在線程代碼執行完畢以後,會將最新的值更新到主內存中去

2)定義了幾個原子操做,用於操做主內存和工做內存中的變量

3)定義了volatile變量的使用規則

4)happens-before,即先行發生原則,定義了操做A必然先行發生於操做B的一些規則,好比在同一個線程內控制流前面的代碼必定先行發生於控制流後面的代碼、一個釋放鎖unlock的動做必定先行發生於後面對於同一個鎖進行鎖定lock的動做等等,只要符合這些規則,則不須要額外作同步措施,若是某段代碼不符合全部的happens-before規則,則這段代碼必定是線程非安全的

66.什麼是CAS

CAS,全稱爲Compare and Swap,即比較-替換。假設有三個操做數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,纔會將內存值修改成B並返回true,不然什麼都不作並返回false。固然CAS必定要volatile變量配合,這樣才能保證每次拿到的變量是主內存中最新的那個值,不然舊的預期值A對某條線程來講,永遠是一個不會變的值A,只要某次CAS操做失敗,永遠都不可能成功。更多CAS詳情請點擊這裏學習。

67.什麼是AQS

簡單說一下AQS,AQS全稱爲AbstractQueuedSychronizer,翻譯過來應該是抽象隊列同步器。

若是說java.util.concurrent的基礎是CAS的話,那麼AQS就是整個Java併發包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS實際上以雙向隊列的形式鏈接全部的Entry,比方說ReentrantLock,全部等待的線程都被放在一個Entry中並連成雙向隊列,前面一個線程使用ReentrantLock好了,則雙向隊列實際上的第一個Entry開始運行。

AQS定義了對雙向隊列全部的操做,而只開放了tryLock和tryRelease方法給開發者使用,開發者能夠根據本身的實現重寫tryLock和tryRelease方法,以實現本身的併發功能。

68.單例模式的線程安全性

老生常談的問題了,首先要說的是單例模式的線程安全意味着:某個類的實例在多線程環境下只會被建立一次出來。單例模式有不少種的寫法,我總結一下:

1)餓漢式單例模式的寫法:線程安全

2)懶漢式單例模式的寫法:非線程安全

3)雙檢鎖單例模式的寫法:線程安全

69.Semaphore有什麼做用

Semaphore就是一個信號量,它的做用是限制某段代碼塊的併發數。Semaphore有一個構造函數,能夠傳入一個int型整數n,表示某段代碼最多隻有n個線程能夠訪問,若是超出了n,那麼請等待,等到某個線程執行完畢這段代碼塊,下一個線程再進入。由此能夠看出若是Semaphore構造函數中傳入的int型整數n=1,至關於變成了一個synchronized了。

70.Hashtable的size()方法中明明只有一條語句"return count",爲何還要作同步?

這是我以前的一個困惑,不知道你們有沒有想過這個問題。某個方法中若是有多條語句,而且都在操做同一個類變量,那麼在多線程環境下不加鎖,勢必會引起線程安全問題,這很好理解,可是size()方法明明只有一條語句,爲何還要加鎖?

關於這個問題,在慢慢地工做、學習中,有了理解,主要緣由有兩點:

1)同一時間只能有一條線程執行固定類的同步方法,可是對於類的非同步方法,能夠多條線程同時訪問。因此,這樣就有問題了,可能線程A在執行Hashtable的put方法添加數據,線程B則能夠正常調用size()方法讀取Hashtable中當前元素的個數,那讀取到的值可能不是最新的,可能線程A添加了完了數據,可是沒有對size++,線程B就已經讀取size了,那麼對於線程B來講讀取到的size必定是不許確的。而給size()方法加了同步以後,意味着線程B調用size()方法只有在線程A調用put方法完畢以後才能夠調用,這樣就保證了線程安全性

2)CPU執行代碼,執行的不是Java代碼,這點很關鍵,必定得記住。Java代碼最終是被翻譯成機器碼執行的,機器碼纔是真正能夠和硬件電路交互的代碼。即便你看到Java代碼只有一行,甚至你看到Java代碼編譯以後生成的字節碼也只有一行,也不意味着對於底層來講這句語句的操做只有一個。一句"return count"假設被翻譯成了三句彙編語句執行,一句彙編語句和其機器碼作對應,徹底可能執行完第一句,線程就切換了。

71.線程類的構造方法、靜態塊是被哪一個線程調用的

這是一個很是刁鑽和狡猾的問題。請記住:線程類的構造方法、靜態塊是被new這個線程類所在的線程所調用的,而run方法裏面的代碼纔是被線程自身所調用的。

若是說上面的說法讓你感到困惑,那麼我舉個例子,假設Thread2中new了Thread1,main函數中new了Thread2,那麼:

1)Thread2的構造方法、靜態塊是main線程調用的,Thread2的run()方法是Thread2本身調用的

2)Thread1的構造方法、靜態塊是Thread2調用的,Thread1的run()方法是Thread1本身調用的

72.同步方法和同步塊,哪一個是更好的選擇

同步塊,這意味着同步塊以外的代碼是異步執行的,這比同步整個方法更提高代碼的效率。請知道一條原則:同步的範圍越小越好

藉着這一條,我額外提一點,雖然說同步的範圍越少越好,可是在Java虛擬機中仍是存在着一種叫作鎖粗化的優化方法,這種方法就是把同步範圍變大。這是有用的,比方說StringBuffer,它是一個線程安全的類,天然最經常使用的append()方法是一個同步方法,咱們寫代碼的時候會反覆append字符串,這意味着要進行反覆的加鎖->解鎖,這對性能不利,由於這意味着Java虛擬機在這條線程上要反覆地在內核態和用戶態之間進行切換,所以Java虛擬機會將屢次append方法調用的代碼進行一個鎖粗化的操做,將屢次的append的操做擴展到append方法的頭尾,變成一個大的同步塊,這樣就減小了加鎖-->解鎖的次數,有效地提高了代碼執行的效率。

73.高併發、任務執行時間短的業務怎樣使用線程池?併發不高、任務執行時間長的業務怎樣使用線程池?併發高、業務執行時間長的業務怎樣使用線程池?

這是我在併發編程網上看到的一個問題,把這個問題放在最後一個,但願每一個人都能看到而且思考一下,由於這個問題很是好、很是實際、很是專業。關於這個問題,我的見解是:

1)高併發、任務執行時間短的業務,線程池線程數能夠設置爲CPU核數+1,減小線程上下文的切換

2)併發不高、任務執行時間長的業務要區分開看:

a)假如是業務時間長集中在IO操做上,也就是IO密集型的任務,由於IO操做並不佔用CPU,因此不要讓全部的CPU閒下來,能夠加大線程池中的線程數目,讓CPU處理更多的業務

b)假如是業務時間長集中在計算操做上,也就是計算密集型任務,這個就沒辦法了,和(1)同樣吧,線程池中的線程數設置得少一些,減小線程上下文的切換

c)併發高、業務執行時間長,解決這種類型任務的關鍵不在於線程池而在於總體架構的設計,看看這些業務裏面某些數據是否能作緩存是第一步,增長服務器是第二步,至於線程池的設置,設置參考其餘有關線程池的文章。最後,業務執行時間長的問題,也可能須要分析一下,看看能不能使用中間件對任務進行拆分和解耦。

74.如何避免死鎖

在有些狀況下死鎖是能夠避免的。三種用於避免死鎖的技術:

  1. 加鎖順序(線程按照必定的順序加鎖)
  2. 加鎖時限(線程嘗試獲取鎖的時候加上必定的時限,超過期限則放棄對該鎖的請求,並釋放本身佔有的鎖)
  3. 死鎖檢測
相關文章
相關標籤/搜索