Java 基礎(chǔ)面試寶典(自己總結(jié))

一、多線程

說明下線程的狀態(tài)

java中的線程一共有 5 種狀態(tài)。

  1. NEW:這種情況指的是,通過 New 關(guān)鍵字創(chuàng)建了 Thread 類(或其子類)的對象

  2. RUNNABLE:這種情況指的是 Thread 類的對象調(diào)用了 start() 方法,這時的線程就等待時間片輪轉(zhuǎn)到自己這,以便獲得 CPU;第二種情況是線程在處于 RUNNING 狀態(tài)時并沒有運行完自己的 run 方法,時間片用完之后回到 RUNNABLE 狀態(tài);還有種情況就是處于 BLOCKED 狀態(tài)的線程結(jié)束了當前的 BLOCKED 狀態(tài)之后重新回到 RUNNABLE 狀態(tài)。

  3. RUNNING:這時的線程正在被 CPU 執(zhí)行任務(wù)。

  1. BLOCKED:阻塞狀態(tài)是線程因為某種原因放棄 CPU 使用權(quán),暫時停止運行。直到線程進入就緒狀態(tài),才有機會轉(zhuǎn)到運行狀態(tài)。阻塞的情況分三種:

    • 等待阻塞:運行的線程執(zhí)行 wait() 方法,JVM 會把該線程放入等待池中。
    • 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則 JVM 會把該線程放入鎖池中。
    • 其他阻塞:運行的線程執(zhí)行 sleep() 或 join() 方法,或者發(fā)出了 I/O 請求時,JVM 會把該線程置為阻塞狀態(tài)。當 sleep() 狀態(tài)超時、join() 等待線程終止或者超時、或者 I/O 處理完畢時,線程重新轉(zhuǎn)入就緒狀態(tài)。
  1. DEAD:線程執(zhí)行完了或者因異常退出了 run() 方法,該線程結(jié)束生命周期。

sleep 和 wait 的區(qū)別

區(qū)別一

==sleep 是 Thread 類的方法,是線程用來控制自身流程的==,比如有一個要報時的線程,每一秒中打印出一個時間,那么我就需要在print方法前面加上一個sleep讓自己每隔一秒執(zhí)行一次。就像個鬧鐘一樣。

==wait 是 Object 類的方法,用來線程間的通信==,這個方法會使當前擁有該對象鎖的進程等待直到其他線程調(diào)用 notify 方法時再醒來,不過你也可以給他指定一個時間,自動醒來。這個方法主要是用走不同線程之間的調(diào)度的。

區(qū)別二

調(diào)用 sleep 方法不會釋放鎖,調(diào)用 wait 方法會釋放當前線程的鎖。

區(qū)別三

在使用域上, wait 必須在同步方法內(nèi)使用,即必須在獲取鎖之后調(diào)用,否則會報出 IllegalMonitorStateException 異常。而 sleep 可以在任意地方使用。

synchronized 的實現(xiàn)原理

synchronized 代碼塊是通過 monitorenter 和 monitorexit 指令實現(xiàn)的。synchronized 方法雖然在 vm 字節(jié)碼層面并沒有任何特別的指令來實現(xiàn)被 synchronized 修飾的方法,而是在 Class 文件的方法表中將該方法的 access_flags 字段中的 synchronized 標志位置1,表示該方法是同步方法。鎖的實現(xiàn)有偏向鎖、輕量級鎖和重量級鎖,其中偏向鎖和輕量級鎖是 JDK 針對鎖的優(yōu)化措施。在多線程的競爭下鎖會升級,依次從偏向鎖 -> 輕量級鎖 -> 重量級鎖,這里的鎖只能升級但不能降級。在 Java 對象頭中的 Mark Word 中存儲了關(guān)于鎖的標志位,其中:無鎖和偏向鎖為 00, 輕量級鎖為 01,重量級鎖為 10。

  • 引入偏向鎖主要目的是:為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑(在無競爭的情況下把整個同步都消除掉,連 CAS 操作都不做了)。

  • 引入輕量級鎖的主要目的是:在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗(在無競爭的情況下使用 CAS 操作去消除同步使用的互斥量)。

  • 重量級鎖通過對象內(nèi)部的監(jiān)視器(monitor)實現(xiàn),其中 monitor 的本質(zhì)是依賴于底層操作系統(tǒng)的 Mutex Lock 實現(xiàn),操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。

鎖的優(yōu)缺點對比如下

優(yōu)點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級的差距。 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 適用于只有一個線程訪問同步塊場景。
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應(yīng)速度。 如果始終得不到鎖競爭的線程使用自旋會消耗CPU。 追求響應(yīng)時間。同步塊執(zhí)行速度非???。
重量級鎖 線程競爭不使用自旋,不會消耗CPU。 線程阻塞,響應(yīng)時間緩慢。 追求吞吐量。同步塊執(zhí)行速度較長。

此外,jdk 1.6 對鎖的實現(xiàn)引入了大量的優(yōu)化,如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化等技術(shù)來減少鎖操作的開銷。

自旋鎖

所謂自旋鎖,就是讓該線程等待一段時間,不會被立即掛起,看持有鎖的線程是否會很快釋放鎖。

適應(yīng)自旋鎖

自適應(yīng)就意味著自旋的次數(shù)不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定。==對于同一個鎖,上一個線程如果自旋成功了,那么下次自旋的次數(shù)會更加多==,因為虛擬機認為既然上次成功了,那么此次自旋也很有可能會再次成功,那么它就會允許自旋等待持續(xù)的次數(shù)更多。==反之,如果對于某個鎖,很少有自旋能夠成功的,那么在以后要獲得這個鎖的時候自旋的次數(shù)會減少甚至省略掉自旋過程,以免浪費處理器資源==。

鎖消除

在有些情況下,JVM 檢測到不可能存在共享數(shù)據(jù)競爭,這時 JVM 會對這些同步鎖進行鎖消除。鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持。

鎖粗化

將多個連續(xù)的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖,來減少頻繁的加鎖和釋放鎖帶來的性能損耗。

volatile 關(guān)鍵字

volatile 關(guān)鍵字,具有兩個特性:1. 內(nèi)存的可見性,2. 禁止指令重排序優(yōu)化。

內(nèi)存可見性是指:被 volatile 關(guān)鍵字修飾的變量,當線程要對這個變量執(zhí)行的寫操作,都不會寫入本地緩存,而是直接刷入主內(nèi)存中。當線程讀取被 volatile 關(guān)鍵字修飾的變量時,也是直接從主內(nèi)存中讀取。(簡單的說,一個線程修改的狀態(tài)對另一個線程是可見的)。注意:volatile 不能保證原子性。

禁止指令重排序優(yōu)化:有volatile修飾的變量,賦值后多執(zhí)行了一個 “l(fā)oad addl $0x0, (%esp)” 操作,這個操作相當于一個內(nèi)存屏障,保證指令重排序時不會把后面的指令重排序到內(nèi)存屏障之前的位置。

詳細說明指令重排序和內(nèi)存屏障

說到指令重排序,那么就要說到 as-if-serial 語義和 happens-before 規(guī)則了。

as-if-serial 的語義是指:不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守“as-if-serial”語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。

happens-before 規(guī)則主要有一下 8 個

★1. 程序次序規(guī)則(Program Order Rule):在一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準確地說應(yīng)該是控制流順序而不是代碼順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。

★2. 監(jiān)視器鎖定規(guī)則(Monitor Lock Rule):一個 unlock 操作先行發(fā)生于后面對同一個對象鎖的lock操作。這里強調(diào)的是同一個鎖,而“后面”指的是時間上的先后順序,如發(fā)生在其他線程中的 lock 操作。

