進程和線程的區別和聯繫java
從資源佔用,切換效率,通訊方式等方面解答程序員
線程具備許多傳統進程所具備的特徵,故又稱爲輕型進程(Light—Weight Process)或進程元;而把傳統的進程稱爲重型進程(Heavy—Weight Process),它至關於只有一個線程的任務。在引入了線程的操做系統中,一般一個進程都有若干個線程,至少須要一個線程。下面,咱們從調度、併發性、 系統開銷、擁有資源等方面,來比較線程與進程。算法
1.調度編程
在傳統的操做系統中,擁有資源的基本單位和獨立調度、分派的基本單位都是進程。而在引入線程的操做系統中,則把線程做爲調度和分派的基本單位。而把進程做 爲資源擁有的基本單位,使傳統進程的兩個屬性分開,線程便能輕裝運行,從而可顯著地提升系統的併發程度。在同一進程中,線程的切換不會引發進程的切換,在 由一個進程中的線程切換到另外一個進程中的線程時,將會引發進程的切換。windows
2.併發性數組
在引入線程的操做系統中,不只進程之間能夠併發執行,並且在一個進程中的多個線程之間,亦可併發執行,於是使操做系統具備更好的併發性,從而能更有效地使 用系統資源和提升系統吞吐量。例如,在一個未引入線程的單CPU操做系統中,若僅設置一個文件服務進程,當它因爲某種緣由而被阻塞時,便沒有其它的文件服 務進程來提供服務。在引入了線程的操做系統中,能夠在一個文件服務進程中,設置多個服務線程,當第一個線程等待時,文件服務進程中的第二個線程能夠繼續運 行;當第二個線程阻塞時,第三個線程能夠繼續執行,從而顯著地提升了文件服務的質量以及系統吞吐量。緩存
3.擁有資源安全
不管是傳統的操做系統,仍是設有線程的操做系統,進程都是擁有資源的一個獨立單位,它能夠擁有本身的資源。通常地說,線程本身不擁有系統資源(也有一點必 不可少的資源),但它能夠訪問其隸屬進程的資源。亦即,一個進程的代碼段、數據段以及系統資源,如已打開的文件、I/O設備等,可供問一進程的其它全部線 程共享。網絡
4.系統開銷數據結構
因爲在建立或撤消進程時,系統都要爲之分配或回收資源,如內存空間、I/o設備等。所以,操做系統所付出的開銷將顯著地大於在建立或撤消線程時的開銷。類 似地,在進行進程切換時,涉及到整個當前進程CPU環境的保存以及新被調度運行的進程的CPU環境的設置。而線程切換隻須保存和設置少許寄存器的內容,並 不涉及存儲器管理方面的操做。可見,進程切換的開銷也遠大於線程切換的開銷。此外,因爲同一進程中的多個線程具備相同的地址空間,導致它們之間的同步和通訊的實現,也變得比較容易。在有的系統中,線程的切換、同步和通訊都無須
簡單介紹一下進程的切換過程
線程上下文的切換代價,要回答,切換會保存寄存器,棧等線程相關的現場,須要由用戶態切換到內核態,能夠用vmstat命令查看線程上下文的切換情況
1.線程的狀態轉換
六個狀態對應ThreadState的枚舉
CAS與ABA問題
Java中的線程的生命週期大致可分爲5種狀態。
1. 新建(NEW):新建立了一個線程對象。
2. 可運行(RUNNABLE):線程對象建立後,其餘線程(好比main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取cpu 的使用權 。
3. 運行(RUNNING):可運行狀態(runnable)的線程得到了cpu 時間片(timeslice) ,執行程序代碼。
4. 阻塞(BLOCKED):阻塞狀態是指線程由於某種緣由放棄了cpu 使用權,也即讓出了cpu timeslice,暫時中止運行。直到線程進入可運行(runnable)狀態,纔有機會再次得到cpu timeslice 轉到運行(running)狀態。阻塞的狀況分三種:
(一). 等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。
(二). 同步阻塞:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中。
(三). 其餘阻塞:運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新轉入可運行(runnable)狀態。
5. 死亡(DEAD):線程run()、main() 方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。
CAS原理
在計算機科學中,比較和交換(Compare And Swap)是用於實現多線程同步的原子指令。 它將內存位置的內容與給定值進行比較,只有在相同的狀況下,將該內存位置的內容修改成新的給定值。 這是做爲單個原子操做完成的。 原子性保證新值基於最新信息計算; 若是該值在同一時間被另外一個線程更新,則寫入將失敗。 操做結果必須說明是否進行替換; 這能夠經過一個簡單的布爾響應(這個變體一般稱爲比較和設置),或經過返回從內存位置讀取的值來完成(摘自維基本科)
ABA問題
產生ABA問題的緣由
CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。這就是CAS的ABA問題。
如何規避ABA問題
經常使用的辦法是在更新數據的時候加入版本號,以版本號來控制更新。
【1】CAS方法:CompareAndSwap
一、樂觀鎖的使用的機制就是CAS。
在CAS方法中,CAS有三個操做數,內存值V,舊的預期值E,要修改的新值U。當且僅當預期值E和內存值V相等時,將內存值V修改成U,不然什麼都不作。
二、非阻塞算法(nonblocking algorithms):一個線程的失敗或者掛起不該該影響其餘線程的失敗或掛起的算法。
(1)非阻塞算法簡介:https://www.ibm.com/developerworks/cn/java/j-jtp04186/
(2)非阻塞算法一般叫做樂觀算法,由於它們繼續操做的假設是不會有干擾。若是發現干擾,就會回退並重試。
三、CAS方法
(1)CompareAndSwap()就使用了非阻塞算法來代替鎖定。
(2)舉例:AtomicInteger來研究在沒有鎖的狀況下是如何作到數據正確性的。
在沒有鎖機制的狀況下,要保證線程間的數據是可見的,就會經常用到volatile原語了。
private volatile int value;
可以使用以下方法讀取內存變量值value:
public final int getValue(){
return value;
}
遞增計數器是如何實現的:
public final int incrementAndGet(){
for(;;){
int current = getValue();
int next = value + 1;
if(compareAndSet(current,next)){
return next;
}
}
}
該方法採用了CAS操做,每次從內存中讀取數據而後將此數據和+1後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。
而compareAndSet操做利用了Java本地接口(JNI,Java Native Interface)完成CPU指令的操做:
public final boolean compareAndSet(int expect, int update){
return unsafe.compareAndSwapInt(this,valueOffset,expect,unsafe);
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(*.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex); }
}
「ABA」問題
一、能夠發現,CAS實現的過程是先取出內存中某時刻的數據,在下一時刻比較並替換,那麼在這個時間差會致使數據的變化,此時就會致使出現「ABA」問題。
二、什麼是」ABA」問題?
好比說一個線程one從內存位置V中取出A,這時候另外一個線程two也從內存中取出A,而且two進行了一些操做變成了B,而後two又將V位置的數據變成A,這時候線程one進行CAS操做發現內存中仍然是A,而後one操做成功。
儘管線程one的CAS操做成功,可是不表明這個過程就是沒有問題的。
【3】用AtomicStampedReference/AtomicMarkableReference解決ABA問題
例子:用AtomicStampedReference解決ABA問題:
package concur.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); //true
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false
}
});
refT1.start();
refT2.start();
}
}
2.同步與互斥
Synchronized實現原理
Synchronized是Java中解決併發問題的一種最經常使用的方法,也是最簡單的一種方法。Synchronized的做用主要有三個:
確保線程互斥的訪問同步代碼
保證共享變量的修改可以及時可見
有效解決重排序問題。
從語法上講,Synchronized總共有三種用法:
修飾普通方法
修飾靜態方法
修飾代碼塊
Java中每個對象均可以做爲鎖,這是synchronized實現同步的基礎:
一、普通同步方法,鎖是當前實例對象
二、靜態同步方法,鎖是當前類的class對象
三、同步方法塊,鎖是括號裏面的對象
synchronize底層原理:
Java 虛擬機中的同步(Synchronization)基於進入和退出Monitor對象實現, 不管是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)仍是隱式同步都是如此。在 Java 語言中,同步用的最多的地方多是被 synchronized 修飾的同步方法。同步方法 並非由 monitorenter 和 monitorexit 指令來實現同步的,而是由方法調用指令讀取運行時常量池中方法表結構的 ACC_SYNCHRONIZED 標誌來隱式實現的,關於這點,稍後詳細分析。
同步代碼塊:monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入到同步代碼塊的結束位置,JVM須要保證每個monitorenter都有一個monitorexit與之相對應。任何對象都有一個monitor與之相關聯,當且一個monitor被持有以後,他將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor全部權,即嘗試獲取對象的鎖;
在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例變量和填充數據。以下:
實例變量:存放類的屬性數據信息,包括父類的屬性信息,若是是數組的實例部分還包括數組的長度,這部份內存按4字節對齊。
填充數據:因爲虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊,這點了解便可。
對象頭:Hotspot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。其中Klass Point是是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵。
Mark Word:用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等等。Java對象頭通常佔有兩個機器碼(在32位虛擬機中,1個機器碼等於4字節,也就是32bit),可是若是對象是數組類型,則須要三個機器碼,由於JVM虛擬機能夠經過Java對象的元數據信息肯定Java對象的大小,可是沒法從數組的元數據來確認數組的大小,因此用一塊來記錄數組長度。
Monior:咱們能夠把它理解爲一個同步工具,也能夠描述爲一種同步機制,它一般被描述爲一個對象。與一切皆對象同樣,全部的Java對象是天生的Monitor,每個Java對象都有成爲Monitor的潛質,由於在Java的設計中 ,每個Java對象自打孃胎裏出來就帶了一把看不見的鎖,它叫作內部鎖或者Monitor鎖。Monitor 是線程私有的數據結構,每個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用。其結構以下:
Owner:初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程惟一標識,當鎖被釋放時又設置爲NULL;
EntryQ:關聯一個系統互斥鎖(semaphore),阻塞全部試圖鎖住monitor record失敗的線程。
RcThis:表示blocked或waiting在該monitor record上的全部線程的個數。
Nest:用來實現重入鎖的計數。
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
Candidate:用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。
Java虛擬機對synchronize的優化:
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖,可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級,關於重量級鎖,前面咱們已詳細分析過,下面咱們將介紹偏向鎖和輕量級鎖以及JVM的其餘優化手段。
偏向鎖
偏向鎖是Java 6以後加入的新鎖,它是一種針對加鎖操做的優化手段,通過研究發現,在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,所以爲了減小同一線程獲取鎖(會涉及到一些CAS操做,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操做,從而也就提供程序的性能。因此,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續屢次是同一個線程申請相同的鎖。可是對於鎖競爭比較激烈的場合,偏向鎖就失效了,由於這樣場合極有可能每次申請鎖的線程都是不相同的,所以這種場合下不該該使用偏向鎖,不然會得不償失,須要注意的是,偏向鎖失敗後,並不會當即膨脹爲重量級鎖,而是先升級爲輕量級鎖。
輕量級鎖
假若偏向鎖失敗,虛擬機並不會當即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段(1.6以後加入的),此時Mark Word 的結構也變爲輕量級鎖的結構。輕量級鎖可以提高程序性能的依據是「對絕大部分的鎖,在整個同步週期內都不存在競爭」,注意這是經驗數據。須要瞭解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,若是存在同一時間訪問同一鎖的場合,就會致使輕量級鎖膨脹爲重量級鎖。
自旋鎖
輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。這是基於在大多數狀況下,線程持有鎖的時間都不會太長,若是直接掛起操做系統層面的線程可能會得不償失,畢竟操做系統實現線程之間的切換時須要從用戶態轉換到核心態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,所以自旋鎖會假設在不久未來,當前的線程能夠得到鎖,所以虛擬機會讓當前想要獲取鎖的線程作幾個空循環(這也是稱爲自旋的緣由),通常不會過久,多是50個循環或100循環,在通過若干次循環後,若是獲得鎖,就順利進入臨界區。若是還不能得到鎖,那就會將線程在操做系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是能夠提高效率的。最後沒辦法也就只能升級爲重量級鎖了。
鎖消除
消除鎖是虛擬機另一種鎖的優化,這種優化更完全,Java虛擬機在JIT編譯時(能夠簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,經過這種方式消除沒有必要的鎖,能夠節省毫無心義的請求鎖時間,以下StringBuffer的append是一個同步方法,可是在add方法中的StringBuffer屬於一個局部變量,而且不會被其餘線程所使用,所以StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。
/**
* Created by zejian on 2017/6/4.
* Blog : http://blog.csdn.net/javazejian
* 消除StringBuffer同步鎖
*/
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,因爲sb只會在append方法中使用,不可能被其餘線程引用
//所以sb屬於不可能共享的資源,JVM會自動消除內部的鎖
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
for (int i = 0; i < 10000000; i++) {
rmsync.add("abc", "123");
}
}
}
synchronize的可重入性:
從互斥鎖的設計上來講,當一個線程試圖操做一個由其餘線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入鎖,請求將會成功,在java中synchronized是基於原子性的內部鎖機制,是可重入的,所以在一個線程調用synchronized方法的同時在其方法體內部調用該對象另外一個synchronized方法,也就是說一個線程獲得一個對象鎖後再次請求該對象鎖,是容許的,這就是synchronized的可重入性。以下:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this,當前實例對象鎖
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
正如代碼所演示的,在獲取當前實例對象鎖後進入synchronized代碼塊執行同步代碼,並在代碼塊中調用了當前實例對象的另一個synchronized方法,再次請求當前實例鎖時,將被容許,進而執行方法體代碼,這就是重入鎖最直接的體現,須要特別注意另一種狀況,當子類繼承父類時,子類也是能夠經過可重入鎖調用父類的同步方法。注意因爲synchronized是基於monitor實現的,所以每次重入,monitor中的計數器仍會加1。
線程中斷:正如中斷二字所表達的意義,在線程運行(run方法)中間打斷它,在Java中,提供瞭如下3個有關線程中斷的方法
//中斷線程(實例方法)
public void Thread.interrupt();
//判斷線程是否被中斷(實例方法)
public boolean Thread.isInterrupted();
//判斷是否被中斷並清除當前中斷狀態(靜態方法)
public static boolean Thread.interrupted();
等待喚醒機制與synchronize:所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處於synchronized代碼塊或者synchronized方法中,不然就會拋出IllegalMonitorStateException異常,這是由於調用這幾個方法前必須拿到當前對象的監視器monitor對象,也就是說notify/notifyAll和wait方法依賴於monitor對象,在前面的分析中,咱們知道monitor 存在於對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字能夠獲取 monitor ,這也就是爲何notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的緣由。
這段話的大概意思爲:
每一個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:
一、若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。
二、若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.
3.若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權。
執行monitorexit的線程必須是objectref所對應的monitor的全部者。
指令執行時,monitor的進入數減1,若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者。其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權。
經過這兩段描述,咱們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是經過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是爲何只有在同步的塊或者方法中才能調用wait/notify等方法,不然會拋出java.lang.IllegalMonitorStateException的異常的緣由。
從反編譯的結果來看,方法的同步並無經過指令monitorenter和monitorexit來完成(理論上其實也能夠經過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成。
運行結果解釋
有了對Synchronized原理的認識,再來看上面的程序就能夠迎刃而解了。
代碼段2結果: 雖然method1和method2是不一樣的方法,可是這兩個方法都進行了同步,而且是經過同一個對象去調用的,因此調用以前都須要先去競爭同一個對象上的鎖(monitor),也就只能互斥的獲取到鎖,所以,method1和method2只能順序的執行。
代碼段3結果: 雖然test和test2屬於不一樣對象,可是test和test2屬於同一個類的不一樣實例,因爲method1和method2都屬於靜態同步方法,因此調用的時候須要獲取同一個類上monitor(每一個類只對應一個class對象),因此也只能順序的執行。
代碼段4結果: 對於代碼塊的同步實質上須要獲取Synchronized關鍵字後面括號中對象的monitor,因爲這段代碼中括號的內容都是this,而method1和method2又是經過同一的對象去調用的,因此進入同步塊以前須要去競爭同一個對象上的鎖,所以只能順序執行同步塊。
總結
Synchronized是經過對象內部的一個叫作監視器鎖(monitor)來實現的。可是監視器鎖本質又是依賴於底層的操做系統的Mutex Lock來實現的。而操做系統實現線程之間的切換這就須要從用戶態轉換到內核態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。
所以,這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲「重量級鎖」。JDK中對Synchronized作的種種優化,其核心都是爲了減小這種重量級鎖的使用。JDK1.6之後,爲了減小得到鎖和釋放鎖所帶來的性能消耗,提升性能,引入了「偏向鎖」和「輕量級鎖」。無鎖 --> 偏向鎖 --> 輕量級 --> 重量級
鎖優化會將鎖由輕量級升級爲重量級
AQS與Lock
什麼是Lock鎖?
在Java中鎖是出現是爲了防止多個線程訪問同一資源,鎖有內建鎖(synchronized),synchronized關鍵字實現鎖是隱式加解鎖,在JDK5後,java.util.concurrent包中增長了lock接口,它提供了和synchronized同樣能夠防止多個線程訪問同一資源,可是lock鎖須要手動加鎖解鎖,也提供了synchronized沒有的同步特性(後文會敘述)。
Reentrantlock(可重入鎖)如何實現一個鎖?
Reentrantlock(可重入鎖)中全部方法實際上都是調用了其靜態內部類Sync中的方法,而Sync繼承了AbstractQueuedSynchronizer(AQS --簡稱同步器)
什麼是AQS?
同步器是用來構建鎖以及其餘同步組件的基礎框架,它的實現主要是依賴一個int狀態變量以及經過一個FIFO隊列共同構成同步對列。
子類必須重寫AQS的用proteted 修飾的用來改變同步狀態的方法,其餘方法主要實現了排隊與阻塞機制。int狀態的更新使用getState( )/setSate以及compareAndSetState( )/
子類推薦使用靜態內部類來繼承AQS實現本身的同步語義,同步器既支持獨佔鎖,也支持共享鎖。
Lock鎖與AQS的關係:
Lock面向使用者,定義了使用者與鎖交互的接口;
AQS—面向鎖的實現者,簡化了鎖的實現, 屏蔽了同步狀態的管理、線程排隊、線程等待與喚醒等底層操做。
瞭解到AQS和Lock鎖的基本知識後咱們能夠本身寫一個類Mutex鎖來實現Lock接口,在Mutex中有靜態內部類繼承AQS類:
深刻理解AQS
在同步組件的實現中,AQS是核心部分,AQS面向鎖的實現,同步組件的實現者經過使用AQS提供的模板方法實現同步組件語義,AQS則實現了對同步狀態的管理,以及對阻塞線程進行排隊,等待通知等一些底層的實現處理。AQS的核心包括了:同步隊列,獨佔式鎖的獲取和釋放,共享鎖的獲取和釋放,超時等待鎖以及可中斷鎖的獲取,這一系列功能的實現。這些實現依靠的是AQS提供的模板方法:
獨佔式鎖:
共享式鎖:
AQS的模板方法基於同步隊列,那麼什麼是同步隊列呢?
同步隊列
當多個線程競爭共享資源時,一個線程競爭到共享資源後,其餘請求資源的線程會被阻塞,進入同步隊列,也就是說同步隊列中存放的被阻塞的線程,這些線程等待cpu調度再次競爭共享資源。
同步隊列是一種隊列,隊列的實現能夠經過數組也可經過鏈表,在AQS中同步隊列的數據結構是鏈表。那是什麼鏈表呢:是有頭結點嗎?是單向仍是雙向呢?
經過源碼能夠發現同步隊列是帶頭尾結點的雙向鏈表。(注意:不帶頭結點和帶頭結點區別:頭插時,不帶頭結點須要頻繁改變頭指針)而且在添加元素是經過尾插。
在AQS中有一個靜態內部類Node
3.線程池
Executors.NewFixedThreadPool固定線程數,無界隊列.適用於任務數量不均勻的場景,對內存壓力不敏感,但系統負載敏感的場景.
Executors..newCachedThreadPool無限線程數,適用於要求低延遲的短時間任務場景.
Executors.newSingleThreadPool單個線程的固定線程池,適用於保證異步執行順序的場景.
Executors.newScheduledThreadPool適用於按期執行任務場景,支持固定頻率和固定延遲.
Executors.newWorkStealingPool使用ForkJoinPool,多任務隊列的固定並行度,適合任務執行時長不均勻的場景.
線程池參數介紹
1、ThreadPoolExecutor的重要參數
一、corePoolSize:核心線程數
* 核心線程會一直存活,及時沒有任務須要執行
* 當線程數小於核心線程數時,即便有線程空閒,線程池也會優先建立新線程處理
* 設置allowCoreThreadTimeout=true(默認false)時,核心線程會超時關閉
二、queueCapacity:任務隊列容量(阻塞隊列)
* 當核心線程數達到最大時,新任務會放在隊列中排隊等待執行
三、maxPoolSize:最大線程數
* 當線程數>=corePoolSize,且任務隊列已滿時。線程池會建立新線程來處理任務
* 當線程數=maxPoolSize,且任務隊列已滿時,線程池會拒絕處理任務而拋出異常
四、 keepAliveTime:線程空閒時間
* 當線程空閒時間達到keepAliveTime時,線程會退出,直到線程數量=corePoolSize
* 若是allowCoreThreadTimeout=true,則會直到線程數量=0
五、allowCoreThreadTimeout:容許核心線程超時
六、rejectedExecutionHandler:任務拒絕處理器
* 兩種狀況會拒絕處理任務:
- 當線程數已經達到maxPoolSize,切隊列已滿,會拒絕新任務
- 當線程池被調用shutdown()後,會等待線程池裏的任務執行完畢,再shutdown。若是在調用shutdown()和線程池真正shutdown之間提交任務,會拒絕新任務
* 線程池會調用rejectedExecutionHandler來處理這個任務。若是沒有設置默認是AbortPolicy,會拋出異常
* ThreadPoolExecutor類有幾個內部實現類來處理這類狀況:
- AbortPolicy 丟棄任務,拋運行時異常
- CallerRunsPolicy 執行任務
- DiscardPolicy 忽視,什麼都不會發生
- DiscardOldestPolicy 從隊列中踢出最早進入隊列(最後一個執行)的任務
* 實現RejectedExecutionHandler接口,可自定義處理器
2、ThreadPoolExecutor執行順序
線程池按如下行爲執行任務
1. 當線程數小於核心線程數時,建立線程。
2. 當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。
3. 當線程數大於等於核心線程數,且任務隊列已滿
- 若線程數小於最大線程數,建立線程
- 若線程數等於最大線程數,拋出異常,拒絕任務
4.JUC經常使用工具
1. JUC 簡介
在 Java 5.0 提供了 java.util.concurrent(簡稱JUC)包,在此包中增長了在併發編程中很經常使用的工具類,
用於定義相似於線程的自定義子系統,包括線程池,異步 IO 和輕量級任務框架;還提供了設計用於多線程上下文中
的 Collection 實現等;
2. volatile 關鍵字
volatile 關鍵字: 當多個線程進行操做共享數據時,能夠保證內存中的數據是可見的;相較於 synchronized 是一種
較爲輕量級的同步策略;
volatile 不具有"互斥性";
volatile 不能保證變量的"原子性";
考察點
1.理解線程的同步與互斥原理
臨界資源與臨界區的概念
重量級鎖,輕量級鎖,自旋鎖,偏向鎖,讀寫鎖,重入鎖的概念
2.掌握線程安全相關機制
CAS,synchronized,lock同步方式的實現原理
ThreadLocal是線程獨享的局部變量,使用弱引用的ThreadLocalMap保存ThreadLocal變量
3.瞭解JUC工具的使用場景與實現原理
ReentrantLock,ConcurrentHash,LongAdder的實現方式
4.熟悉線程池的原理,使用場景,經常使用配置
慎用無界隊列,可能會有OOM的風險
5.理解線程的同步與異步,阻塞與非阻塞
同步異步:任務是否在同一個線程中執行
阻塞非阻塞:異步執行任務時,線程是否會阻塞等待結果
加分項
1.結合實際項目經驗或實際案例介紹原理
2.解決多線程問題的排查思路與經驗
多線程併發執行可能會致使一些問題:
安全性問題:在單線程系統上正常運行的代碼,在多線程環境中可能會出現意料以外的結果。
活躍性問題:不正確的加鎖、解鎖方式可能會致使死鎖or活鎖問題。
性能問題:多線程併發即多個線程切換運行,線程切換會有必定的消耗而且不正確的加鎖。
3.熟悉經常使用的線程分析工具與方法
如Jstack
排查過程以下:
一、top #查看java進程佔用cpu、內存狀況
二、ps (ps -mp java進程ID -o THREAD,tid,time) #查看java線程佔用cpu、優先級、時間等
第二步找出該進程內最耗費CPU的線程,能夠使用ps -Lfp pid或者ps -mp pid -o THREAD, tid, time或者top -Hp pid
三、將可疑線程的tid轉成16進制(經過windows自動程序員計算器轉換)
四、jstack -l java進程ID > aa.log #導出java進程堆棧信息
五、在裏面查找16進制的線程id,看對應堆棧代碼日誌。能夠知道這個線程在作什麼事情
4.瞭解Java8對JUC的加強
用LongAdder替換AtomicLong,更適合併發度高的場景
5.瞭解Reactive異步編程思想
真題彙總
1.如何實現生產者消費者模型?
可利用鎖,信號量,線程通訊,阻塞隊列等方法實現
用synchronized對存儲加鎖,而後用object原生的wait() 和 notify()作同步。
用concurrent.locks.Lock,而後用condition的await() 和signal()作同步。
直接使用concurrent.BlockingQueue。
使用PipedInputStream/PipedOutputStream。
使用信號量semaphore。
個人理解,生產者消費者模式,其實只要保證在存儲端同一時刻只有一個線程讀或寫就不會有問題,而後再去考慮線程同步。方法1 2 5都比較相似,都是加鎖來限制同一時刻只能有一個讀或寫。而方法3 4實際上是在存儲內部去保證讀和寫的惟一的,最低層確定仍是經過鎖機制來實現的,java底層代碼都封裝好了而已。
我本身嘗試寫了下前三種,代碼以下:
synchronized版本
import java.util.LinkedList;
import java.util.Queue;
public class ProducerAndConsumer {
private final int MAX_LEN = 10;
private Queue<Integer> queue = new LinkedList<Integer>();
class Producer extends Thread {
@Override
public void run() {
producer();
}
private void producer() {
while(true) {
synchronized (queue) {
while (queue.size() == MAX_LEN) {
queue.notify();
System.out.println("當前隊列滿");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(1);
queue.notify();
System.out.println("生產者生產一條任務,當前隊列長度爲" + queue.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Consumer extends Thread {
@Override
public void run() {
consumer();
}
private void consumer() {
while (true) {
synchronized (queue) {
while (queue.size() == 0) {
queue.notify();
System.out.println("當前隊列爲空");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll();
queue.notify();
System.out.println("消費者消費一條任務,當前隊列長度爲" + queue.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
ProducerAndConsumer pc = new ProducerAndConsumer();
Producer producer = pc.new Producer();
Consumer consumer = pc.new Consumer();
producer.start();
consumer.start();
}
}
lock版實現,使用了condition作線程之間的同步。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* version 1 doesn't use synchronized to improve performance
*/
public class ProducerAndConsumer1 {
private final int MAX_LEN = 10;
private Queue<Integer> queue = new LinkedList<Integer>();
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
class Producer extends Thread {
@Override
public void run() {
producer();
}
private void producer() {
while(true) {
lock.lock();
try {
while (queue.size() == MAX_LEN) {
System.out.println("當前隊列滿");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(1);
condition.signal();
System.out.println("生產者生產一條任務,當前隊列長度爲" + queue.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
}
}
class Consumer extends Thread {
@Override
public void run() {
consumer();
}
private void consumer() {
while (true) {
lock.lock();
try {
while (queue.size() == 0) {
System.out.println("當前隊列爲空");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll();
condition.signal();
System.out.println("消費者消費一條任務,當前隊列長度爲" + queue.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) {
ProducerAndConsumer pc = new ProducerAndConsumer();
Producer producer = pc.new Producer();
Consumer consumer = pc.new Consumer();
producer.start();
consumer.start();
}
}
BlockingQueue版實現
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerAndConsumer {
private BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(10);
class Producer extends Thread {
@Override
public void run() {
producer();
}
private void producer() {
while(true) {
try {
queue.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生產者生產一條任務,當前隊列長度爲" + queue.size());
try {
Thread.sleep(new Random().nextInt(1000)+500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer extends Thread {
@Override
public void run() {
consumer();
}
private void consumer() {
while (true) {
try {
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消費者消費一條任務,當前隊列長度爲" + queue.size());
try {
Thread.sleep(new Random().nextInt(1000)+500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ProducerAndConsumer pc = new ProducerAndConsumer();
Producer producer = pc.new Producer();
Consumer consumer = pc.new Consumer();
producer.start();
consumer.start();
}
}
2.如何理解線程的同步與異步,阻塞與非阻塞?
一、進程和線程的概念
進程:運行中的應用程序稱爲進程,擁有系統資源(cpu、內存)
線程:進程中的一段代碼,一個進程中能夠有多段代碼。自己不擁有資源(共享所在進程的資源);
在java中,程序入口被自動建立爲主線程,在主線程中能夠建立多個子線程。
多進程: 在操做系統中能同時運行多個任務(程序)
多線程: 在同一應用程序中有多個功能流同時執行
已經有了進程,爲何還會須要線程呢?主要緣由以下:
許多應用程序中,同時發生着多個活動。將這些應用程序分解成多個準並行的線程,程序設計的模型會變成更加簡單。
因爲線程比進程進行更加輕量,建立和取消更加容易。
若是程序是IO密集型,那麼多線程執行可以加快程序的執行速度。(若是是CPU密集型,則沒有這個優點)
在多CPU系統中,多線程是能夠真正並行執行的。
二、線程的主要特色
①、不能以一個文件名的方式獨立存在在磁盤中;
②、不能單獨執行,只有在進程啓動後纔可啓動;
③、線程能夠共享進程相同的內存(代碼與數據)。
三、多線程原理
同一時間,CPU只能處理1條線程,只有1條線程在工做(執行)
多線程併發(同時)執行,實際上是CPU快速地在多條線程之間調度(切換)
若是CPU調度線程的時間足夠快,就形成了多線程併發執行的假象
思考:若是線程很是很是多,會發生什麼狀況?
CPU會在N多線程之間調度,CPU會累死,消耗大量的CPU資源
每條線程被調度執行的頻次會下降(線程的執行效率下降)
四、線程的主要用途
①、利用它能夠完成重複性的工做(如實現動畫、聲音等的播放)。
②、從事一次性較費時的初始化工做(如網絡鏈接、聲音數據文件的加載)。
③、併發執行的運行效果(一個進程多個線程)以實現更復雜的功能
五、多線程(多個線程同時運行)程序的優缺點
優勢:
①、能夠減輕系統性能方面的瓶頸,由於能夠並行操做;
②、提升CPU的處理器的效率,在多線程中,經過優先級管理,能夠使重要的程序優先操做,提升了任務管理的靈活性;
另外一方面,在多CPU系統中,能夠把不一樣的線程在不一樣的CPU中執行,真正作到同時處理多任務。
缺點:
一、開啓線程須要佔用必定的內存空間(默認狀況下,主線程佔用1M,子線程佔用512KB),若是開啓大量的線程,會佔用大量的內存空間,下降程序的性能
二、線程越多,CPU在調度線程上的開銷就越大
三、程序設計更加複雜:好比線程之間的通訊、多線程的數據共享
六、多線程的生命週期
線程狀態:
與人有生老病死同樣,線程也一樣要經歷新建、就緒、運行(活動)、阻塞和死亡五種不一樣的狀態。這五種狀態均可以經過Thread類中的方法進行控制。
建立並運行線程:
① 新建狀態(New Thread):在Java語言中使用new 操做符建立一個線程後,該線程僅僅是一個空對象,它具有類線程的一些特徵,但此時系統沒有爲其分配資源,這時的線程處於建立狀態。
線程處於建立狀態時,可經過Thread類的方法來設置各類屬性,如線程的優先級(setPriority)、線程名(setName)和線程的類型(setDaemon)等。
② 就緒狀態(Runnable):使用start()方法啓動一個線程後,系統爲該線程分配了除CPU外的所需資源,使該線程處於就緒狀態。此外,若是某個線程執行了yield()方法,那麼該線程會被暫時剝奪CPU資源,從新進入就緒狀態。
③ 運行狀態(Running):Java運行系統經過調度選中一個處於就緒狀態的線程,使其佔有CPU並轉爲運行狀態。此時,系統真正執行線程的run()方法。
a) 能夠經過Thread類的isAlive方法來判斷線程是否處於就緒/運行狀態:當線程處於就緒/運行狀態時,isAlive返回true,當isAlive返回false時,可能線程處於阻塞狀態,也可能處於中止狀態。
④ 阻塞和喚醒線程
阻塞狀態(Blocked):一個正在運行的線程因某些緣由不能繼續運行時,就進入阻塞 狀態。這些緣由包括:
等待阻塞:當線程執行了某個對象的wait()方法時,線程會被置入該對象的等待集中,直到執行了該對象的notify()方法wait()/notify()方法的執行要求線程首先得到該對象的鎖。
同步阻塞:當多個線程試圖進入某個同步區域(同步鎖)時,沒能進入該同步區域(同步鎖)的線程會被置入鎖定集(鎖池)中,直到得到該同步區域的鎖,進入就緒狀態。
其餘阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新轉入就緒狀態。
⑤ 死亡狀態(Dead):線程在run()方法執行結束後進入死亡狀態。此外,若是線程執行了interrupt()或stop()方法,那麼它也會以異常退出的方式進入死亡狀態。
七、終止線程的三種方法
① 使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止,推薦使用。
② 使用stop方法強制終止線程(這個方法不推薦使用,由於stop和suppend、resume同樣,也可能發生不可預料的結果)。
③ 使用interrupt方法中斷線程。
八、概念解釋
8.1 同步/異步, 它們是消息的通知機制
同步:
所謂同步,當前程序執行完才能執行後面的程序,程序執行時按照順序執行,須要等待。平時寫的代碼基本都是同步的;
異步:
異步的概念和同步相對。
程序沒有等到上一步程序執行完才執行下一步,而是直接往下執行,前提是下面的程序沒有用到異步操做的值,異步的實現方式基本上都是多線程(定時任務也可實現,可是狀況少)。
8.2 阻塞/非阻塞, 它們是程序在等待消息(無所謂同步或者異步)時的狀態.
阻塞:
阻塞調用是指調用結果返回以前,當前線程會被掛起。函數只有在獲得結果以後纔會返回。
有人也許會把阻塞調用和同步調用等同起來,實際上他是不一樣的。
對於同步調用來講,不少時候當前線程仍是激活的,只是從邏輯上當前函數沒有返回而已。
非阻塞:
非阻塞和阻塞的概念相對應,指在不能馬上獲得結果以前,該函數不會阻塞當前線程,而會馬上返回。
簡單示例:老張燒水
老張愛喝茶,廢話不說,煮開水。
出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。
1 老張把水壺放到火上,立等水開。(同步阻塞)
老張以爲本身有點傻
2 老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有。(同步非阻塞)
老張仍是以爲本身有點傻,因而變高端了,買了把會響笛的那種水壺。水開以後,能大聲發出嘀~~~~的噪音。
3 老張把響水壺放到火上,立等水開。(異步阻塞)(本能夠坐着等通知的卻非要當即等着,實際不大會出現這種狀況,異步異步阻塞沒有實際意義)
老張以爲這樣傻等意義不大
4 老張把響水壺放到火上,去客廳看電視,水壺響以前再也不去看它了,響了再去拿壺。(異步非阻塞)
所謂同步異步,只是對於水壺而言。
普通水壺,同步;響水壺,異步。
雖然都能幹活,但響水壺能夠在本身完工以後,提示老張水開了。這是普通水壺所不能及的。
同步只能讓調用者去輪詢本身(狀況2中),形成老張效率的低下。
所謂阻塞非阻塞,僅僅對於老張而言。
立等的老張,阻塞;看電視的老張,非阻塞。
狀況1和狀況3中老張就是阻塞的,媳婦喊他都不知道。雖然3中響水壺是異步的,可對於立等的老張沒有太大的意義。因此通常異步是配合非阻塞使用的,這樣才能發揮異步的效用。
同步阻塞關係:
線程阻塞(祥見多線程介紹)除了程序主動調用休眠外常見的就是程序遇到同步代碼塊,同一時間不能並行執行,當有多個請求了出現線程等待的狀況即爲阻塞。
同步緣由:
阻塞源於同步代碼塊,首先須要弄清楚什麼時候須要同步,須要同步的地方是由於多個線程操做了同一個變量,致使在並行執行時變量值的混亂,故須要加同步鎖來實現同一時間只能有同一個線程執行同步代碼塊中的程序,若是不涉及多線程操做同一個變量的狀況是不須要使用同步的,在多線程編程時儘可能避免操做公共變量來避免阻塞。
九、Java同步機制有4種實現方式
ThreadLocal
synchronized( )
wait() 與 notify()
volatile
目的:都是爲了解決多線程中的對同一變量的訪問衝突
9.1 ThreadLocal
ThreadLocal 保證不一樣線程擁有不一樣實例,相同線程必定擁有相同的實例,即爲每個使用該變量的線程提供一個該變量值的副本,每個線程均可以獨立改變本身的副本,而不是與其它線程的副本衝突。
優點:提供了線程安全的共享對象與其它同步機制的區別:同步機制是爲了同步多個線程對相同資源的併發訪問,是爲了多個線程之間進行通訊;而ThreadLocal 是隔離多個線程的數據共享,從根本上就不在多個線程之間共享資源,這樣固然不須要多個線程進行同步了。
9.2 volatile
volatile 修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。並且當成員變量發生變化時,強迫線程將變化值回寫到共享內存。
優點:這樣在任什麼時候刻,兩個不一樣的線程老是看到某個成員變量的同一個值。
原因:Java 語言規範中指出,爲了得到最佳速度,容許線程保存共享成員變量的私有拷貝,並且只當線程進入或者離開同步代碼塊時才與共享成員變量的原始值對比。這樣當多個線程同時與某個對象交互時,就必需要注意到要讓線程及時的獲得共享成員變量的變化。而 volatile 關鍵字就是提示 VM :對於這個成員變量不能保存它的私有拷貝,而應直接與共享成員變量交互。
使用技巧:在兩個或者更多的線程訪問的成員變量上使用 volatile 。當要訪問的變量已在synchronized 代碼塊中,或者爲常量時,沒必要使用。
線程爲了提升效率,將某成員變量(如A)拷貝了一份(如B),線程中對A的訪問其實訪問的是B。只在某些動做時才進行A和B的同步,所以存在A和B不一致的狀況。volatile就是用來避免這種狀況的。 volatile告訴jvm,它所修飾的變量不保留拷貝,直接訪問主內存中的(讀操做多時使用較好;線程間須要通訊,本條作不到)
Volatile 變量具備 synchronized 的可見性特性,可是不具有原子特性。這就是說線程可以自動發現 volatile 變量的最新值。Volatile 變量可用於提供線程安全,可是隻能應用於很是有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。
只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:
對變量的寫操做不依賴於當前值;
該變量沒有包含在具備其餘變量的不變式中。
9.3 sleep() vs wait()
sleep是線程類(Thread)的方法,致使此線程暫停執行指定時間,把執行機會給其餘線程,可是監控狀態依然保持,到時後會自動恢復。調用sleep不會釋放對象鎖。
wait() 是Object類的方法,對此對象調用wait方法致使本線程放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象發出notify方法(或notifyAll)後本線程才進入對象鎖定池準備得到對象鎖進入運行狀態。
(若是變量被聲明爲volatile,在每次訪問時都會和主存一致;若是變量在同步方法或者同步塊中被訪問,當在方法或者塊的入口處得到鎖以及方法或者塊退出時釋放鎖時變量被同步。)
3.線程池處理任務的流程?
流程解釋爲:
當線程池新加入一個線程時,首先判斷當前線程數,是否小於coreSize,若是小於,則執行步驟2,不然執行3
建立新線程添加到線程池中,跳轉結束
判斷當前線程池等待隊列是否已滿,若已滿,則跳轉至步驟5
加入等待隊列,等待線程池空閒,跳轉結束
判斷當前線程數是否已達到maximumPoolSize,若未達到,則跳轉至步驟7
執行線程池拒絕策略,跳轉結束
建立一個新線程,執行任務
跳轉結束
4.wait與sleep有什麼不一樣?
wait是Object方法,sleep是Thread方法
wait會釋放鎖,sleep不會
wait要在同步塊中使用,sleep在任何地方使用
wait不須要捕獲異常,sleep須要
5.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對象能夠同時綁定對個對象。
6.讀寫鎖適用與什麼場景,ReentrantReadWriteLock是如何實現的?
互斥鎖:mutex,用於保證在任什麼時候刻,都只能有一個線程訪問該對象。當獲取鎖操做失敗時,線程會進入睡眠,等待鎖釋放時被喚醒
自旋鎖:spinlock,在任什麼時候刻一樣只能有一個線程訪問對象。可是當獲取鎖操做失敗時,不會進入睡眠,而是會在原地自旋,直到鎖被釋放。這樣節省了線程從睡眠狀態到被喚醒期間的消耗,在加鎖時間短暫的環境下會極大的提升效率。但若是加鎖時間過長,則會很是浪費CPU資源
讀寫鎖:rwlock,區分讀和寫,處於讀操做時,能夠容許多個線程同時得到讀操做。可是同一時刻只能有一個線程能夠得到寫鎖。其它獲取寫鎖失敗的線程都會進入睡眠狀態,直到寫鎖釋放時被喚醒。
注意:寫鎖會阻塞其它讀寫鎖。當有一個線程得到寫鎖在寫時,讀鎖也不能被其它線程獲取;寫優先於讀,當有線程由於等待寫鎖而進入睡眠時,則後續讀者也必須等待
適用於讀取數據的頻率遠遠大於寫數據的頻率的場合。
RCU:即read-copy-update,在修改數據時,首先須要讀取數據,而後生成一個副本,對副本進行修改。修改完成後,再將老數據update成新的數據。使用RCU時,讀者幾乎不須要同步開銷,既不須要得到鎖,也不使用原子指令,不會致使鎖競爭,所以就不用考慮死鎖問題了。而對於寫者的同步開銷較大,它須要複製被修改的數據,還必須使用鎖機制同步並行其它寫者的修改操做。在有大量讀操做,少許寫操做的狀況下效率很是高
信號量:semaphore,是用於線程間同步的,當一個線程完成操做後就經過信號量通知其它線程,而後別的線程就能夠繼續進行某些操做了。
信號量和互斥鎖的區別:semaphore
信號量是用於線程間同步的,而互斥鎖是用於線程的互斥的
互斥量的獲取和釋放都是在同一線程中完成的,pthread_mutex_lock(),pthread_mutex_unlock()。而信號量的得到和釋放是在不一樣的線程的操做爲sem_wait(),sempost();
互斥量的值只能爲0和1,而信號量只要value>0,其它線程就能夠sem_wait成功,成功後信號量value減一。若value值不大於0,則sem_wait阻塞,直到sem_post釋放後value加1。所以信號量的值能夠爲非負整數
讀寫鎖頂層接口是 ReadWriteLock , 實現類是 ReentrantReadWriteLock;
其實讀寫鎖,運用沒什麼好說的. 同時讀,沒有安全性問題, 因此不用到互斥, 而讀寫, 或寫寫則涉及到安全性問題, 就要互斥.直接上代碼吧
package com.zz.amqp1.locktest;
import lombok.Data;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Description:讀寫鎖
* <p>
* 1 寫寫, 讀寫, 須要互斥
* 2 讀讀, 不須要互斥
*
* </p>
* User: zhouzhou
* Date: 2018-12-27
* Time: 2:20 PM
*/
public class TestReadWriteLock {
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo.set(new Random().nextInt(101));
},"write:" + (i+1)).start();
}
for (int i = 0; i < 100; i++) {
new Thread(()->{
demo.read();
}).start();
}
}
}
@Data
class ReadWriteLockDemo{
private int number = 0;
// 可重入讀寫鎖
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void read(){
lock.readLock().lock();
try {
System.out.println("當前的值爲" + this.number);
} finally {
lock.readLock().unlock();
}
}
public void set(Integer value){
lock.writeLock().lock();
try {
System.out.println(String.format("當前線程{%s}正在進行寫操做{%s}", Thread.currentThread().getName(),value));
this.setNumber(value);
} finally {
lock.writeLock().unlock();
}
}
}
7.線程之間如何通訊?
wait/notify機制
共享變量的synchronize&lock同步機制
線程之間的通訊方式:共享內存(隱式通訊),消息傳遞(顯示通訊)
線程之間同步:在共享內存的併發模型中,同步是顯示作的;在消息傳遞的併發模型中,因爲消息的發生必須在消息接收以前,因此同步是隱式作的
8.保證線程安全的方法有哪些?
CAS,synchronized,lock,ThreadLocal
第一種實現線程安全的方式
同步代碼塊
第二種 方式
同步方法
第三種 方式
Lock鎖機制, 經過建立Lock對象,採用lock()加鎖,unlock()解鎖,來保護指定的代碼塊
最後總結:
因爲synchronized是在JVM層面實現的,所以系統能夠監控鎖的釋放與否;而ReentrantLock是使用代碼實現的,系統沒法自動釋放鎖,須要在代碼中的finally子句中顯式釋放鎖lock.unlock()。
另外,在併發量比較小的狀況下,使用synchronized是個不錯的選擇;可是在併發量比較高的狀況下,其性能降低會很嚴重,此時ReentrantLock是個不錯的方案。
補充:
在使用synchronized 代碼塊時,能夠與wait()、notify()、nitifyAll()一塊兒使用,從而進一步實現線程的通訊。
其中,wait()方法會釋放佔有的對象鎖,當前線程進入等待池,釋放cpu,而其餘正在等待的線程便可搶佔此鎖,得到鎖的線程便可運行程序;線程的sleep()方法則表示,當前線程會休眠一段時間,休眠期間,會暫時釋放cpu,但並不釋放對象鎖,也就是說,在休眠期間,其餘線程依然沒法進入被同步保護的代碼內部,當前線程休眠結束時,會從新得到cpu執行權,從而執行被同步保護的代碼。
wait()和sleep()最大的不一樣在於wait()會釋放對象鎖,而sleep()不會釋放對象鎖。
notify()方法會喚醒由於調用對象的wait()而處於等待狀態的線程,從而使得該線程有機會獲取對象鎖。調用notify()後,當前線程並不會當即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼所有執行完畢,纔會釋放對象鎖。JVM會在等待的線程中調度一個線程去得到對象鎖,執行代碼。
須要注意的是,wait()和notify()必須在synchronized代碼塊中調用。
notifyAll()是喚醒全部等待的線程。
9.如何儘量提升多線程併發性能?
減小臨界區範圍
使用ThreadLocal
減小線程切換
使用讀寫鎖或CopyOnWrite
在Java程序中,多線程幾乎已經無處不在。與單線程相比,多線程程序的設計和實現略微困難,但經過多線程,咱們卻能夠得到多核CPU帶來的性能飛躍,從這個角度說,多線程是一種值得嘗試的技術。那麼如何寫出高效的多線程程序呢?
有關多線程的誤區:線程越多,性能越好
很多初學者可能認爲,線程數量越多,那麼性能應該越好。由於程序給咱們的直觀感覺老是這樣。一個兩個線程可能跑的很難,線程一多可能就快了。但事實並不是如此。由於一個物理CPU一次只能執行一個線程,多個線程則意味着必須進行線程的上下文切換,而這個代價是很高的。所以,線程數量必須適量,最好的狀況應該是N個CPU使用N個線程,而且讓每一個CPU的佔有率都達到100%,這種狀況下,系統的吞吐量才發揮到極致。但現實中,不太可能讓單線程獨佔CPU達到100%,一個廣泛的願意是由於IO操做,不管是磁盤IO仍是網絡IO都是很慢的。線程在執行中會等待,所以效率就下來了。這也就是爲何在一個物理核上執行多個線程會感受效率高了,對於程序調度來講,一個線程等待時,也正是其它線程執行的大好機會,所以,CPU資源獲得了充分的利用。
儘量不要掛起線程
多線程程序免不了要同步,最直接的方法就是使用鎖。每次只容許一個線程進入臨界區,讓其它相關線程等待。等待有2種,一種是直接使用操做系統指令掛起線程,另一種是自旋等待。在操做系統直接掛起,是一種簡單粗暴的實現,性能較差,不太適用於高併發的場景,由於隨之而來的問題就是大量的線程上下文切換。若是能夠,嘗試一下進行有限的自旋等待,等待不成功再去掛起線程也不遲。這樣頗有可能能夠避免一些無謂的開銷。JDK中ConcurrentHashMap的實現裏就有一些自旋等待的實現。此外Java虛擬機層面,對synchronized關鍵字也有自旋等待的優化。
善用「無鎖」
阻塞線程會帶來性能開銷,所以,一種提供性能的方案就是使用無鎖的CAS操做。JDK中的原子類,如AtomicInteger正是使用了這種方案。在高併發環境中,衝突較多的狀況下,它們的性能遠遠好於傳統的鎖操做(《實戰Java高併發程序設計》 P158)。
處理好「僞共享」問題
你們知道,CPU有一個高速緩存Cache。在Cache中,讀寫數據的最小單位是緩存行,若是2個變量存在一個緩存行中,那麼在多線程訪問中,可能會相互影響彼此的性能。所以將變量存放於獨立的緩存行中,也有助於變量在多線程訪問是的性能提高(《實戰Java高併發程序設計》 P200),大量的高併發庫都會採用這種技術。
10.ThreadLocal用來解決什麼問題,ThreadLocal是如何實現的?
不是用來解決多線程共享變量問題,而是用來解決線程數據隔離問題
ThreadLocal是一個解決線程併發問題的一個類,用於建立線程的本地變量,咱們知道一個對象的全部線程會共享它的全局變量,因此這些變量不是線程安全的,咱們能夠使用同步技術。可是當咱們不想使用同步的時候,咱們能夠選擇ThreadLocal變量。
每一個線程都會擁有他們本身的Thread變量,他們能夠使用get/set方法去獲取他們的默認值或者在線程內部改變他們的值。ThreadLocal實例一般是但願他們同線程狀態關聯起來是private static屬性。
底層實現主要是存有一個map,以線程做爲key,泛型做爲value,能夠理解爲線程級別的緩存。每個線程都會得到一個單獨的map。
11.死鎖的產生條件,如何分析線程是否有死鎖?
其實,真正理清楚了死鎖產生的必要的條件,寫出一個死鎖的例子並不困難。那麼,就java的多線程而言,產生死鎖有哪些必要條件呢?
1,必須有2個或以上的線程。一個線程是不會產生死鎖的,它頂多產生等待。
2,必須有2個臨界資源,即,必須有2個鎖。這也是死鎖產生的必要的條件。當只有一個臨界資源,或者說只有一個鎖時,當一個線程獲取了鎖,另外一個線程雖然暫時沒法獲取鎖,但它至多也就是須要進行等待。而不會陷入死鎖。
3,兩個線程,每一個線程都獲取了其中的一個鎖,但爲了完成工做,還需對方的另外一個鎖。這種狀況下,纔會產生死鎖。這種狀況也稱爲循環等待。
4,不可剝奪。
以上即爲死鎖產生的必要條件。
避免死鎖能夠歸納成三種方法:
固定加鎖的順序(針對鎖順序死鎖)
開放調用(針對對象之間協做形成的死鎖)
使用定時鎖-->tryLock()
12.在實際工做中遇到過什麼樣的併發問題,如何發現排查並解決的?異步協程方式解決