一、IO和多線程專題
1.介紹下進程和線程的關系
進程:一個獨立的正在執行的程序
(資料圖片僅供參考)
線程:一個進程的最基本的執行單位,執行路徑
多進程:在操作系統中,同時運行多個程序
多進程的好處:可以充分利用CPU,提高CPU的使用率
多線程:在同一個進程(應用程序)中同時執行多個線程
多線程的好處:提高進程的執行使用率,提高了CPU的使用率
注意:
在同一個時間點一個CPU中只可能有一個線程在執行
多線程不能提高效率、反而會降低效率,但是可以提高CPU的使用率
一個進程如果有多條執行路徑,則稱為多線程程序
Java虛擬機的啟動至少開啟了兩條線程,主線程和垃圾回收線程
一個線程可以理解為進程的子任務
2.說說Java中實現多線程的幾種方法
Thread對象就是一個線程
創建線程的常用三種方式:
繼承Thread類
實現Runnable接口
實現Callable接口(JDK1.5>=)
線程池方式創建
通過繼承Thread類或者實現Runnable接口、Callable接口都可以實現多線程,不過實現Runnable接口與實現Callable接口的方式基本相同,只是Callable接口里定義的方法返回值,可以聲明拋出異常而已。因此將實現Runnable接口和實現Callable接口歸為一種方式。這種方式與繼承Thread方式之間的主要差別如下。
繼承Thread類
實現的步驟:
創建Thread類的子類
重寫run方法
創建線程對象
啟動線程
案例代碼
注意點:
啟動線程是使用start方法而不是run方法
線程不能啟動多次,如果要創建多個線程,那么就需要創建多個Thread對象
實現Runnable接口
在第一種實現方式中,我們是將線程的創建和線程執行的業務都封裝在了Thread對象中,我們可以通過Runable接口來實現線程程序代碼和數據有效的分離。
實現的步驟:
創建Runable的實現類
重寫run方法
創建Runable實例對象(通過實現類來實現)
創建Thread對象,并把第三部的Runable實現作為Thread構造方法的參數
啟動線程
實現Runable接口的好處:
1. 可以避免Java單繼承帶來的局限性
2. 適合多個相同的程序代碼處理同一個資源的情況,把線程同程序的代碼和數據有效的分離,較好的體現了面向對象的設計思想
Callable的方式
  前面我們介紹的兩種創建線程的方式都是重寫run方法,而且run方法是沒有返回結果的,也就是main方法是不知道開啟的線程什么時候開始執行,什么時候結束執行,也獲取不到對應的返回結果。而且run方法也不能把可能產生的異常拋出。在JDK1.5之后推出了通過實現Callable接口的方式來創建新的線程,這種方式可以獲取對應的返回結果