★3. volatile變量規(guī)則(Volatile Variable Rule):對一個 volatile 變量的寫操作發(fā)生于后面對這個變量的讀操作,這里的“后面”也指的是時間上的先后順序。

  1. 線程啟動規(guī)則(Thread Start Rule):Thread 獨享的 start() 方法先行于此線程的每一個動作。

  2. 線程終止規(guī)則(Thread Termination Rule):線程中的每個操作都先行發(fā)生于對此線程的終止檢測,我們可以通過 Thread.join() 方法結(jié)束、Thread.isAlive() 的返回值檢測到線程已經(jīng)終止執(zhí)行。

  3. 線程中斷規(guī)則(Thread Interruption Rule):對線程 interrupte() 方法的調(diào)用優(yōu)先于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過 Thread.interrupted() 方法檢測線程是否已中斷。

  4. 對象終結(jié)原則(Finalizer Rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的 finalize() 方法的開始。

★8. 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。

我認為比較重要的規(guī)則是:程序次序規(guī)則、監(jiān)視器鎖定規(guī)則、volatile 變量規(guī)則和傳遞性。

除此之外,Java 內(nèi)存模型對 volatile 和 final 的語義做了擴展。對 volatile 語義的擴展保證了 volatile 變量在一些情況下不會重排序,volatile 的 64 位變量 double 和 long 的讀取和賦值操作都是原子的。

對于基本類型 final 域,編譯器和處理器要遵守兩個重排序規(guī)則:

  1. 在構(gòu)造函數(shù)內(nèi)對一個final域的寫入,與隨后把這個被構(gòu)造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。(StoreStore屏障)
  2. 初次讀一個包含final域的對象的引用,與隨后初次讀這個final域,這兩個操作之間不能重排序。(LoadLoad屏障)

對于引用類型,寫final域的重排序規(guī)則對編譯器和處理器增加了如下約束:

  1. 在構(gòu)造函數(shù)內(nèi)對一個final引用的對象的成員域的寫入,與隨后在構(gòu)造函數(shù)外把這個被構(gòu)造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

關(guān)于內(nèi)存屏障

內(nèi)存屏障,是一種CPU指令,用于控制特定條件下的重排序和內(nèi)存可見性問題。Java編譯器也會根據(jù)內(nèi)存屏障的規(guī)則禁止重排序。

內(nèi)存屏障可以被分為以下幾種類型

  1. LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。

  2. StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。

  3. LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。

  4. StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。 在大多數(shù)處理器的實現(xiàn)中,這個屏障是個萬能屏障,兼具其它三種內(nèi)存屏障的功能。

什么是 CAS?

CAS,compare and swap的縮寫,中文翻譯成比較并交換。CAS指令在Intel CPU上稱為CMPXCHG指令,它的作用是將指定內(nèi)存地址的內(nèi)容與所給的某個值相比,如果相等,則將其內(nèi)容替換為指令中提供的新值,如果不相等,則更新失敗。

從內(nèi)存領(lǐng)域來說這是樂觀鎖,因為它在對共享變量更新之前會先比較當前值是否與更新前的值一致,如果是,則更新,如果不是,則無限循環(huán)執(zhí)行(稱為自旋),直到當前值與更新前的值一致為止,才執(zhí)行更新。

CAS 有 3 個操作數(shù),內(nèi)存值 V,舊的預(yù)期值 A,要修改的新值 B。當且僅當預(yù)期值 A 和內(nèi)存值 V 相同時,將內(nèi)存值 V 修改為 B,否則什么都不做。

CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環(huán)時間長開銷大和只能保證一個共享變量的原子操作

  1. ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,
    那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。
    在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。

    從Java1.5開始JDK的atomic包里提供了一個類 AtomicStampedReference 來解決ABA問題。
    這個類的compareAndSet方法作用是首先檢查當前引用是否等于預(yù)期引用,并且當前標志是否等于預(yù)期標志,如果全部相等,
    則以原子方式將該引用和該標志的值設(shè)置為給定的更新值。

  1. 循環(huán)時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執(zhí)行開銷。
  1. 只能保證一個共享變量的原子操作。當對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作,
    但是對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,可以把多個共享變量合并成一個共享變量來操作。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象里來進行CAS操作。

AQS 的實現(xiàn)原理(JUC 的并發(fā)包中的很多類都是基于這個模板方法的,很重要)

AbstractQueuedSynchronizer(簡稱AQS),隊列同步器,是用來構(gòu)建鎖或者其他同步組建的基礎(chǔ)框架。AQS的核心思想是基于volatile int state這樣的volatile變量,配合Unsafe工具對其原子性的操作來實現(xiàn)對當前鎖狀態(tài)進行修改。同步器內(nèi)部依賴一個FIFO的雙向隊列來完成資源獲取線程的排隊工作。

AQS 的鎖在內(nèi)部實現(xiàn)上分為獨占模式和共享模式兩種。

獨占鎖: 鎖在一個時間點只能被一個線程占有。ReentrantLock 和 ReentrantReadWriteLock.Writelock 是獨占鎖。

共享鎖:同一個時候能夠被多個線程獲取的鎖,能被共享的鎖。JUC包中ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch和Semaphore都是共享鎖。

AQS 獨占模式獲取鎖的過程

  1. 調(diào)用自定義同步器的tryAcquire()嘗試直接去獲取資源,如果成功則直接返回

  2. 沒成功,則addWaiter()將該線程封裝成 Node 對象并加入等待隊列的尾部,并標記為獨占模式

  3. acquireQueued()使線程在等待隊列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源。獲取到資源后才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false

  4. 如果線程在等待過程中被中斷過,它是不響應(yīng)的。只是獲取資源后才再進行自我中斷selfInterrupt(),將中斷補上

AQS 獨占模式釋放鎖的過程

  1. release() 根據(jù) tryRelease() 的返回值來判斷該線程是否已經(jīng)完成釋放掉資源了,如果已經(jīng)徹底釋放資源(state=0),要返回true,否則返回false。

  2. 如果已經(jīng)徹底釋放資源,則調(diào)用 unparkSuccessor 方法,喚醒等待隊列里的下一個線程。

AQS 共享模式獲取鎖的過程

  1. 調(diào)用子類實現(xiàn)的 tryAcquireShared() 方法嘗試獲取資源,當前線程獲取資源成功后,如果還有剩余資源,那么還會喚醒后面的線程來嘗試獲取資源。

  2. 失敗則通過doAcquireShared()進入等待隊列park(),直到被unpark()/interrupt()并成功獲取到資源才返回。整個等待過程也是忽略中斷的。

AQS 共享模式釋放鎖的過程

  1. 調(diào)用子類實現(xiàn)的 tryReleaseShared() 方法釋放資源,如果成功釋放資源,那么就調(diào)用 doReleaseShared() 方法喚醒后面的線程。

ReentrantLock 實現(xiàn)原理

ReentrantLock 是基于 AQS 實現(xiàn)的獨占鎖。內(nèi)部分為公平鎖和非公平鎖,默認使用的是非公平鎖。

公平鎖是指:線程獲取鎖的順序和調(diào)用lock的順序一樣,F(xiàn)IFO。

非公平鎖是指:線程獲取鎖的順序和調(diào)用lock的順序無關(guān),全憑運氣。 ReentrantLock 默認使用非公平鎖是基于性能考慮,公平鎖為了保證線程規(guī)規(guī)矩矩地排隊,需要增加阻塞和喚醒的時間開銷。如果直接插隊獲取非公平鎖,跳過了對隊列的處理,速度會更快。

公平鎖和非公平鎖在釋放鎖的步驟上沒有區(qū)別,只是在加鎖的時候有區(qū)別,區(qū)別如下:

非公平鎖加鎖的步驟

忽視隊列前面的等待線程,上來直接基于CAS嘗試將state(鎖數(shù)量)從0設(shè)置為1

A、如果設(shè)置成功,設(shè)置當前線程為獨占鎖的線程;

B、如果設(shè)置失敗,還會再獲取一次鎖數(shù)量,

B1、如果鎖數(shù)量為0,再基于CAS嘗試將state(鎖數(shù)量)從0設(shè)置為1一次,如果設(shè)置成功,設(shè)置當前線程為獨占鎖的線程;

B2、如果鎖數(shù)量不為0或者上邊的嘗試又失敗了,查看當前線程是不是已經(jīng)是獨占鎖的線程了,如果是,則將當前的鎖數(shù)量+1;如果不是,則將該線程封裝在一個Node內(nèi),并加入到等待隊列中去。等待被其前一個線程節(jié)點喚醒。

公平鎖加鎖的步驟

A1、獲取一次鎖數(shù)量

B1、如果鎖數(shù)量為0,如果當前線程是等待隊列中的頭節(jié)點,基于CAS嘗試將state(鎖數(shù)量)從0設(shè)置為1一次,如果設(shè)置成功,設(shè)置當前線程為獨占鎖的線程;

B2、如果鎖數(shù)量不為0或者當前線程不是等待隊列中的頭節(jié)點或者上邊的嘗試又失敗了,查看當前線程是不是已經(jīng)是獨占鎖的線程了,如果是,則將當前的鎖數(shù)量+1;如果不是,則將該線程封裝在一個Node內(nèi),并加入到等待隊列中去。等待被其前一個線程節(jié)點喚醒。

線程池的實現(xiàn)原理

在 Java 中一共有 5 種線程池,他們分別是:CachedThreadPool,F(xiàn)ixedThreadPool,SingleThreadExecutor,ScheduleThreadPool,ScheduledThreadPoolExecutor。它們分別是通過 Executors 的靜態(tài)方法創(chuàng)建出來的。而他們底層是通過 ThreadPoolExecutor 類創(chuàng)建出來的。創(chuàng)建線程池時會傳入以下參數(shù),他們分別是:

  1. corePoolSize:核心線程池的大小,在線程池被創(chuàng)建之后,其實里面是沒有線程的。(當然,調(diào)用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法會預(yù)創(chuàng)建線程,而不用等著任務(wù)的到來)。當有任務(wù)進來的時候,才會創(chuàng)建線程。當線程池中的線程數(shù)量達到corePoolSize之后,就把任務(wù)放到緩存隊列當中。(就是 workQueue )。

  2. maximumPoolSize:最大線程數(shù)量是多少。它標志著這個線程池的最大線程數(shù)量。如果沒有最大數(shù)量,當創(chuàng)建的線程數(shù)量達到了 某個極限值,到最后內(nèi)存肯定就爆掉了。

  3. keepAliveTime:當線程沒有任務(wù)時,最多保持的時間,超過這個時間就被終止了,默認值 60 秒。默認情況下,只有線程池中線程數(shù)量大于 corePoolSize 時,keepAliveTime 值才會起作用。也就說,只有在線程池線程數(shù)量超出 corePoolSize 了。我們才會把超時的空閑線程給停止掉。否則就保持線程池中有 corePoolSize 個線程就可以了。

  4. Unit:參數(shù)keepAliveTime的時間單位,就是 TimeUnit類當中的幾個屬性。

  5. workQueue:用來存儲待執(zhí)行任務(wù)的隊列,不同的線程池它的隊列實現(xiàn)方式不同(因為這關(guān)系到排隊策略的問題)比如有以下幾種:

    • ArrayBlockingQueue:基于數(shù)組的隊列,創(chuàng)建時需要指定大小。

    • LinkedBlockingQueue:基于鏈表的隊列,如果沒有指定大小,則默認值是 Integer.MAX_VALUE。(newFixedThreadPool和newSingleThreadExecutor使用的就是這種隊列),吞吐量通常要高于ArrayBlockingQuene。

    • SynchronousQueue:一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài),吞吐量通常要高于LinkedBlockingQuene(newCachedThreadPool使用的就是這種隊列)。

  6. threadFactory:線程工廠,用來創(chuàng)建線程。通過自定義的線程工廠可以給每個新建的線程設(shè)置一個具有識別度的線程名。

  7. Handler:拒絕執(zhí)行任務(wù)時的策略,一般來講有以下四種策略:

    • ThreadPoolExecutor.AbortPolicy (默認的執(zhí)行策略)丟棄任務(wù),并拋出 RejectedExecutionException 異常。

    • ThreadPoolExecutor.CallerRunsPolicy:該任務(wù)被線程池拒絕,由調(diào)用 execute 方法的線程執(zhí)行該任務(wù)。

    • ThreadPoolExecutor.DiscardOldestPolicy : 拋棄隊列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)。

    • ThreadPoolExecutor.DiscardPolicy,丟棄任務(wù),不過也不拋出異常。

