「進程」java
進程的本質是一個正在執行的程序,程序運行時系統會建立一個進程,而且「給每一個進程分配獨立的內存地址空間,用來保證每一個進程地址不會相互干擾」。面試
同時,在 CPU 對進程作時間片的切換時,保證進程切換過程當中仍然要從進程切換以前運行的位置處開始執行。因此進程一般還會包括程序計數器、堆棧指針。數據庫
相對好理解點的案例:電腦上開啓QQ就是開啓一個進程、打開IDEA就是開啓一個進程、瀏打開覽器就是開啓一個進程.....編程
當咱們的電腦開啓你太多的運用(QQ,微信,瀏覽器、PDF、word、IDEA等)後,電腦就很容易出現卡頓,甚至死機,這裏最主要緣由就是CPU一直不停地切換致使的。數組
下圖是單核CPU狀況下,多進程之間的切換:瀏覽器
有了進程之後,可讓操做系統從宏觀層面實現多應用併發。安全
而併發的實現是經過 CPU 時間片不端切換執行的,對於單核 CPU來講,在任意一個時刻只會有一個進程在被CPU 調度。微信
既然是生命週期,那麼就頗有可能會有階段性的或者狀態的,好比人的一輩子同樣:數據結構
❝精子和卵子結合---> 嬰兒---> 小孩--> 成年--> 中年--> 老年-->去世多線程
❞
關於線程的生命週期網上有不同的答案,有說五種也有說六種。
Java中線程確實有6種,這是有理有據的,能夠看看java.lang.Thread
類中有個這麼一個枚舉。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
這就是Java線程對應的狀態,組合起來就是Java中一個線程的生命週期。下面是這個枚舉的註釋:
每種狀態簡單說明:
借用網上的這張圖,這張圖描述的很清楚了,這裏就不在囉嗦。
咱們常常會據說某個類是線程安全,某個類不是線程安全的。那麼究竟什麼叫作線程安全呢?
咱們引用《Java Concurrency in Practice》裏面的定義:
❝在不使用額外同步的狀況下,多個線程訪問一個對象時,不論線程之間如何交替執行或者在調用方進行任何其它的協調操做,調用這個對象的行爲都能獲得正確的結果,那麼這個對象是線程安全的。
❞
也能夠這麼理解:
❝多個線程訪問同一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘操做,調用這個對象的行爲均可以得到正確的結果,那麼這個對象就是線程安全的。或者說:一個類或者程序所提供的接口對於線程來講是原子操做或者多個線程之間的切換不會致使該接口的執行結果存在二義性,也就是說咱們不用考慮同步的問題。
❞
能夠簡單的理解爲:「你隨便怎麼調用,出了問題算我輸」。
這個定義對於類來講是十分嚴格的,即便是Java API
中標爲線程安全的類也很難知足這個要求。
好比Vector是標記爲線程安全的,但實際上並不能知足這個條件,舉個例子:
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
public synchronized void removeElementAt(int index) {
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}
//....基本上全部方法都是synchronized修飾的
}
來看下面一個案例:
判斷Vector中第0個元素是否是空字符,若是是空字符就將其刪除。
package com.java.tian.blog.utils;
import java.util.Vector;
public class SynchronizedDemo{
static Vector<String> vct = new Vector<String>();
public void remove() {
if("".equals(vct.get(0))) {
vct.remove(0);
}
}
public static void main(String[] args) {
vct.add("");
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.remove();
}
},"線程1").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.remove();
}
},"線程2").start();
}
}
上面的邏輯看起來沒有問題,其實是有可能致使錯誤的:假設第0個元素是空字符,判斷的時候獲得的結果是true。
兩個線程同時執行上面的remove方法,(「極端的狀況」)都「可能」get到的是"",而後都去刪除第0個元素,這個元素有可能已經被其它線程刪除了,所以Vector不是絕對線程安全的。(上面這個案例只是作演示而已,在你的業務代碼裏面這麼寫的話,線程安全真的就不能靠Vector來保證了)。
一般狀況下咱們說的線程安全都是相對線程安全,相對線程安全只要求調用單個方法的時候不須要同步就能夠獲得正確的結果,但數多個方法組合調用的時候也是有可能致使多線程問題的。
若是想讓上面的操做執行正確,咱們須要在調用Vector方法的時候添加額外的同步操做:
package com.java.tian.blog.utils;
import java.util.Vector;
public class SynchronizedDemo {
static Vector<String> vct = new Vector<String>();
public void remove() {
synchronized (vct) {
//synchronized (SynchronizedDemo.class) {
if ("".equals(vct.get(0))) {
vct.remove(0);
}
}
}
public static void main(String[] args) {
vct.add("");
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.remove();
}
}, "線程1").start();
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.remove();
}
}, "線程2").start();
}
}
根據Vector的源代碼可知:Vector的每一個方法都使用了synchronized關鍵字修飾,所以鎖對象就是這個對象自己。在上面的代碼中咱們嘗試獲取的也是vct對象的鎖,能夠和vct對象的其它方法互斥,所以這樣作能夠保證獲得正確的結果。
若是Vector內部使用的是其它鎖同步的,並封裝了鎖對象,那麼咱們不管如何都沒法正確執行這個「先判斷後修改」的操做。
假設被封裝的對象鎖爲obj,get()和remove()方法對應的鎖都是obj,而整個操做過程獲取的是vct的鎖,一個線程調用get()方法成功後就釋放了obj的鎖,這時這個線程只持有vct的鎖,而其它線程能夠得到obj的鎖並搶先一步刪除了第0個元素。
Java爲開發者提供了不少強大的工具類,這些工具類裏面有的是線程安全的,有的不是線程安全的。在這裏咱們列舉幾個面試常考的:
線程安全的類:Vector、Hashtable、StringBuffer
非線程安全的類:ArrayList、HashMap、StringBuilder
有人可能會反問:爲何Java不把全部的類都設計成線程安全的呢?這樣對於咱們開發者來講豈不是更爽嗎?咱們就不用考慮什麼線程安全問題了。
事情都是具備兩面性的,得到線程安全可是性能會有所降低,畢竟鎖的開銷是擺在那裏的。線程不安全可是性能會有所提高,具體場景還得看業務更偏向於哪個。
一個問題引起的思考:
public class SynchronizedDemo {
static int count;
public void incre() {
try {
//每一個線程都睡一會,模仿業務代碼
Thread.sleep(100 );
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronizedDemo.incre();
}
}).start();
}
try {
//讓主線程等待全部線程執行完畢
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
上面這段代碼輸出的結果是不肯定的,結果是小於等於1000。
1000線程都去對count進行++操做。
對象在內存中的存儲能夠分爲 3 塊區域,分別是對象頭、實例數據和對齊填充。
其中,對象頭包括兩部份內容,一部分是對象自己的運行時數據,像 GC 分代年齡、哈希碼、鎖狀態標識等等,官方稱之爲「Mark Word」,若是忽略壓縮指針的影響,這部分數據在 32 位和 64 位的虛擬機中分別佔 32 位和 64 位。
可是對象須要存儲的運行時數據不少,32 位或者 64 位都不必定能存的下,考慮到虛擬機的空間效率,這個 Mark Word 被設計成一個非固定的數據結構,它會根據對象的狀態複用本身的存儲空間,對象處於不一樣狀態的時候,對應的 bit 表示的含義可能會不同,見下圖,以 32 位 Hot Spot 虛擬機爲例:
從上圖中咱們能夠看出,若是對象處於未鎖定狀態(無鎖態),那麼 Mark Word 的 25 位用於存儲對象的哈希碼,4 位用於存儲對象分代年齡,1 位固定爲 0,兩位用於存儲鎖標誌位。
這個圖對於理解後面提到的輕量級鎖、偏向鎖是很是重要的,固然咱們如今能夠先着重考慮對象處於重量級鎖狀態下的狀況,也就是鎖標誌位爲 10。同時咱們看到,無鎖態和偏向鎖狀態下,2 位鎖標誌位都是「01」,留有 1 位表示是否可偏向,咱們姑且叫它「偏向位」。
「注」:對象頭的另外一部分則是類型指針,虛擬機能夠經過這個指針來確認該對象是哪一個類的實例。可是咱們要注意,並非全部的虛擬機都必須以這種方式來肯定對象的元數據信息。對象的訪問定位通常有句柄和直接指針兩種,若是使用句柄的話,那麼對象的元數據信息能夠直接包含在句柄中(固然也包括對象實例數據的地址信息),也就不必將這些元數據和實例數據存儲在一塊兒了。至於實例數據和對齊填充,這裏暫不作討論。
前面咱們提到了,Java 中的每一個對象都與一個 monitor 相關聯,當鎖標誌位爲 10 時,除了 2bit 的標誌位,指向的就是 monitor 對象的地址(仍是以 32 位虛擬機爲例)。這裏咱們能夠翻閱一下 OpenJDK 的源碼,若是咱們須要下載openJDK的源碼:
找到。這裏先看一下markOpp.hpp
文件。該文件的相對路徑爲:
openjdk\hotspot\src\share\vm\oops
下圖是文件中的註釋部分:
咱們能夠看到,其中描述了 32 位和 64 位下 Mark World 的存儲狀態。也能夠看到64位下,前25位是沒有使用的。
咱們也能夠看到 markOop.hpp 中定義的鎖狀態枚舉,對應咱們前面提到的無鎖、偏向鎖、輕量級鎖、重量級鎖(膨脹鎖)、GC 標記等:
enum { locked_value = 0,//00 輕量級鎖
unlocked_value = 1,//01 無鎖
monitor_value = 2,//10 重量級鎖
marked_value = 3,//11 GC標記
biased_lock_pattern = 5 //101 偏向鎖,1位偏向標記和2位狀態標記(01)
};
從註釋中,咱們也能夠看到對其的簡要描述,後面會咱們詳細解釋:
這裏咱們的重心仍是是重量級鎖,因此咱們看看源碼中 monitor 對象是如何定義的,對應的頭文件是 objectMonitor.hpp,文件路徑爲:
openjdk\hotspot\src\share\vm\runtime
咱們來簡單看一下這個 objectMonitor.hpp 的定義:
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,//等待線程數
_recursions = 0;//重入次數
_object = NULL;
_owner = NULL;//持有鎖的線程(邏輯上,實際上除了THREAD,還多是Lock Record)
_WaitSet = NULL;//線程wait以後會進入該列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//等待獲取鎖的線程列表,和_EntryList配合使用
FreeNext = NULL ;
_EntryList = NULL ;//等待獲取鎖的線程列表,和_cxq配合使用
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;//當前持有者是否爲THREAD類型,若是是輕量級鎖膨脹而來,尚未enter的話,
//_owner存儲的可能會是Lock Record
_previous_owner_tid = 0;
}
簡單的說,當多個線程競爭訪問同一段同步代碼塊時,若是線程獲取到了 monitor,那麼就會把 _owner 設置成當前線程,若是是重入的話,_recursions 會加 1,若是獲取 monitor 失敗,則會進入 _cxq
隊列。
鎖被釋放時,_cxq
中的線程會被移動到 _EntryList
中,而且喚醒_EntryList
隊首線程。固然,選取喚醒線程有幾個不一樣的策略(Knob_QMode
),仍是後面結合源碼解析。
「注」:_cxq
和 _EntryList
本質上是ObjectWaiter
類型,它本質上實際上是一個雙向鏈表 (具備先後指針),只是在使用的時候不必定要當作雙向鏈表使用,好比 _cxq
是當作單向鏈表使用的,_EntryList
是當作雙向鏈表使用的。
致使線程上下文切換的有兩種類型:
自發性上下文切換是指線程由 Java 程序調用致使切出,在多線程編程中,執行調用上圖中的方法或關鍵字,經常就會引起自發性上下文切換。
非自發性上下文切換指線程因爲調度器的緣由被迫切出。常見的有:線程被分配的時間片用完,虛擬機垃圾回收致使或者執行優先級的問題致使。
注意兩個隊列:
讓當前線程進入等待狀態,當別的其餘線程調用notify()或者notifyAll()方法時,當前線程進入就緒狀態
wait方法必須在同步上下文中調用,例如:同步方法塊或者同步方法中,這也就意味着若是你想要調用wait方法,前提是必須獲取對象上的鎖資源
當wait方法調用時,當前線程將會釋放已獲取的對象鎖資源,並進入等待隊列,其餘線程就能夠嘗試獲取對象上的鎖資源。
wait是Object中的方法
做爲一個Java開發多年的人來講,確定多多少少熟悉一些鎖,或者聽過一些鎖。今天就來作一個鎖相關總結。
顧名思義,他就是很悲觀,把事情都想的最壞,是指該鎖只能被一個線程鎖持有,若是A線程獲取到鎖了,這時候線程B想獲取鎖只能排隊等待線程A釋放。
在數據庫中這樣操做:
select user_name,user_pwd from t_user for update;
顧名思義,樂觀,人樂觀就是什麼是都想得開,船到橋頭天然直。樂觀鎖就是我都以爲他們都沒有拿到鎖,只有我拿到鎖了,最後再去問問這個鎖真的是我獲取的嗎?是就把事情給幹了。
典型的表明:CAS
=Compare and Swap
先比較哈,資源是否是我以前看到的那個,是那我就把他換成個人。不是就算了。
在Java中java.util.concurrent.atomic
包下面的原子變量就是使用了樂觀鎖的一種實現方式CAS
實現。
一般都是 使用version、時間戳等來比較是否已被其餘線程修改過。
在樂觀鎖與悲觀鎖的選擇上面,主要看下二者的區別以及適用場景就能夠了。
「響應效率」
若是須要很是高的響應速度,建議採用樂觀鎖方案,成功就執行,不成功就失敗,不須要等待其餘併發去釋放鎖。樂觀鎖並未真正加鎖,效率高。一旦鎖的粒度掌握很差,更新失敗的機率就會比較高,容易發生業務失敗。
「衝突頻率」
若是衝突頻率很是高,建議採用悲觀鎖,保證成功率。衝突頻率大,選擇樂觀鎖會須要屢次重試才能成功,代價比較大。「重試代價」
若是重試代價大,建議採用悲觀鎖。悲觀鎖依賴數據庫鎖,效率低。更新失敗的機率比較低。
樂觀鎖若是有人在你以前更新了,你的更新應當是被拒絕的,可讓用戶重新操做。悲觀鎖則會等待前一個更新完成。這也是區別。
顧名思義,是公平的,先來先得,FIFO;必須遵照排隊規則。不能僭越。多個線程按照申請鎖的順序去得到鎖,線程會直接進入隊列去排隊,永遠都是隊列的第一位才能獲得鎖。
在ReentrantLock
中默認使用的非公平鎖,可是能夠在構建ReentrantLock
實例時候指定爲公平鎖。
ReentrantLock fairSyncLock = new ReentrantLock(true);
假設線程 A 已經持有了鎖,這時候線程 B 請求該鎖將會被掛起,當線程 A 釋放鎖後,假如當前有線程 C 也須要獲取該鎖,那麼在公平鎖模式下,獲取鎖和釋放鎖的步驟爲:
「優勢」
全部的線程都能獲得資源,不會餓死在隊列中。
「缺點」
吞吐量會降低不少,隊列裏面除了第一個線程,其餘的線程都會阻塞,CPU喚醒阻塞線程的開銷會很大。
顧名思義,老子才無論大家誰先排隊的,也就是平時你們在生活中很討厭的。生活中排隊的不少,上車排隊、坐電梯排隊、超市結帳付款排隊等等。可是不是每一個人都會遵照規則站着排隊,這就對站着排隊的人來講就不公平了。等搶不到後再去乖乖排隊。
多個線程去獲取鎖的時候,會直接去嘗試獲取,獲取不到,再去進入等待隊列,若是能獲取到,就直接獲取到鎖。
上面說過在ReentrantLock
中默認使用的非公平鎖,兩種方式:
ReentrantLock fairSyncLock = new ReentrantLock(false);
或者:
ReentrantLock fairSyncLock = new ReentrantLock();
均可以實現非公平鎖。
「優勢」
能夠減小CPU喚醒線程的開銷,總體的吞吐效率會高點,CPU也沒必要取喚醒全部線程,會減小喚起線程的數量。
「缺點」
你們可能也發現了,這樣可能致使隊列中間的線程一直獲取不到鎖或者長時間獲取不到鎖,致使餓死。
獨享鎖也叫排他鎖/互斥鎖,是指該鎖一次只能被一個線程鎖持有。若是線程T對數據A加上排他鎖後,則其餘線程不能再對A加任何類型的鎖。得到排他鎖的線程既能讀數據又能修改數據。JDK
中的synchronized和JUC
中Lock的實現類就是互斥鎖。
共享鎖是指該鎖可被多個線程所持有。若是線程T對數據A加上共享鎖後,則其餘線程只能對A再加共享鎖,不能加排他鎖。得到共享鎖的線程只能讀數據,不能修改數據。
對於ReentrantLock
而言,其是獨享鎖。可是對於Lock的另外一個實現類ReadWriteLock
,其讀鎖是共享鎖,其寫鎖是獨享鎖。
AQS
來實現的,經過實現不一樣的方法,來實現獨享或者共享。若當前線程執行中已經獲取了鎖,若是再次獲取該鎖時,就會獲取不到被阻塞。
public class RentrantLockDemo {
public synchronized void test(){
System.out.println("test");
}
public synchronized void test1(){
System.out.println("test1");
test();
}
public static void main(String[] args) {
RentrantLockDemo rentrantLockDemo = new RentrantLockDemo();
//線程1
new Thread(() -> rentrantLockDemo.test1()).start();
}
}
當一個線程執行test1()
方法的時候,須要獲取rentrantLockDemo
的對象鎖,在test1
方法彙總又會調用test方法,可是test()的調用是須要獲取對象鎖的。
可重入鎖也叫「遞歸鎖」,指的是同一線程外層函數得到鎖以後,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。
ThreadLocal
名字中有個Thread表示線程,Local表示本地,咱們就理解爲線程本地變量了。
先看看ThreadLocal
的總體:
最關心的三個公有方法:set、get、remove。
public ThreadLocal() {
}
構造方法裏沒有任何邏輯處理,就是簡單的建立一個實例。
源碼爲:
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
//這是什麼鬼?
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
先看看ThreadLocalMap是個什麼東東:
ThreadLocalMap
是ThreadLocal
的靜態內部類。
set方法總體爲:
ThreadLocalMap構造方法:
//這個屬性是ThreadLocal的,就是獲取hashcode(這列頗有學問,可是咱們的目的不是他)
private final int threadLocalHashCode = nextHashCode();
private Entry[] table;
private static final int INITIAL_CAPACITY = 16;
//Entry是一個弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//數組默認大小爲16
table = new Entry[INITIAL_CAPACITY];
//len 爲2的n次方,以ThreadLocal的計算的哈希值按照Entry[]取模(爲了更好的散列)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//設置閾值(擴容閾值)
setThreshold(INITIAL_CAPACITY);
}
而後咱們看看map.set()方法中是如何處理的:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//len 爲2的n次方,以ThreadLocal的計算的哈希值按照Entry[]取模
int i = key.threadLocalHashCode & (len-1);
//找到ThreadLocal對應的存儲的下標,若是當前槽內Entry不爲空,
//即當前線程已經有ThreadLocal已經使用過Entry[i]
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 當前佔據該槽的就是當前的ThreadLocal ,更新value結束
if (k == key) {
e.value = value;
return;
}
//當前卡槽的弱引用可能會回收了,key:null value:xxxObject ,
//需清理Entry原來的value ,便於垃圾回收value,且將新的value 放在該槽裏,結束
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//在這以前沒有ThreadLocal使用Entry[i],並進行值存儲
tab[i] = new Entry(key, value);
//累計Entry所佔的個數
int sz = ++size;
// 清理key 爲null 的Entry ,可能須要擴容,擴容長度爲原來的2倍,並須要進行從新hash
if (!cleanSomeSlots(i, sz) && sz >= threshold){
rehash();
}
}
從上面這個set方法,咱們就大體能夠把這三個進行一個關聯了:
Thread
、ThreadLocal
、ThreadLocalMap
。
expungeStaleEntry
方法代碼裏有點大,因此這裏就貼了出來。
//刪除陳舊entry的核心方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;//刪除value
tab[staleSlot] = null;//刪除entry
size--;//map的size自減
// 遍歷指定刪除節點,全部後續節點
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//key爲null,執行刪除操做
e.value = null;
tab[i] = null;
size--;
} else {//key不爲null,從新計算下標
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {//若是不在同一個位置
tab[i] = null;//把老位置的entry置null(刪除)
// 從h開始日後遍歷,一直到找到空爲止,插入
while (tab[h] != null){
h = nextIndex(h, len);
}
tab[h] = e;
}
}
}
return i;}