實現Runnable接口和實現Callable接口的區別:
Runnable是自從java1.1就有了,而Callable是1.5之后才加上去的
Callable規定的方法是call(),Runnable規定的方法是run()
Callable的任務執行后可返回值,而Runnable的任務是不能返回值(是void)
call方法可以拋出異常,run方法不可以
運行Callable任務可以拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,并檢索計算的結果。通過Future對象可以了解任務執行情況,可取消任務的執行,還可獲取執行結果。
加入線程池運行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。
其實Callable接口底層的實現就是對Runable接口實現的封裝,線程啟動執行的也是Runable接口實現中的run方法,只是在run方法中有調用call方法罷了
3.如何停止一個正在運行的線程
設置標志位:如果線程的run方法中執行的是一個重復執行的循環,可以提供一個標記來控制循環是否繼續
利用中斷標志位: 在線程中有個中斷的標志位,默認是false,當我們顯示的調用 interrupted方法或者isInterrupted方法是會修改標志位為true。我們可以利用此來中斷運行的線程。
利用InterruptedException:如果線程因為執行join(),sleep()或者wait()而進入阻塞狀態,此時要想停止它,可以讓他調用interrupt(),程序會拋出InterruptedException異常。我們利用這個異??梢詠斫K止線程。
4.介紹下線程中的常用方法
1.start方法
start方法是我們開啟一個新的線程的方法,但是并不是直接開啟,而是告訴CPU我已經準備好了,快點運行我,這是啟動一個線程的唯一入口。
2.run方法
線程的線程體,當一個線程開始運行后,執行的就是run方法里面的代碼,我們不能直接通過線程對象來調用run方法。因為這并沒有產生一個新的線程。僅僅只是一個普通對象的方法調用。
3.getName方法
獲取線程名稱的方法
4.優先級
我們創建的多個線程的執行順序是由CPU決定的。Java中提供了一個線程調度器來監控程序中啟動后進入就緒狀態的所有的線程,優先級高的線程會獲取到比較多
運行機會
大家會發現,設置了優先級后輸出的結果和我們預期的并不一樣,這是為什么呢?優先級在CPU調動線程執行的時候會是一個參考因數,但不是決定因數,
5.sleep方法
將當前線程暫定指定的時間,
6.isAlive
獲取線程的狀態。
輸出結果
7.join
調用某線程的該方法,將當前線程和該線程合并,即等待該線程結束,在恢復當前線程的運行
輸出結果:
8.yield
讓出CPU,當前線程進入就緒狀態
9.wait和notify/notifyAll
阻塞和喚醒的方法,是Object中的方法,我們在數據同步的時候會介紹到
5.介紹下線程的生命周期
生命周期:對象從創建到銷毀的全過程
線程的生命周期:線程對象(Thread)從開始到銷毀的全過程
線程的狀態:
創建 ?Thread對象
就緒狀態 ?執行start方法后線程進入可運行的狀態
運行狀態 CPU運行
阻塞狀態 ?運行過程中被中斷(等待阻塞,對象鎖阻塞,其他阻塞)
終止狀態 ?線程執行完成
6.為什么wait, notify和notifyAll這些方法不在thread類里面?
明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需要等待某些鎖那么調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個鎖就不明顯了。簡單的說,由于wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因為鎖屬于對象。
7.為什么wait和notify方法要在同步塊中調用?
1.只有在調用線程擁有某個對象的獨占鎖時,才能夠調用該對象的wait(),notify()和notifyAll()方法。2.如果你不這么做,你的代碼會拋出IllegalMonitorStateException異常。3.還有一個原因是為了避免wait和notify之間產生競態條件。??wait()方法強制當前線程釋放對象鎖。這意味著在調用某對象的wait()方法之前,當前線程必須已經獲得該對象的鎖。因此,線程必須在某個對象的同步方法或同步代碼塊中才能調用該對象的wait()方法。??在調用對象的notify()和notifyAll()方法之前,調用線程必須已經得到該對象的鎖。因此,必須在某個對象的同步方法或同步代碼塊中才能調用該對象的notify()或notifyAll()方法。??調用wait()方法的原因通常是,調用線程希望某個特殊的狀態(或變量)被設置之后再繼續執行。調用notify() 或notifyAll()方法的原因通常是,調用線程希望告訴其他等待中的線程:"特殊狀態已經被設置"。這個狀態作為線程間通信的通道,它必須是一個可變的共享狀態(或變量)。
8.synchronized和ReentrantLock的區別
相似點:??這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個線程獲得了對象鎖,進入了同步塊,其他訪問該同步塊的線程都必須阻塞在同步塊外面等待,而進行線程阻塞和喚醒的代價是比較高的.區別:??這兩種方式最大區別就是對于Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之后提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成。
Synchronized進過編譯,會在同步塊的前后分別形成monitorenter和monitorexit這個兩個字節碼指令。在執行monitorenter指令時,首先要嘗試獲取對象鎖。如果這個對象沒被鎖定,或者當前線程已經擁有了那個對象鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計算器為0時,鎖就被釋放了。如果獲取對象鎖失敗,那當前線程就要阻塞,直到對象鎖被另一個線程釋放為止。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級功能,主要有以下3項:
1.等待可中斷,持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,這相當于Synchronized來說可以避免出現死鎖的情況。
2.公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock默認的構造函數是創建的非公平鎖,可以通過參數true設為公平鎖,但公平鎖表現的性能不是很好。
3.鎖綁定多個條件,一個ReentrantLock對象可以同時綁定對個對象
9.什么是線程安全
線程安全就是說多線程訪問同一段代碼,不會產生不確定的結果。??如果你的代碼在多線程下執行和在單線程下執行永遠都能獲得一樣的結果,那么你的代碼就是線程安全的。這個問題有值得一提的地方,就是線程安全也是有幾個級別的:(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等都是線程非安全的類
10.Thread類中yield方法的作用
yield方法可以暫停當前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜態方法而且只保證當前線程放棄CPU占用而不能保證使其它線程一定能占用CPU,執行yield()的線程有可能在進入到暫停狀態后馬上又被執行。
11.常用的線程池有哪些
new SingleThreadExecutor:創建一個單線程的線程池,此線程池保證所有任務的執行順序按照任務的提交順序執行。new FixedThreadPool:創建固定大小的線程池,每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。new CachedThreadPool:創建一個可緩存的線程池,此線程池不會對線程池大小做限制,線程池大小完全依賴于操作系統(或者說JVM)能夠創建的最大線程大小。new ScheduledThreadPool:創建一個大小無限的線程池,此線程池支持定時以及周期性執行任務的求。
12. 簡述一下你對線程池的理解
如果問到了這樣的問題,可以展開的說一下線程池如何用、線程池的好處、線程池的啟動策略合理利用線程池能夠帶來三個好處。第一:降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。第三:提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
參數含義:
線程池工作原理:
提交一個任務到線程池中,線程池的處理流程如下:
判斷線程池里的核心線程是否都在執行任務,如果不是(核心線程空閑或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。
線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程。
判斷線程池里的線程是否都處于工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。
13.線程池的拒絕策略有哪些?
主要有4種拒絕策略:
AbortPolicy:直接丟棄任務,拋出異常,這是默認策略
CallerRunsPolicy:只用調用者所在的線程來處理任務
DiscardOldestPolicy:丟棄等待隊列中最舊的任務,并執行當前任務
DiscardPolicy:直接丟棄任務,也不拋出異常
14.線程安全需要保證幾個基本特性?
原子性,簡單說就是相關操作不會中途被其他線程干擾,一般通過同步機制實現。可見性,是一個線程修改了某個共享變量,其狀態能夠立即被其他線程知曉,通常被解釋為將線程本地狀態反映到主內存上,volatile就是負責保證可見性的。有序性,是保證線程內串行語義,避免指令重排等。
15.說下線程間是如何通信的?
線程之間的通信有兩種方式:共享內存和消息傳遞。
共享內存在共享內存的并發模型里,線程之間共享程序的公共狀態,線程之間通過寫-讀內存中的公共狀態來隱式進行通信。典型的共享內存通信方式,就是通過共享對象進行通信。
例如線程A與線程B之間如果要通信的話,那么就必須經歷下面兩個步驟:1.線程A把本地內存A更新過得共享變量刷新到主內存中去。2.線程B到主內存中去讀取線程A之前更新過的共享變量。消息傳遞在消息傳遞的并發模型里,線程之間沒有公共狀態,線程之間必須通過明確的發送消息來顯式進行通信。在Java中典型的消息傳遞方式,就是wait()和notify(),或者BlockingQueue
16.說說ThreadLocal的原理
ThreadLocal可以理解為線程本地變量,他會在每個線程都創建一個副本,那么在線程之間訪問內部副本變量就行了,做到了線程之間互相隔離,相比于synchronized的做法是用空間來換時間。
ThreadLocal有一個靜態內部類ThreadLocalMap,ThreadLocalMap又包含了一個Entry數組,Entry本身是一個弱引用,他的key是指向ThreadLocal的弱引用,Entry具備了保存key value鍵值對的能力。
弱引用的目的是為了防止內存泄露,如果是強引用那么ThreadLocal對象除非線程結束否則始終無法被回收,弱引用則會在下一次GC的時候被回收。
但是這樣還是會存在內存泄露的問題,假如key和ThreadLocal對象被回收之后,entry中就存在key為null,但是value有值的entry對象,但是永遠沒辦法被訪問到,同樣除非線程結束運行。
但是只要ThreadLocal使用恰當,在使用完之后調用remove方法刪除Entry對象,實際上是不會出現這個問題的。
17.解釋下:同步、異步、阻塞、非阻塞
同步和異步指的是:當前線程是否需要等待方法調用執行完畢。
阻塞和非阻塞指的是:當前接口數據還未準備就緒時,線程是否被阻塞掛起
同步&異步其實是處于框架這種高層次維度來看待的,而阻塞&非阻塞往往針對底層的系統調用方面來抉擇,也就是說兩者是從不同維度來考慮的。
這四個概念兩兩組合,會形成4個新的概念,如下:
同步阻塞:客戶端發送請求給服務端,此時服務端處理任務時間很久,則客戶端則被服務端堵塞了,所以客戶端會一直等待服務端的響應,此時客戶端不能做其他任何事,服務端也不會接受其他客戶端的請求。這種通信機制比較簡單粗暴,但是效率不高。
同步非阻塞:客戶端發送請求給服務端,此時服務端處理任務時間很久,這個時候雖然客戶端會一直等待響應,但是服務端可以處理其他的請求,過一會回來處理原先的。這種方式很高效,一個服務端可以處理很多請求,不會在因為任務沒有處理完而堵著,所以這是非阻塞的。
異步阻塞:客戶端發送請求給服務端,此時服務端處理任務時間很久,但是客戶端不會等待服務器響應,它可以做其他的任務,等服務器處理完畢后再把結果響應給客戶端,客戶端得到回調后再處理服務端的響應。這種方式可以避免客戶端一直處于等待的狀態,優化了用戶體驗,其實就是類似于網頁里發起的ajax異步請求。
異步非阻塞:客戶端發送請求給服務端,此時服務端處理任務時間很久,這個時候的任務雖然處理時間會很久,但是客戶端可以做其他的任務,因為他是異步的,可以在回調函數里處理響應;同時服務端是非阻塞的,所以服務端可以去處理其他的任務,如此,這個模式就顯得非常的高效了。
18.什么是BIO?
BIO: 同步并阻塞,服務器實現一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,沒處理完之前此線程不能做其他操作(如果是單線程的情況下,我傳輸的文件很大呢?),當然可以通過線程池機制改善。
BIO方式 適用于連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,并發局限于應用中JDK1.4以前的唯一選擇,但程序直觀簡單易理解。
19.什么是NIO?
NIO: 同步非阻塞,服務器實現一個連接一個線程,即客戶端發送的連接請求都會注冊到多路復用器上,多復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。
NIO方式 適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,并發局限于應用中,編程比較復雜,JDK1.4之后開始支持。
20.什么是AIO?
AIO: 異步非阻塞,服務器實現模式為一個有效請求一個線程,客戶端的I/O請求都是由操作系統先完成了再通知服務器應用去啟動線程進行處理,AIO方式使用于連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用操作系統參與并發操作,編程比較復雜,JDK1.7之后開始支持。
AIO屬于NIO包中的類實現,其實 IO主要分為BIO和NIO,AIO只是附加品,解決IO不能異步的實現在以前很少有Linux系統支持AIO,Windows的IOCP就是該AIO模型。但是現在的服務器一般都是支持AIO操作