幾種線程池的比較

2.1 CachedThreadPool

優(yōu)點

工作線程的創(chuàng)建數(shù)量幾乎沒有限制(其實也有限制的,數(shù)目為Interger. MAX_VALUE), 這樣可靈活的往線程池中添加線程。

如果長時間沒有往線程池中提交任務(wù),即如果工作線程空閑了指定的時間(默認為1分鐘),則該工作線程將自動終止。終止后,如果你又提交了新的任務(wù),則線程池重新創(chuàng)建一個工作線程。

缺點

在使用CachedThreadPool時,一定要注意控制任務(wù)的數(shù)量,否則,由于大量線程同時運行,很有會造成系統(tǒng)癱瘓。

2.2 FixedThreadPool

創(chuàng)建一個指定工作線程數(shù)量的線程池。每當提交一個任務(wù)就創(chuàng)建一個工作線程,如果工作線程數(shù)量達到線程池初始的最大數(shù),則將提交的任務(wù)存入到池隊列中。定長線程池的大小最好根據(jù)系統(tǒng)資源進行設(shè)置如Runtime.getRuntime().availableProcessors()

優(yōu)點

FixedThreadPool是一個典型且優(yōu)秀的線程池,它具有線程池提高程序效率和節(jié)省創(chuàng)建線程時所耗的開銷的優(yōu)點。

缺點

但是,在線程池空閑時,即線程池中沒有可運行任務(wù)時,它不會釋放工作線程,還會占用一定的系統(tǒng)資源。

2.3 SingleThreadExecutor

創(chuàng)建一個單線程化的Executor,即只創(chuàng)建唯一的工作者線程來執(zhí)行任務(wù),它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行。如果這個線程異常結(jié)束,會有另一個取代它,保證順序執(zhí)行。單工作線程最大的特點是可保證順序地執(zhí)行各個任務(wù),并且在任意給定的時間不會有多個線程是活動的。

2.4 ScheduleThreadPool

創(chuàng)建一個定長的線程池,而且支持定時的以及周期性的任務(wù)執(zhí)行,支持定時及周期性任務(wù)執(zhí)行。

線程池任務(wù)的提交流程

  1. 如果當前線程池線程數(shù)目小于 corePoolSize(核心池還沒滿呢),那么就創(chuàng)建一個新線程去處理任務(wù)。

  2. 如果核心池已經(jīng)滿了,來了一個新的任務(wù)后,會嘗試將其添加到任務(wù)隊列中,如果成功,則等待空閑線程將其從隊列中取出并且執(zhí)行,如果隊列已經(jīng)滿了,則繼續(xù)下一步。

  3. 此時,如果線程池線程數(shù)量 小于 maximumPoolSize,則創(chuàng)建一個新線程執(zhí)行任務(wù),否則,那就說明線程池到了最大飽和能力了,沒辦法再處理了,此時就按照拒絕策略來處理。(就是構(gòu)造函數(shù)當中的 Handler 對象)。

  4. 如果線程池的線程數(shù)量大于 corePoolSize,則當某個線程的空閑時間超過了 keepAliveTime,那么這個線程就要被銷毀了,直到線程池中線程數(shù)量不大于 corePoolSize 為止。

線程池的 shutdown 和 shutdownNow 方法的區(qū)別是什么

  1. shutdown 設(shè)置狀態(tài)為 SHUTDOWN,而 shutdownNow 設(shè)置狀態(tài)為 STOP

  2. shutdown 只中斷空閑的線程,已提交的任務(wù)可以繼續(xù)被執(zhí)行,而 shutdownNow 中斷所有線程

  3. shutdown 無返回值,shutdownNow 返回任務(wù)隊列中還未執(zhí)行的任務(wù)

線程池的參數(shù)要如何配置?(待整理給出)

http://www.cnblogs.com/waytobestcoder/p/5323130.html

單利模式DCL(雙重檢查)失效問題,如何寫出一個高效的單利模式?

單例模式,針對延遲加載法的同步實現(xiàn)所產(chǎn)生的性能低的問題,我們可以采用DCL,即雙重檢查加鎖(Double Check Lock)的方法來避免每次調(diào)用getInstance()方法時都同步。實現(xiàn)方式如下:

public class LazySingleton {
    private int someField;

    private static LazySingleton instance;

    private LazySingleton() {
        this.someField = new Random().nextInt(200)+1;         // (1)
    }

    public static LazySingleton getInstance() {
        if (instance == null) {                               // (2)
            synchronized(LazySingleton.class) {               // (3)
                if (instance == null) {                       // (4)
                    instance = new LazySingleton();           // (5)
                }
            }
        }
        return instance;                                      // (6)
    }

    public int getSomeField() {
        return this.someField;                                // (7)
    }
}

但是這種雙重檢查模式會在多線程的情況下出現(xiàn)線程不安全的情況,即調(diào)用 getInstance().getSomeField() 方法得到的值可能是其默認值,并非是初始化后的值。

詳細的原因參考筆記,篇幅的問題不展開

針對這種情況的解決方案

  1. 最簡單而且安全的解決方法是使用static內(nèi)部類的思想,它利用的思想是:一個類直到被使用時才被初始化,而類初始化的過程是非并行的,這些都有JLS保證。
public class Singleton {

  private Singleton() {}

  private static class InstanceHolder {
   private static final Singleton instance = new Singleton();
  }

  public static Singleton getSingleton() {
    return InstanceHolder.instance;
  }
}
  1. 另外,可以將 instance 聲明為 volatile。即
    private volatile static LazySingleton instance;
    這樣我們便可以得到,線程Ⅰ的語句(5) -> 語線程Ⅱ的句(2),根據(jù)單線程規(guī)則,線程Ⅰ的語句(1) -> 線程Ⅰ的語句(5)和語線程Ⅱ的句(2) -> 語線程Ⅱ的句(7),再根據(jù)傳遞規(guī)則就有線程Ⅰ的語句(1) -> 語線程Ⅱ的句(7),這表示線程Ⅱ能夠觀察到線程Ⅰ在語句(1)時對someFiled的寫入值,程序能夠得到正確的行為。

  2. 利用 java5 的 final 語義,final 變量一旦在構(gòu)造函數(shù)中設(shè)置完成(前提是在構(gòu)造函數(shù)中沒有泄露this引用),其它線程必定會看到在構(gòu)造函數(shù)中設(shè)置的值。可以將 LazySingleton 的 someField 變量設(shè)置成 final,這樣在 java5 中就能夠正確運行了。

線程同步與阻塞的關(guān)系?同步一定阻塞嗎?阻塞一定同步嗎?

  1. 同步和異步的關(guān)系

同步和異步關(guān)注的是消息通信機制 (synchronous communication/ asynchronous communication)所謂同步,就是在發(fā)出一個調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用就不返回。但是一旦調(diào)用返回,就得到返回值了。換句話說,就是由調(diào)用者主動等待這個調(diào)用的結(jié)果。而異步則是相反,調(diào)用在發(fā)出之后,這個調(diào)用就直接返回了,所以沒有返回結(jié)果。換句話說,當一個異步過程調(diào)用發(fā)出后,調(diào)用者不會立刻得到結(jié)果。而是在調(diào)用發(fā)出后,被調(diào)用者通過狀態(tài)、通知來通知調(diào)用者,或通過回調(diào)函數(shù)處理這個調(diào)用。

  1. 阻塞與非阻塞的關(guān)系

阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時的狀態(tài)。

阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當前線程會被掛起。調(diào)用線程只有在得到結(jié)果之后才會返回。非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會阻塞當前線程。

  1. 總結(jié)

同步是個過程,阻塞是線程的一種狀態(tài)。多個線程操作共享變量時可能會出現(xiàn)競爭。這時需要同步來防止兩個以上的線程同時進入臨界區(qū),在這個過程中,后進入臨界區(qū)的線程將阻塞,等待先進入的線程走出臨界區(qū)。

同步不一定發(fā)生阻塞,線程同步的時候,需要協(xié)調(diào)推進速度,相互等待和相互喚醒,因此會發(fā)生阻塞,等待另一個事件發(fā)生。線程阻塞原因很多,等待輸入輸出或者其他事件發(fā)生,不一定是因為同步產(chǎn)生的。阻塞也不一定同步,可能是線程等待著某個輸入或者輸出導(dǎo)致的阻塞。

同步和異步有什么區(qū)別?

同步和異步關(guān)注的是消息通信機制 (synchronous communication/ asynchronous communication)所謂同步,就是在發(fā)出一個調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用就不返回。但是一旦調(diào)用返回,就得到返回值了。換句話說,就是由調(diào)用者主動等待這個調(diào)用的結(jié)果。而異步則是相反,調(diào)用在發(fā)出之后,這個調(diào)用就直接返回了,所以沒有返回結(jié)果。換句話說,當一個異步過程調(diào)用發(fā)出后,調(diào)用者不會立刻得到結(jié)果。而是在調(diào)用發(fā)出后,被調(diào)用者通過狀態(tài)、通知來通知調(diào)用者,或通過回調(diào)函數(shù)處理這個調(diào)用。

Thread類中的yield方法有什么作用?

yield 方法可以暫停當前正在執(zhí)行的線程對象,讓其它有相同優(yōu)先級的線程執(zhí)行。它是一個靜態(tài)方法而且只保證當前線程放棄 CPU 占用而不能保證使其它線程一定能占用 CPU,執(zhí)行 yield() 的線程有可能在進入到暫停狀態(tài)后馬上又被執(zhí)行。

產(chǎn)生死鎖的四個必要條件,如何避免死鎖

  1. 互斥條件:一個資源每次只能被一個進程使用。
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
  4. 循環(huán)等待條件:若干進程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

避免死鎖只要破壞其中的一個條件就可以了,其中最簡單的方法就是阻止循環(huán)等待條件。

一個線程運行時發(fā)生異常會怎樣?

如果異常沒有被捕獲該線程將會停止執(zhí)行。Thread.UncaughtExceptionHandler 是用于處理未捕獲異常造成線程突然中斷情況的一個內(nèi)嵌接口。當一個未捕獲異常將造成線程中斷的時候 JVM 會使用 Thread.getUncaughtExceptionHandler() 來查詢線程的 UncaughtExceptionHandler 并將線程和異常作為參數(shù)傳遞給 handler 的 uncaughtException() 方法進行處理。在開發(fā)中如果想要處理這種未捕獲的異常,可以實現(xiàn)這個接口,并在 ThreadFactory 中設(shè)置異常處理的 handler。

ThreadLocal 的原理

http://www.cnblogs.com/lqminn/p/3751206.html

簡單說 ThreadLocal 就是一種以空間換時間的做法,在每個Thread里面維護了一個 ThreadLocal.ThreadLocalMap,它以ThreadLocal為鍵,以屬于該線程的資源副本為值。==我們可以這樣看待ThreadLocal:ThreadLocal是為一組線程維護資源副本的對象,通過它,可以為每一個線程創(chuàng)建資源副本,也可以正確獲得屬于某一線程的資源副本。==

應(yīng)用場景:當很多線程需要多次使用同一個對象,并且需要該對象具有相同初始化值的時候最適合使用ThreadLocal。

ThreadLocal 是否會造成內(nèi)存泄漏? 答案是不會:每一個線程對資源副本都有一個隱式引用,當一個線程運行結(jié)束銷毀時,所有的資源副本都是可以被垃圾回收的。也就是說當線程被回收的時候,那么 ThreadLocal 變量也會被回收。

你如何在Java中獲取線程堆棧?如何分析線程堆棧?(要精確到命令,重寫)

  1. 如何獲取

不同的操作系統(tǒng)有不同的方式,在Windows你可以使用 Ctrl + Break 組合鍵來獲取線程堆棧,Linux 下用 kill -3 命令。你也可以用 jstack 這個工具來獲取,它對線程 id 進行操作,你可以用 jps 這個工具找到 id。

  1. 如何分析

Java中synchronized 和 ReentrantLock 有什么不同?

synchronized 是關(guān)鍵字, ReentrantLock 是類,這是它們本質(zhì)的不同。 和synchronized相比,ReentrantLock用起來會復(fù)雜一些。在基本的加鎖和解鎖上,兩者是一樣的,所以無特殊情況下,推薦使用synchronized。ReentrantLock的優(yōu)勢在于它更靈活、更強大,增加了輪訓(xùn)、超時、中斷等高級功能。

怎么檢測一個線程是否持有對象監(jiān)視器

Thread 類提供了一個 holdsLock(Object obj) 方法,當且僅當對象 obj 的監(jiān)視器被某條線程持有的時候才會返回 true,注意這是一個 static 方法,這意味著"某條線程"指的是當前線程。

CyclicBarrier 和 CountDownLatch 的區(qū)別

CyclicBarrier 主要用于一組線程之間的相互等待,而 CountDownLatch 一般用于一組線程等待另一組些線程。

  1. CountDownLatch

    一個同步輔助類,在完成一組正在其他線程中執(zhí)行的操作之前,它允許一個或多個線程一直等待。

    用給定的計數(shù)初始化 CountDownLatch。由于調(diào)用了 countDown() 方法,所以在當前計數(shù)到達 0 之前,await 方法會一直受阻塞。

    之后,會釋放所有等待的線程,await 的所有后續(xù)調(diào)用都將立即返回。這種現(xiàn)象只出現(xiàn)一次——計數(shù)無法被重置。

  1. CyclicBarrier

    • CyclicBarrier初始化時規(guī)定一個數(shù)目,然后計算調(diào)用了CyclicBarrier.await()進入等待的線程數(shù)。當線程數(shù)達到了這個數(shù)目時,所有進入等待狀態(tài)的線程被喚醒并繼續(xù)。

    • CyclicBarrier就象它名字的意思一樣,可看成是個障礙, 所有的線程必須到齊后才能一起通過這個障礙。

    • CyclicBarrier初始時還可帶一個Runnable的參數(shù), 此Runnable任務(wù)在CyclicBarrier的數(shù)目達到后,所有其它線程被喚醒前被執(zhí)行

CyclicBarrier 的實現(xiàn)原理

CyclicBarrier 的字面意思是可循環(huán)(Cyclic)使用的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最后一個線程到達屏障時,屏障才會開門,所有被屏障攔截的線程才會繼續(xù)干活。線程進入屏障通過CyclicBarrier的await()方法。

CyclicBarrier 默認的構(gòu)造方法是 CyclicBarrier(int parties),其參數(shù)表示屏障攔截的線程數(shù)量,每個線程調(diào)用 await 方法告訴 CyclicBarrier 我已經(jīng)到達了屏障,然后當前線程被阻塞。

CyclicBarrier 還提供一個更高級的構(gòu)造函數(shù) CyclicBarrier(int parties, Runnable barrierAction),用于在線程到達屏障時,優(yōu)先執(zhí)行 barrierAction 這個 Runnable 對象,方便處理更復(fù)雜的業(yè)務(wù)場景。

實現(xiàn)原理:在 CyclicBarrier 的內(nèi)部定義了一個 Lock 對象,每當一個線程調(diào)用 CyclicBarrier 的 await() 方法時,將剩余攔截的線程數(shù)減 1,然后判斷剩余攔截數(shù)是否為 0,如果不是,進入 Lock 對象的條件隊列等待。如果是,執(zhí)行 barrierAction 對象的 Runnable 方法,然后將鎖的條件隊列中的所有線程放入鎖等待隊列中,這些線程會依次的獲取鎖、釋放鎖,接著先從 await() 方法返回,再從 CyclicBarrier 的 await 方法中返回。當最后一個線程到達屏障點,也就是執(zhí)行 dowait 方法時,會在 return 0 返回之前調(diào)用 finally 塊中的 breakBarrier 方法。

==CycliBarrier 對象可以重復(fù)使用,重用之前應(yīng)當調(diào)用 CyclicBarrier 對象的 reset 方法。==

CountDownLatch 實現(xiàn)原理

java 中的阻塞隊列詳細說明

http://ifeve.com/java-blocking-queue/

什么是阻塞隊列

阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:在隊列為空時,獲取元素的線程會等待隊列變?yōu)榉强?。當隊列滿時,存儲元素的線程會等待隊列可用。阻塞隊列常用于生產(chǎn)者和消費者的場景,生產(chǎn)者是往隊列里添加元素的線程,消費者是從隊列里拿元素的線程。阻塞隊列就是生產(chǎn)者存放元素的容器,而消費者也只從容器里拿元素。

BlockingQueue 具有 4 組不同的方法用于插入、移除以及對隊列中的元素進行檢查。如果請求的操作不能得到立即執(zhí)行的話,每個方法的表現(xiàn)也不同。這些方法如下:

[圖片上傳失敗...(image-c4d148-1573020669224)]

四組不同的行為方式解釋

  • 拋異常:如果試圖的操作無法立即執(zhí)行,拋一個異常。
  • 特定值:如果試圖的操作無法立即執(zhí)行,返回一個特定的值(常常是 true / false)。
  • 阻塞:如果試圖的操作無法立即執(zhí)行,該方法調(diào)用將會發(fā)生阻塞,直到能夠執(zhí)行。
  • 超時:如果試圖的操作無法立即執(zhí)行,該方法調(diào)用將會發(fā)生阻塞,直到能夠執(zhí)行,但等待時間不會超過給定值。返回一個特定值以告知該操作是否成功(典型的是true / false)。

無法向一個 BlockingQueue 中插入 null。如果你試圖插入 null,BlockingQueue 將會拋出一個 NullPointerException。

JDK7提供了7個阻塞隊列,分別是

  • ArrayBlockingQueue :一個由數(shù)組結(jié)構(gòu)組成的有界阻塞隊列。
  • LinkedBlockingQueue :一個由鏈表結(jié)構(gòu)組成的有界阻塞隊列。
  • PriorityBlockingQueue :一個支持優(yōu)先級排序的無界阻塞隊列。
  • DelayQueue:一個使用優(yōu)先級隊列實現(xiàn)的無界阻塞隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。
  • LinkedTransferQueue:一個由鏈表結(jié)構(gòu)組成的無界阻塞隊列。
  • LinkedBlockingDeque:一個由鏈表結(jié)構(gòu)組成的雙向阻塞隊列。

BlockingQueue 是個接口,你需要使用它的實現(xiàn)之一來使用 BlockingQueue,java.util.concurrent 包下具有以下 BlockingQueue 接口的實現(xiàn)類:

  • ArrayBlockingQueue:ArrayBlockingQueue 是一個有界的阻塞隊列,其內(nèi)部實現(xiàn)是將對象放到一個數(shù)組里。有界也就意味著,它不能夠存儲無限多數(shù)量的元素。它有一個同一時間能夠存儲元素數(shù)量的上限。你可以在對其初始化的時候設(shè)定這個上限,但之后就無法對這個上限進行修改了(譯者注:因為它是基于數(shù)組實現(xiàn)的,也就具有數(shù)組的特性:一旦初始化,大小就無法修改)。

  • DelayQueue:DelayQueue 對元素進行持有直到一個特定的延遲到期。注入其中的元素必須實現(xiàn) java.util.concurrent.Delayed 接口。

  • LinkedBlockingQueue:LinkedBlockingQueue 內(nèi)部以一個鏈式結(jié)構(gòu)(鏈接節(jié)點)對其元素進行存儲。如果需要的話,這一鏈式結(jié)構(gòu)可以選擇一個上限。如果沒有定義上限,將使用 Integer.MAX_VALUE 作為上限。

  • PriorityBlockingQueue:PriorityBlockingQueue 是一個無界的并發(fā)隊列。它使用了和類 java.util.PriorityQueue 一樣的排序規(guī)則。你無法向這個隊列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必須實現(xiàn) java.lang.Comparable 接口。因此該隊列中元素的排序就取決于你自己的 Comparable 實現(xiàn)。

  • SynchronousQueue:SynchronousQueue 是一個特殊的隊列,它的內(nèi)部同時只能夠容納單個元素。如果該隊列已有一元素的話,試圖向隊列中插入一個新元素的線程將會阻塞,直到另一個線程將該元素從隊列中抽走。同樣,如果該隊列為空,試圖向隊列中抽取一個元素的線程將會阻塞,直到另一個線程向隊列中插入了一條新的元素。據(jù)此,把這個類稱作一個隊列顯然是夸大其詞了。它更多像是一個匯合點。

阻塞隊列原理:其實阻塞隊列實現(xiàn)阻塞同步的方式很簡單,使用的就是是lock鎖的多條件(condition)阻塞控制。使用BlockingQueue封裝了根據(jù)條件阻塞線程的過程,而我們就不用關(guān)心繁瑣的await/signal操作了。

ReadWriteLock 是什么

ReadWriteLock 是一個讀寫鎖接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一個具體實現(xiàn),實現(xiàn)了讀寫的分離,讀鎖是共享的,寫鎖是獨占的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的性能。

Linux 環(huán)境下如何查找哪個線程使用 CPU 最長

  1. 獲取項目的pid,jps或者ps -ef | grep java

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

這樣就可以打印出當前的項目,每條線程占用CPU時間的百分比。注意這里打出的是LWP,也就是操作系統(tǒng)原生線程的線程號。打出來的LWP是十進制的,"jps pid"打出來的本地線程號是十六進制的,轉(zhuǎn)換一下,就能定位到占用CPU高的線程的當前線程堆棧了。

使用"top -H -p pid"+"jps pid"可以很容易地找到某條占用CPU高的線程的線程堆棧,從而定位占用CPU高的原因,一般是因為不當?shù)拇a操作導(dǎo)致了死循環(huán)。

Thread.sleep(0) 的作用是什么(要弄懂)

由于Java采用搶占式的線程調(diào)度算法,因此可能會出現(xiàn)某條線程常常獲取到CPU控制權(quán)的情況,為了讓某些優(yōu)先級比較低的線程也能獲取到CPU控制權(quán),可以使用Thread.sleep(0)手動觸發(fā)一次操作系統(tǒng)分配時間片的操作,這也是平衡CPU控制權(quán)的一種操作。

Java 內(nèi)存模型

概括總結(jié)

JMM 是 Java 程序?qū)€程如何交互的統(tǒng)一的約定協(xié)議。Java 內(nèi)存模型是圍繞著并發(fā)編程中原子性、可見性、有序性這三個特征來建立的。原子性:一個操作是最小單元,不可分割。可見性:一個線程的修改對另一個線程可見。

在 JMM 中規(guī)定了 Happens-Before 順序: 保證了一個線程的操作結(jié)果能夠?qū)α硪粋€線程可見。

synchronized 關(guān)鍵字提供互斥區(qū)和內(nèi)存可見性, 防止重排序。

volatile 提供內(nèi)存可見性,防止重排序,保證 64 位元素(double、long)的原子性讀寫。

對 final 語義的增強,使得被 final 修飾的變量或者引用在特定的情況下不能被重排序。

https://liuzhengyang.github.io/2017/05/12/javamemorymodel/


Java Memory Model 簡稱 JMM, 是一系列的 Java 虛擬機平臺對開發(fā)者提供的多線程環(huán)境下的內(nèi)存可見性、是否可以重排序等問題的無關(guān)具體平臺的統(tǒng)一的保證。Java 內(nèi)存模型是圍繞著并發(fā)編程中原子性、可見性、有序性這三個特征來建立的。

插入內(nèi)存屏障

單處理器上由于會保證透明的順序一致性,所以并不需要明確的插入內(nèi)存屏障。

在多處理器情況下,基于上面的規(guī)則,可以在 volatile 字段、synchronized 關(guān)鍵字的處理上增加屏障來滿足內(nèi)存模型的規(guī)則。

最保守的策略是在volatile的前后都加上所有的屏障,但是這樣在大多數(shù)情況都是不必要且重復(fù)的,所以再以volatile字段通常讀多寫少的假設(shè),可以得出以下一種策略供編譯器開發(fā)者參考:

  1. volatile store前插入LoadStore;StoreStore屏障
  2. 所有final字段寫入后但在構(gòu)造器返回前插入StoreStore
  3. volatile store后插入StoreLoad屏障
  4. 在volatile load后插入LoadLoad和LoadStore屏障
  5. monitor enter和volatile load規(guī)則一致,monitor exit 和volatile store規(guī)則一致。

內(nèi)存屏障的規(guī)則

需要的屏障 第二個操作 第二個操作 第二個操作 第二個操作
第一個操作 普通讀 普通寫 volatile讀/monitor enter volatile寫/monitor exit
普通讀 LoadStore
普通讀 StoreStore
voaltile讀/monitor enter LoadLoad LoadStore LoadLoad LoadStore
volatile寫/monitor exit StoreLoad StoreStore

什么是 Java 內(nèi)存模型(重新寫)

http://www.infoq.com/cn/articles/java-memory-model-1

http://www.cnblogs.com/skywang12345/p/3447546.html

http://ifeve.com/java-memory-model-6/

什么是樂觀鎖和悲觀鎖

樂觀鎖:就像它的名字一樣,對于并發(fā)間操作產(chǎn)生的線程安全問題持樂觀狀態(tài),樂觀鎖認為競爭不總是會發(fā)生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改內(nèi)存中的變量,如果失敗則表示發(fā)生沖突,那么就應(yīng)該有相應(yīng)的重試邏輯。

悲觀鎖:還是像它的名字一樣,對于并發(fā)間操作產(chǎn)生的線程安全問題持悲觀狀態(tài),悲觀鎖認為競爭總是會發(fā)生,因此每次對某資源進行操作時,都會持有一個獨占的鎖,就像 synchronized,不管三七二十一,直接上了鎖就操作資源了。

Semaphore 有什么作用

Semaphore 就是一個信號量,它的作用是限制某段代碼塊的并發(fā)數(shù)。Semaphore 有一個構(gòu)造函數(shù),可以傳入一個 int 型整數(shù) n,表示某段代碼最多只有 n 個線程可以訪問,如果超出了 n,那么請等待,等到某個線程執(zhí)行完畢這段代碼塊,下一個線程再進入。由此可以看出如果 Semaphore 構(gòu)造函數(shù)中傳入的 int 型整數(shù) n = 1,相當于變成了一個 synchronized 了。

使用場景:Semaphore可以用于做流量控制,特別公用資源有限的應(yīng)用場景,比如數(shù)據(jù)庫連接。

高并發(fā)、任務(wù)執(zhí)行時間短的業(yè)務(wù)怎樣使用線程池?并發(fā)不高、任務(wù)執(zhí)行時間長的業(yè)務(wù)怎樣使用線程池?并發(fā)高、業(yè)務(wù)執(zhí)行時間長的業(yè)務(wù)怎樣使用線程池?

  1. 高并發(fā)、任務(wù)執(zhí)行時間短的業(yè)務(wù),線程池線程數(shù)可以設(shè)置為 CPU 核數(shù) +1,減少線程上下文的切換

  2. 并發(fā)不高、任務(wù)執(zhí)行時間長的業(yè)務(wù)要區(qū)分開看:

    • 假如是業(yè)務(wù)時間長集中在 IO 操作上,也就是 IO 密集型的任務(wù),因為 IO 操作并不占用 CPU,所以不要讓所有的 CPU 閑下來,可以加大線程池中的線程數(shù)目,讓 CPU 處理更多的業(yè)務(wù)
    • 假如是業(yè)務(wù)時間長集中在計算操作上,也就是計算密集型任務(wù),只能把線程池中的線程數(shù)設(shè)置得少一些,減少線程上下文的切換
  3. 并發(fā)高、業(yè)務(wù)執(zhí)行時間長,解決這種類型任務(wù)的關(guān)鍵不在于線程池而在于整體架構(gòu)的設(shè)計,看看這些業(yè)務(wù)里面某些數(shù)據(jù)是否能做緩存是第一步,增加服務(wù)器是第二步,至于線程池的設(shè)置,設(shè)置參考(2)。最后,業(yè)務(wù)執(zhí)行時間長的問題,也可能需要分析一下,看看能不能使用中間件對任務(wù)進行拆分和解耦。

Fork/Join 框架的理解

http://www.infoq.com/cn/articles/fork-join-introduction

Fork/Join 框架是 Java7 提供了的一個用于并行執(zhí)行任務(wù)的框架, 是一個把大任務(wù)分割成若干個小任務(wù),最終匯總每個小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架。Fork 就是把一個大任務(wù)切分為若干子任務(wù)并行的執(zhí)行,Join 就是合并這些子任務(wù)的執(zhí)行結(jié)果,最后得到這個大任務(wù)的結(jié)果。Fork/Join 的核心是 work-stealing 算法。

Fork/Join 使用兩個類來完成以上兩件事情:

  • ForkJoinTask:我們要使用 ForkJoin 框架,必須首先創(chuàng)建一個 ForkJoin 任務(wù)。它提供在任務(wù)中執(zhí)行 fork() 和 join() 操作的機制,通常情況下我們不需要直接繼承 ForkJoinTask 類,而只需要繼承它的子類,F(xiàn)ork/Join 框架提供了以下兩個子類:

    • RecursiveAction:用于沒有返回結(jié)果的任務(wù)。

    • RecursiveTask :用于有返回結(jié)果的任務(wù)。

  • ForkJoinPool :ForkJoinTask 需要通過 ForkJoinPool 來執(zhí)行,任務(wù)分割出的子任務(wù)會添加到當前工作線程所維護的雙端隊列中,進入隊列的頭部。當一個工作線程的隊列里暫時沒有任務(wù)時,它會隨機從其他工作線程的隊列的尾部獲取一個任務(wù)。

ForkJoinTask 與一般的任務(wù)的主要區(qū)別在于它需要實現(xiàn) compute 方法,在這個方法里,首先需要判斷任務(wù)是否足夠小,如果足夠小就直接執(zhí)行任務(wù)。如果不足夠小,就必須分割成兩個子任務(wù),每個子任務(wù)在調(diào)用 fork 方法時,又會進入 compute 方法,看看當前子任務(wù)是否需要繼續(xù)分割成孫任務(wù),如果不需要繼續(xù)分割,則執(zhí)行當前子任務(wù)并返回結(jié)果。使用 join 方法會等待子任務(wù)執(zhí)行完并得到其結(jié)果。

ForkJoinTask 在執(zhí)行的時候可能會拋出異常,但是我們沒辦法在主線程里直接捕獲異常,所以 ForkJoinTask 提供了 isCompletedAbnormally() 方法來檢查任務(wù)是否已經(jīng)拋出異?;蛞呀?jīng)被取消了,并且可以通過 ForkJoinTask 的 getException 方法獲取異常。

work-stealing 工作竊取算法

所謂 Work-Stealing,在 ForkJoinPool 中的實現(xiàn)為:線程池中每個線程都有一個互不影響的任務(wù)隊列(雙端隊列),線程每次都從自己的任務(wù)隊列的隊頭中取出一個任務(wù)來運行;如果某個線程對應(yīng)的隊列已空并且處于空閑狀態(tài),而其他線程的隊列中還有任務(wù)需要處理但是該線程處于工作狀態(tài),那么空閑的線程可以從其他線程的隊列的隊尾取一個任務(wù)來幫忙運行 —— 感覺就像是空閑的線程去偷人家的任務(wù)來運行一樣,所以叫 “工作竊取”。

Work-Stealing 的適用場景是不同的任務(wù)的耗時相差比較大,即某些任務(wù)需要運行較長時間,而某些任務(wù)會很快的運行完成,這種情況下用 Work-Stealing 很合適;但是如果任務(wù)的耗時很平均,則此時 Work-Stealing 并不適合,因為竊取任務(wù)時不同線程需要搶占鎖,這可能會造成額外的時間消耗,而且每個線程維護雙端隊列也會造成更大的內(nèi)存消耗。所以 ForkJoinPool 并不是 ThreadPoolExecutor 的替代品,而是作為對 ThreadPoolExecutor 的補充。

ForkJoinPool 和 ThreadPoolExecutor 的區(qū)別

ForkJoinPool 和 ThreadPoolExecutor 都是 ExecutorService(線程池),但ForkJoinPool 的獨特點在于:

  1. ThreadPoolExecutor 只能執(zhí)行 Runnable 和 Callable 任務(wù),而 ForkJoinPool 不僅可以執(zhí)行 Runnable 和 Callable 任務(wù),還可以執(zhí)行 Fork/Join 型任務(wù) —— ForkJoinTask —— 從而滿足并行地實現(xiàn)分治算法的需要。

  2. ThreadPoolExecutor 中任務(wù)的執(zhí)行順序是按照其在共享隊列中的順序來執(zhí)行的,所以后面的任務(wù)需要等待前面任務(wù)執(zhí)行完畢后才能執(zhí)行,而 ForkJoinPool 每個線程有自己的任務(wù)隊列,并在此基礎(chǔ)上實現(xiàn)了 Work-Stealing 的功能,使得在某些情況下 ForkJoinPool 能更大程度的提高并發(fā)效率。

forkjoin 框架和 mapreduce 框架有什么區(qū)別?

MapReduce 是把大數(shù)據(jù)集切分成小數(shù)據(jù)集,并行分布計算后再合并。

ForkJoin 是將一個問題遞歸分解成子問題,再將子問題并行運算后合并結(jié)果。

二者共同點:都是用于執(zhí)行并行任務(wù)的。基本思想都是把問題分解為一個個子問題分別計算,再合并結(jié)果。應(yīng)該說并行計算都是這種思想,彼此獨立的或可分解的。從名字上看 Fork 和 Map 都有切分的意思,Join 和 Reduce 都有合并的意思,比較類似。

區(qū)別

  1. 環(huán)境差異,分布式 vs 單機多核:ForkJoin 設(shè)計初衷針對單機多核(處理器數(shù)量很多的情況)。MapReduce 一開始就明確是針對很多機器組成的集群環(huán)境的。也就是說一個是想充分利用多處理器,而另一個是想充分利用很多機器做分布式計算。這是兩種不同的的應(yīng)用場景,有很多差異,因此在細的編程模式方面有很多不同。

  2. 編程差異:MapReduce 一般是:做較大粒度的切分,一開始就先切分好任務(wù)然后再執(zhí)行,并且彼此間在最后合并之前不需要通信。這樣可伸縮性更好,適合解決巨大的問題,但限制也更多。ForkJoin 可以是較小粒度的切分,任務(wù)自己知道該如何切分自己,遞歸地切分到一組合適大小的子任務(wù)來執(zhí)行,因為是一個 JVM 內(nèi),所以彼此間通信是很容易的,更像是傳統(tǒng)編程方式。

二、集合

ConcurrentHashMap JDK 1.7 的實現(xiàn)原理

ConcurrentHashMap 在 1.7 中采用了相同的數(shù)據(jù)結(jié)構(gòu),即分段鎖的技術(shù)來實現(xiàn)的。ConcurrentHashMap 內(nèi)部有一個叫 Segment 的數(shù)組,里面存放的都是 Segment 對象。Segment 對象繼承了 ReentrantLock,這樣就使得每個段都有一把鎖。 Segment 里面有一個被 volatile 修飾的 HashEntry 的數(shù)組。(在 ConcurrentHashMap 初始化的時候,創(chuàng)建了 Segment 數(shù)組,并初始化第一個元素。)

put 方法

  1. 判斷 value 是否為 null, 如果 value 為 null 則拋出異常。

  2. 當用戶調(diào)用 put 方法的時候,首先根據(jù) key 的 hash 值找到具體的 Segment 在 table 中的位置。

  3. 如果這個位置上的 Segment 沒有初始化,則進行初始化的操作。

  4. 最后委托給 Segment 的 put 方法。此方法中會根據(jù)計算出的 (tab.length - 1) & hash 的 index 定位到 HashEntry, 如果這個位置上的節(jié)點為null,則新建一個 HashEntry并返回 。如果不為 null,則遍歷 HashEntry 的每一個節(jié)點,如果有相同的 key 存在則更新 value, 如果沒有則新建一個 HashEntry 放入鏈表頭部的位置。

  5. 如果 ConcurrentHashMap 內(nèi)存放的元素個數(shù)超過了閾值,那么需要對其進行擴容。整個操作都是加鎖的。

get 操作

==get 操作的時候沒有對 ConcurrentHashMap 進行上鎖==

  1. 根據(jù) key 的 hash 值計算出在哪個 Segment 上,再根據(jù) hash 值計算出在哪個 HashEntry 上

  2. 然后遍歷 HashEntry 的所有節(jié)點,如果找到 key,那么就返回對應(yīng)的 value,如果 key 沒有找到,就返回 null。

擴容

ConcurrentHashMap不會增加Segment的數(shù)量,而只會增加Segment中鏈表數(shù)組的容量大小,這樣的好處是擴容過程不需要對整個ConcurrentHashMap做rehash,而只需要對Segment里面的元素做一次 resize 就可以了。

ConcurrentHashMap JDK 1.8 的實現(xiàn)原理

ConcurrentHashMap 在 JDK 1.8 中進行了大幅度的改進。取消了 Segment 分段鎖的概念。采用了數(shù)組 + 鏈表 + 紅黑樹的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)。內(nèi)部存放了一個 Node<K,V>[] table 的 table。 ConcurrentHashMap 在初始化的時候只是設(shè)置了一些變量值,并沒有對整個 table 進行初始化,初始化的動作被放入到了第一次 put 元素的時候。當鏈表長度大于 8 并且數(shù)組的長度大于 64 的時候會將鏈表轉(zhuǎn)成紅黑樹結(jié)構(gòu)。

put 操作

  1. 首先判斷 key 和 value 是否為 null, 如果為 null 則拋出異常。

  2. 然后在判斷 table 是否初始化,如果沒有初始化則通過 CAS 操作將 sizeCtl 的值設(shè)置為 -1,并執(zhí)行 table 的初始化操作。

  3. 根據(jù) key 的 hash 定位到 key 所在 table 的位置,如果這個位置上沒有元素,則直接插入元素后返回。

  4. 如果當前節(jié)點的 hash 值為 -1,說明當前的節(jié)點是 forwardingNode 節(jié)點,表示 table 正在擴容,當前線程需要幫助一起擴容。(上面的過程走完之后,說明當前的節(jié)點上有元素,需要對當前節(jié)點加鎖然后操作)。

  5. 如果當前節(jié)點的 hash 值大于等于 0,說明是一個鏈表結(jié)構(gòu),則遍歷鏈表,如果存在當前 key 節(jié)點則替換 value,否則插入到鏈表尾部。

  6. 如果 f 是 TreeBin 類型節(jié)點,則按照紅黑樹的方法更新或者增加節(jié)點。

  7. 若鏈表長度 > TREEIFY_THRESHOLD(默認是8),則將鏈表轉(zhuǎn)換為紅黑樹結(jié)構(gòu)(并不是直接轉(zhuǎn)的,還需要進一步判斷,具體的在treeifyBin()方法中)。

  8. 最后調(diào)用 addCount 方法,將 ConcurrentHashMap 的 size + 1,并判斷是否需要執(zhí)行擴容操作,整個 put 過程結(jié)束。

get 操作

get 操作的時候沒有上鎖,如果整個table 為空,則返回null,否則根據(jù) key 的 hash 值找到 table 的 index 位置,然后根據(jù)鏈表或者樹形方式找到相對應(yīng)的節(jié)點,返回其 value 值。

紅黑樹轉(zhuǎn)換

鏈表的元素個數(shù)達到了閾值 8 ,則會調(diào)用 treeifyBin 方法把鏈表轉(zhuǎn)換成紅黑樹,不過在結(jié)構(gòu)轉(zhuǎn)換之前,會對數(shù)組長度進行判斷。如果數(shù)組長度 n 小于閾值 MIN_TREEIFY_CAPACITY,默認是 64,則會調(diào)用 tryPresize 方法把數(shù)組長度擴大到原來的兩倍,并觸發(fā) transfer 方法,重新調(diào)整節(jié)點的位置。

擴容操作

整個擴容操作分為兩步:

  1. 構(gòu)建一個nextTable,其大小為原來大小的兩倍,這個步驟是在單線程環(huán)境下完成的。

  2. 將原來table里面的內(nèi)容復(fù)制到nextTable中,這個步驟是允許多線程操作的,所以性能得到提升,減少了擴容的時間消耗。

并發(fā)擴容的具體步驟如下:

  1. 為每個內(nèi)核分任務(wù),并保證其不小于16

  2. 檢查nextTable是否為null,如果是,則初始化 nextTable,使其容量為 table 的兩倍。然后死循環(huán)遍歷節(jié)點,直到finished。節(jié)點從 table 復(fù)制到 nextTable 中,支持并發(fā),思路如下:

  3. 如果節(jié)點 f 為 null,則插入 ForwardingNode(采用 Unsafe.compareAndSwapObject 方法實現(xiàn)),這個是觸發(fā)并發(fā)擴容的關(guān)鍵

  4. 如果 f 為鏈表的頭節(jié)點(fh >= 0),則先構(gòu)造一個反序鏈表,然后把他們分別放在nextTable的 i 和 i + n位置,并將 ForwardingNode 插入原節(jié)點位置,代表已經(jīng)處理過了

  5. 如果 f 為 TreeBin 節(jié)點,同樣也是構(gòu)造一個反序鏈表 ,==同時需要判斷是否需要進行 unTreeify() 操作==,并把處理的結(jié)果分別插入到 nextTable 的 i 和 i+n 位置,并插入 ForwardingNode 節(jié)點

  6. 所有節(jié)點復(fù)制完成后,則將 table 指向 nextTable,同時更新 sizeCtl = nextTable 的 0.75 倍,完成擴容過程。

在多線程環(huán)境下,ConcurrentHashMap 用兩點來保證正確性:ForwardingNode 和 synchronized。當一個線程遍歷到的節(jié)點如果是 ForwardingNode,則繼續(xù)往后遍歷,如果不是,則將該節(jié)點加鎖,防止其他線程進入,完成后設(shè)置 ForwardingNode 節(jié)點,以便要其他線程可以看到該節(jié)點已經(jīng)處理過了,如此交叉進行,高效而又安全。

HashMap JDK 1.7 實現(xiàn)原理

HashMap 在 JDK 7 中的數(shù)據(jù)結(jié)構(gòu)是數(shù)組 + 鏈表的實現(xiàn)。 HashMap 中存儲了一個 Entry[] 類型的數(shù)組,里面存儲了 Entry 對象。

put

  1. 當調(diào)用 put 方法的時候,會根據(jù) key 的 hash 值定位到 key 要存到 table 的哪個 index 上

  2. 如果 key 為 null,那么就 put 到鏈表的頭結(jié)點上。

  3. 如果 key 不為 null,那么遍歷 index 上的鏈表,如果存在相同的 key,那么就更新 value。

  4. 如果不存在相同的 key,那么將 key 存到鏈表的頭結(jié)點中。

get

  1. 根據(jù) key 的 hash 值計算出 key 在 table 的哪個 index 上。遍歷 index 上的鏈表,找到相同的 key,并返回 value。

  2. 如果 key 不存在則返回 null。

擴容

什么時候擴容?

當 HashMap 內(nèi)的容量數(shù)超過了閾值(默認 12 個)的時候會觸發(fā)擴容。整個擴容過程如下:

  1. 新建一個比原來數(shù)組兩倍大的新數(shù)組。

  2. 重算 key 的 hash 值來得到在新數(shù)組的位置,并將 key 放入新數(shù)組中。

死循環(huán)問題

主要是多線程同時put時,如果同時觸發(fā)了rehash操作,會導(dǎo)致HashMap中的鏈表中出現(xiàn)循環(huán)節(jié)點,進而使得后面get的時候,會死循環(huán)。而且還會丟失元素。

HashMap JDK 1.8 實現(xiàn)原理

在 JDK 1.8 中, HashMap 重新設(shè)計了實現(xiàn)。放棄 1.7 中的數(shù)組 + 鏈表的存儲結(jié)構(gòu),改為了數(shù)組 + 鏈表 + 紅黑樹的實現(xiàn)。

put

  1. 根據(jù) key 的 hash 值,計算出 table 中的 index。

  2. 如果 index 上沒有元素,那么直接插入元素。

  3. 如果 index 上有元素的話,并且是鏈表結(jié)構(gòu)的話,就遍歷鏈表,判斷是否有相同的 key 存在,如果存在則替換 value,如果不存在則新建 Node ==放入鏈表尾部==。同時判斷當前鏈表是否過長,如果超過 TREEIFY_THRESHOLD(默認 8 個) 的話,則需要將鏈表轉(zhuǎn)換成紅黑樹。

  4. 如果 index 上的節(jié)點是 TreeNode 類型的話,則用紅黑樹的方式添加元素。

  5. 最后判斷 HashMap 中的元素是否超過了閾值,如果超過了需要進行 resize 擴容。

get

  1. 根據(jù) key 的 hash 值定位到 table 中的 index。

  2. 如果 index 上沒有元素,則返回 null。

  3. 如果 index 上有元素,那么根據(jù)節(jié)點類型的不同,調(diào)用鏈表或紅黑樹的方式獲取 value。

擴容

在 JDK 1.8 的實現(xiàn)中,優(yōu)化了高位運算的算法,通過 hashCode() 的高 16 位異或低 16 位實現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質(zhì)量來考慮的,這么做可以在數(shù)組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。

ArrayList 和 LinkedList 的區(qū)別

1.對ArrayList和LinkedList而言,在列表末尾增加一個元素所花的開銷都是固定的。對ArrayList而言,主要是在內(nèi)部數(shù)組中增加一項,指向所添加的元素,偶爾可能會導(dǎo)致對數(shù)組重新進行分配;而對LinkedList而言,這個開銷是統(tǒng)一的,分配一個內(nèi)部Entry對象。

2.在ArrayList的中間插入或刪除一個元素意味著這個列表中剩余的元素都會被移動;而在LinkedList的中間插入或刪除一個元素的開銷是固定的。

3.LinkedList不支持高效的隨機元素訪問。

4.ArrayList的空間浪費主要體現(xiàn)在在list列表的結(jié)尾預(yù)留一定的容量空間,而LinkedList的空間花費則體現(xiàn)在它的每一個元素都需要消耗相當?shù)目臻g

可以這樣說:當操作是在一列數(shù)據(jù)的后面添加數(shù)據(jù)而不是在前面或中間,并且需要隨機地訪問其中的元素時,使用ArrayList會提供比較好的性能;當你的操作是在一列數(shù)據(jù)的前面或中間添加或刪除數(shù)據(jù),并且按照順序訪問其中的元素時,就應(yīng)該使用LinkedList了。

三、IO/NIO

TCP協(xié)議與UDP協(xié)議的區(qū)別

  1. TCP 是面向連接的,而 UDP 是面向無連接的

  2. 對系統(tǒng)資源的要求(TCP 較多,UDP 少)

  3. UDP 程序結(jié)構(gòu)較簡單

  4. TCP 采用流模式,而 UDP 采用數(shù)據(jù)報文模式

  5. TCP 保證數(shù)據(jù)正確性,UDP 可能丟包,TCP 保證數(shù)據(jù)順序,UDP 不保證。

三次握手和四次揮手的過程

三次握手

所謂三次握手(Three-way Handshake),是指建立一個 TCP 連接時,需要客戶端和服務(wù)器總共發(fā)送 3 個包。

三次握手的目的是連接服務(wù)器指定端口,建立 TCP 連接,并同步連接雙方的序列號和確認號并交換 TCP 窗口大小信息.在 socket 編程中,客戶端執(zhí)行 connect() 時。將觸發(fā)三次握手。

第一次握手: 客戶端發(fā)送一個 TCP 的 SYN 標志位置1的包指明客戶打算連接的服務(wù)器的端口,以及初始序號 X,保存在包頭的序列號(Sequence Number)字段里。

第二次握手:服務(wù)器發(fā)回確認包(ACK)應(yīng)答。即 SYN 標志位和 ACK 標志位均為 1 同時,將確認序號(Acknowledgement Number)設(shè)置為客戶的 ISN 加 1 以.即 X + 1。

第三次握手:客戶端再次發(fā)送確認包(ACK) SYN 標志位為 0,ACK 標志位為1。并且把服務(wù)器發(fā)來 ACK 的序號字段 +1,放在確定字段中發(fā)送給對方。并且在數(shù)據(jù)段放寫 ISN 的 +1。


四次揮手

客戶端或服務(wù)器均可主動發(fā)起揮手動作,在socket編程中,任何一方執(zhí)行close()操作即可產(chǎn)生揮手操作。

第一次分手:主機1(可以使客戶端,也可以是服務(wù)器端),設(shè)置Sequence Number和Acknowledgment Number,向主機2發(fā)送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態(tài);這表示主機1沒有數(shù)據(jù)要發(fā)送給主機2了;

第二次分手:主機 2 收到了主機 1 發(fā)送的 FIN 報文段,向主機 1 回一個 ACK 報文段,Acknowledgment Number 為 Sequence Number 加 1;主機 1 進入 FIN_WAIT_2 狀態(tài);主機 2 告訴主機 1,我“同意”你的關(guān)閉請求;

第三次分手:主機 2 向主機 1 發(fā)送 FIN 報文段,請求關(guān)閉連接,同時主機 2 進入 LAST_ACK 狀態(tài);

第四次分手:主機 1 收到主機 2 發(fā)送的 FIN 報文段,向主機 2 發(fā)送 ACK 報文段,然后主機 1 進入 TIME_WAIT 狀態(tài);主機 2 收到主機 1 的 ACK 報文段以后,就關(guān)閉連接;此時,主機 1 等待 2MS 后依然沒有收到回復(fù),則證明 Server 端已正常關(guān)閉,那好,主機 1 也可以關(guān)閉連接了。

java 中的字節(jié)順序

字節(jié)順序是指:占用內(nèi)存多于一個字節(jié)類型的數(shù)據(jù)在內(nèi)存中的存放順序,有小端、大端兩種順序。小端字節(jié)序(little endian):低字節(jié)數(shù)據(jù)存放在內(nèi)存低地址處,高字節(jié)數(shù)據(jù)存放在內(nèi)存高地址處;大端字節(jié)序(bigendian):高字節(jié)數(shù)據(jù)存放在低地址處,低字節(jié)數(shù)據(jù)存放在高地址處。

至于計算機到底是 BIG-ENDIAN、LITTLE-ENDIAN、跟 CPU 有關(guān)的,一種 CPU 不是 BIG-ENDIAN 就是 LITTLE-ENDIAN。IA 架構(gòu)(Intel、AMD)的 CPU 中是 Little-Endian,而 PowerPC 、SPARC 和 Motorola 處理器是 Big-Endian。這其實就是所謂的主機字節(jié)序。而網(wǎng)絡(luò)字節(jié)序是指數(shù)據(jù)在網(wǎng)絡(luò)上傳輸時是大頭還是小頭的,在 Internet 的網(wǎng)絡(luò)字節(jié)序是 BIG-ENDIAN。所謂的 JAVA 字節(jié)序指的是在 JAVA 虛擬機中多字節(jié)類型數(shù)據(jù)的存放順序,JAVA 字節(jié)序也是 BIG-ENDIAN。

JDK為我們提供一個類ByteOrder,通過以下代碼就可以知道機器的字節(jié)序

System.out.println(ByteOrder.nativeOrder());  

在java.nio包下提供了ByteOrder、ByteBuffer等于字節(jié)序相關(guān)的類,我們也可以改變JVM中默認的字節(jié)序。代碼如下:

public class JVMEndianTest {  
      
    public static void main(String[] args) {  
          
        int x = 0x01020304;  
          
        ByteBuffer bb = ByteBuffer.wrap(new byte[4]);  
        bb.asIntBuffer().put(x);  
        String ss_before = Arrays.toString(bb.array());  
          
        System.out.println("默認字節(jié)序 " +  bb.order().toString() +  ","  +  " 內(nèi)存數(shù)據(jù) " +  ss_before);  
          
        bb.order(ByteOrder.LITTLE_ENDIAN);  
        bb.asIntBuffer().put(x);  
        String ss_after = Arrays.toString(bb.array());  
          
        System.out.println("修改字節(jié)序 " + bb.order().toString() +  ","  +  " 內(nèi)存數(shù)據(jù) " +  ss_after);  
    }  
}  

執(zhí)行結(jié)果如下:

默認字節(jié)序 BIG_ENDIAN, 內(nèi)存數(shù)據(jù) [1, 2, 3, 4]

修改字節(jié)序 LITTLE_ENDIAN, 內(nèi)存數(shù)據(jù) [4, 3, 2, 1]

socket 的一些重要參數(shù)

http://www.cnblogs.com/ggjucheng/archive/2012/01/06/2314679.html

  1. backlog

    連接請求的最大隊列長度被設(shè)置為 backlog 參數(shù)。如果隊列滿時則拒絕該連接。

    • backlog 這個參數(shù)設(shè)置為 -1 表示無限制,默認是 50 個最大等待隊列
    • 經(jīng)過測試這個隊列是按照 FIFO(先進先出)的原則。
    • 如果將 accept 這個函數(shù)放在一個循環(huán)體中時,backlog 參數(shù)也不會有什么作用。
  1. TcpNoDelay

    禁用納格算法,將數(shù)據(jù)立即發(fā)送出去。納格算法是以減少封包傳送量來增進TCP/IP網(wǎng)絡(luò)的效能。

  1. SoLinger

    當我們調(diào)用 socket.close() 返回時,socket 已經(jīng) write 的數(shù)據(jù)未必已經(jīng)發(fā)送到對方了,那么我們設(shè)置了 socket.setSoLinger(true, 100) 時,那么 close 會等到發(fā)送的數(shù)據(jù)已經(jīng)確認了才返回。但是如果對方宕機,超時,那么會根據(jù) linger 設(shè)定的時間返回。

  1. SoTimeout

    設(shè)置 socket 調(diào)用 InputStream 讀數(shù)據(jù)的超時時間,以毫秒為單位,如果超過這個時候,會拋出 java.net.SocketTimeoutException。

  2. KeepAlive

    keepalive 不是說 TCP 的常連接,當我們作為服務(wù)端,一個客戶端連接上來,如果設(shè)置了 keeplive 為 true,當對方?jīng)]有發(fā)送任何數(shù)據(jù)過來,超過一個時間(看系統(tǒng)內(nèi)核參數(shù)配置),那么我們這邊會發(fā)送一個 ack 探測包發(fā)到對方,探測雙方的 TCP/IP 連接是否有效(對方可能斷點,斷網(wǎng)),在 Linux 好像這個時間是 75 秒。如果不設(shè)置,那么客戶端宕機時,服務(wù)器永遠也不知道客戶端宕機了,仍然保存這個失效的連接。

  1. SendBufferSize和ReceiveBufferSize

    TCP 發(fā)送緩存區(qū)和接收緩存區(qū),默認是 8192,一般情況下足夠了,而且就算你增加了發(fā)送緩存區(qū),對方?jīng)]有增加它對應(yīng)的接收緩沖,那么在 TCP 三握手時,最后確定的最大發(fā)送窗口還是雙方最小的那個緩沖區(qū),就算你無視,發(fā)了更多的數(shù)據(jù),那么多出來的數(shù)據(jù)也會被丟棄。除非雙方都協(xié)商好。

ByteBuffer 如何創(chuàng)建,如何讀寫?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容