多線程複習
1.進程和線程
1.1定義
(1)進程:操作系統中一個程序的執行週期
(2)線程:進程中的一個任務。一個進程中可以包含n個線程(main()->主線程)
1.2進程與線程的關係
- 每個進程擁有自己的一整套變量,是操作系統中資源分配的最小的單位。
線程依託於進程存在,多個線程共享進程的資源,os中任務調度的基本單位。
- 啓動、撤銷一個進程的開銷要比啓動、撤銷以一個線程大的多(線程輕量級)
- 進程間通信遠比線程間通信複雜的多;
- 沒有進程就沒有線程,進程一旦終止,其內的線程全部取消;
1.3線程的狀態
2.多線程實現
Java中創建線程的四種方式:
(1)繼承Thread類
(2)實現Runnable接口
(3)實現Callable接口
(4)使用線程池(推薦)
2.1繼承Thread類
java.lang.Thread是線程操作的核心類
2.1.1出現版本
JDK1.0
2.1.2實現方法
直接繼承Thread類而後覆寫類中的run()方法(相當於主方法,一個線程的入口就是run方法)
無論哪種方式實現多線程,線程啓動一律調用Thread類提供的start()方法。
2.1.3start()方法解析
1.首先檢查線程狀態是否爲0(線程默認狀態爲0表示爲啓動),如果線程已經啓動再次調用start()方法會拋出IllegalThreadStateException異常。所以一個線程的start()方法只能調用一次。
2.private native void start0();通過start0()方法真正將線程啓動。
3.JVM調用start0()方法進行資源分配與系統調度,**準備好資源啓動線程後回調run()**來執行線程的具體任務!
2.1.4線程啓動調用方法流程
start()->start0()->JVM_StartThread()->run_method_name()->run()
2.1.5侷限性
單繼承問題
2.2實現Runnable接口
java.lang.Runnable
2.2.1出現版本
JDK1.0
2.2.2實現方法
Java中Thread類本身也實現了Runnable接口,與用戶自定義的線程類共同組成代理設計模式
其中Thread類實現輔助操作,包括線程的資源調度等任務;自定義線程類完成真實業務·
實現Runnable接口,覆寫run()
public Thread(Runnable target){}
public static void main(String[] args) {
MyThread1 mt1 = new MyThread1();
MyThread1 mt2 = new MyThread1();
MyThread1 mt3 = new MyThread1();
Thread t1 = new Thread(mt1);
Thread t2 = new Thread(mt2);
Thread t3 = new Thread(mt3);
t1.start();
t2.start();
t3.start();
}
2.2.3繼承Thread類與實現Runnable接口區別
- 繼承Thread類有單繼承侷限,相對而言實現Runnable接口更加靈活,並且Thread類本身也實現了Runnable輔助真實業務類
- 實現Runnable接口可以更好的實現程序共享的概念(Thread類也可以,)
2.3實現Callable接口
java.util.concurrent
2.3.1出現版本
JDK1.5
2.3.2實現方法
實現Callable接口,覆寫call()方法
Future:可接收call()方法的返回值
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.產生Callable對象
MyThread3 mt = new MyThread3();
//2.產生FutureTask對象
FutureTask<String> ft = new FutureTask<>(mt);
new Thread(ft).start();
new Thread(ft).start();
new Thread(ft).start();
//3.接收call()返回值,FutureTask的get()繼承自Future接口中的get()
System.out.println(ft.get());
}
2.3.3實現Runnable接口與實現Callable接口的區別
- 實現Callable接口,線程執行任務後有返回值;而實現Runnable接口,線程執行後沒有返回值
3.多線程常用方法
3.1線程的命名與取得
- 取得當前JVM正在執行的線程對象:public static native Thread currentThread();
- 線程命名:
- 創建線程時設置名稱:public Thread (Runnable target,String name)
- 設置線程名稱:public final synchronized void setName(String name)
- 取得線程名:public final String getName()
3.2線程休眠
3.2.1定義
讓線程暫緩執行,等到了預計時間再回復執行
public static native void sleep(long millis) throws InterruptedException{};
3.2.2狀態變換
運行狀態---->阻塞狀態---->休眠結束---->就緒狀態
3.2.3注意
(1)線程休眠會立即交出CPU,讓CPU去執行其他任務。線程休眠不會釋放對象鎖;
(2)多個線程進入一個方法,不是同時休眠的;
3.3線程讓步
3.3.1定義
暫停當前正在執行的線程對象,並執行其他線程
public static native void yield();
3.3.2狀態變換
運行狀態<---->就緒狀態
3.3.3注意
(1)yield()會讓當前線程交出CPU,但不一定會立即交出。yield()交出CPU後只能讓擁有相同優先級的線程有獲取CPU的機會,不會釋放對象鎖
3.4線程等待
3.4.1定義
等待該線程終止。
若線程1需要等待線2執行完畢後再恢復執行,可以在線程1中調用線程2的join();在哪個線程調用,哪個線程阻塞,等待線程執行完畢再恢復執行
3.4.2狀態轉換
運行狀態->阻塞狀態->就緒狀態
3.4.3注意
(1)join()方法只是對Object類wait()做了一層包裝而已;
(2)交出cpu,釋放對象鎖
3.5線程停止
3.5.1三種方式停止線程
-
設置標記位,可以使線程正常退出(推薦)
-
使用stop方法強制讓線程退出,但是該方法不安全已經被@Deprecated
-
使用Thread類提供的interrupt()中斷線程(就是系統設置了一個標記位,直接使用即可)
interrupt方法只是將線程狀態改爲中斷狀態而已,它不會中斷一個正在運行的線程;
3.5.2設置標記位
class MyThread02 implements Runnable{
private int books = 0;
private boolean flag = true;
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while (flag == true){
System.out.println(Thread.currentThread().getName()+"賣出書號:"+(books--));
}
System.out.println("線程停止");
}
}
public class test02 {
public static void main(String[] args) throws InterruptedException {
MyThread02 mt1 = new MyThread02();
Thread t1 = new Thread(mt1);
t1.start();
Thread.sleep(1000);
mt1.setFlag(false);
}
}
3.5.3stop方法
強行關閉線程,不管有沒有執行完循環(不推薦使用)
廢棄的原因:不安全,stop()會解除由線程獲得的所有鎖定,當在一個線程對象上調用了stop(),這個線程對象所運行的線程就會立即停止,可能會產生不完整數據信息
3.5.4interrupt()方法
調用interrupt()
-
如果在線程中沒有調用wait() sleep() join(),則標記線程(isInterrupt = true);
並不會真正中斷線程,只是簡單的將線程的狀態置爲interrupt而已,我們可以根據此狀態來進一步確定如何處理線程;
Thread類提供的public boolean isInterrupted()可以檢測當前線程狀態是否爲中斷狀態
class MyThread1 implements Runnable{
boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
boolean bool = Thread.currentThread().isInterrupted();
if(bool){
System.out.println("線程進入中斷狀態");
return;
}
System.out.println("當前線程狀態爲"+bool);
System.out.println(Thread.currentThread().getName()+","+(i++));
}
System.out.println("線程停止");
}
}
public class threadInterrupt {
public static void main(String[] args) throws InterruptedException {
MyThread1 mt1 = new MyThread1();
Thread thread = new Thread(mt1);
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
一秒後設置線程狀態爲中斷狀態,調用isInterrupt()返回值爲true,進入if語句,執行操作,輸出線程進入中斷狀態;
- 如果在線程中調用了wait() sleep() join(),當調用interrupt()會拋出InterruptedException異常,同時將線程狀態還原(isInterrupted = false)
class MyThread1 implements Runnable{
boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
try {
Thread.sleep(200);
boolean bool = Thread.currentThread().isInterrupted();
System.out.println("當前線程狀態爲"+bool);
System.out.println(Thread.currentThread().getName()+","+(i++));
} catch (InterruptedException e) {
System.out.println("拋出中斷異常");
return;
}
}
System.out.println("線程停止");
}
}
public class threadInterrupt {
public static void main(String[] args) throws InterruptedException {
MyThread1 mt1 = new MyThread1();
Thread thread = new Thread(mt1);
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
輸出:一秒後中斷
當前線程狀態爲false
Thread-0,0
當前線程狀態爲false
Thread-0,1
當前線程狀態爲false
Thread-0,2
當前線程狀態爲false
Thread-0,3
拋出中斷異常
3.6線程的優先級
3.6.1定義
線程的優先級越高越有可能先執行
3.6.2常用方法
- 設置優先級:public final void setPriority(int newPriority)
- 取得優先級:public final int getPriority()
- 最高優先級:public final static int MAX_PRIORITY = 10
- 中等優先級:public final static int NORM_PRIORITY = 5
- 最低優先級:public final static int MIN_PRIORITY = 1
3.6.3注意
- 主方法是一箇中等優先級
- 線程具有繼承性(比如在A線程中啓動B線程,那麼B和A的優先級將是一樣的)
3.7守護線程(後臺線程)
(1)java中的兩種線程:用戶線程和守護線程
(2)判斷線程種類的方法:isDaemon();返回true,是守護線程;返回false,是用戶線程
(3)設置守護線程:setDaemon()
(4)典型的守護線程:垃圾回收線程
(5)只有當最後一個非守護線程結束時,守護線程纔會隨着JVM一同停止工作
(6)設置線程A爲守護線程,必須在start()前執行
(7)java中啓動的線程默認爲用戶線程,包括main線程
4.線程的同步和死鎖
多線程編程3大問題
分工:線程的分工
同步:線程間通信(線程合作)
互斥:多線程併發時,某一時刻只能有一個線程訪問共享資源
一個鎖保護一個相應資源,不同鎖保護的不同的對象/資源
java中鎖的實現
(1)內建鎖:synchronized關鍵字
(2)Lock
4.1核心問題
每一個線程對象輪番搶佔資源帶來的問題
4.2同步處理
4.2.1synchronized關鍵字
處理同步的兩種模式:同步代碼塊、同步方法
- 同步代碼塊:要使用同步代碼塊必須要設置一個鎖定的對象(類對象),一般可以鎖當前對象this
- 同步方法:在方法上添加synchronized關鍵字,表示此方法在任意時刻只能有一個線程進入
4.2.2兩種模式
同步代碼塊
(1)鎖類的實例對象
synchronized(this){}
(2)鎖類對象(class對象)----全局鎖
synchronized(類名稱.class){}—鎖的是當前類的反射對象(全局唯一)
(3)任意實例對象
String lock = “”;
synchronized(lock){}
同步方法
-
普通方法+synchronized
鎖的是當前對象
-
synchronized+靜態方法
鎖的是的類對象----全局鎖,效果等同於同步代碼塊的鎖類對象
栗子
1.銀行賬戶
如何保護毫無關係的資源?使用多把鎖鎖住不同的資源
class Account{
int sal;//餘額
String password;//密碼
private Object salLock = new Object();
private Object passwordLock = new Object();
public int getMoney(){
synchronized(salLock){}
}
public int setMoney(){
synchronized(salLock){}
}
public String getPassword(){
synchronized(passwordLock){}
}
public void setPassword(){
synchronized(passwordLock){}
}
}
此時可以同時取錢和查看密碼;
2.轉賬
由於轉賬涉及兩個賬戶間的sal操作,因此需要將兩個賬戶同時鎖定。由於方法上的synchronized只能鎖一個對象,因此鎖不住轉賬操作
如何保護有關係的屬性?使用一把鎖鎖住多個資源
A->B 100
A -= 100
B += 100
public void zhuanzhang(Account target){
synchronized(Accout.class){
this.sal -= 100;
target.sal += 100;
}
}
此時可是實現A->B 100; B -> A 200 轉賬操作
4.2.3synchronized底層實現
在使用synchronized時必須保證鎖定對象必須爲Object以及其子類對象
synchronized使用的是JVM層級別的MonitorEnter和MonitorExit實現
這兩個指令都必須獲取對象的同步監視器Monitor
對象鎖機制
monitorenter:
當執行monitorenter時;
(1)如果目標鎖對象的monitor計數器爲0,表示此對象沒有被任何其他對象所持有。此時JVM會將該鎖對象的持有線程設置爲當前線程,並且將計數器+1;
(2)如果目標鎖對象的計數器不爲0,判斷鎖對象的持有線程是否是當前線程,如果是,再次將計數器,則+1;如果鎖對象的持有線程不是當前線程,已經被其他線程佔用,當前線程需要阻塞等待,直至持有線程釋放鎖。
monitorexit:
當執行monitorexit時;
JVM會將鎖對象的計數器-1.當計數器值減爲0時,代表該鎖對象已經被釋放
-
執行同步代碼塊後首先要執monitorenter指令,退出時執行monitorexit指令。
-
使用內建鎖(synchronized)進行同步關鍵在於要獲取指定鎖對象的monitor對象,
-
當線程獲取monitor後才能繼續向下執行,否則就只能等待。這個獲取過程是互斥的,即同一時刻只有一個線程能夠獲取到對象monitor;
-
通常一個monitorenter指令會包含若干個monitorexit指令;
原因:JVM需要確保鎖在正常執行路徑以及異常執行路徑上都能夠正確的解鎖
-
當使用synchronized標記方法時,編譯後字節碼中方法的訪問標記多了一個ACC_SYNCHRONIZED.
該標記表示,進入該方法時,JVM需要進行monitorenter操作,退出該方法時,無論是否正常返回,JVM均需要進行monitorexit操作。
可重入鎖:
當執行MonitorEnter時,對象的monitor計數器值不爲0,但是持有線程恰好是當前線程,此時將Monitor計數器值+1,當前線程繼續進入同步方法或代碼塊
可重入性:當線程A拿到對象鎖後,可以進入對應類的所有方法
class MyThreads implements Runnable{
@Override
public void run() {
test1();
}
public synchronized void test1(){
//保證第一個線程進入test1()
if(Thread.currentThread().getName().equals("A"))
{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程A進入同步方法test1()");
test2();
}
}
public synchronized void test2(){
System.out.println(Thread.currentThread().getName()+"進入同步方法test2()");
}
}
public class TestMyThread {
public static void main(String[] args) throws InterruptedException {
MyThreads myThreads = new MyThreads();
Thread threadA = new Thread(myThreads,"A");
threadA.start();
}
}
out:線程A進入同步方法test1()
A進入同步方法test2()
互斥:test1()只允許進入線程A,while循環一直進行,線程B永遠進不了test1(),更進不了test2()
class MyThreads implements Runnable{
@Override
public void run() {
test1();
test2();
}
public synchronized void test1(){
//保證第一個線程進入test1()
if(Thread.currentThread().getName().equals("A"))
{
while (true){
System.out.println("線程A進入同步方法test1()");
}
}
}
public synchronized void test2(){
if(Thread.currentThread().getName().equals("B")){
System.out.println("線程B進入同步方法test2()");
}
}
}
public class TestMyThread {
public static void main(String[] args) throws InterruptedException {
MyThreads myThreads = new MyThreads();
Thread threadA = new Thread(myThreads,"A");
Thread threadB = new Thread(myThreads,"B");
threadA.start();
threadB.start();
}
}
out:線程A進入同步方法test1()無限循環下去
4.2.4synchronized的優化(瞭解)
synchronized:互斥
優化讓每個線程通過同步代碼塊時速度提高
4.2.4.1CAS操作
(1)定義
CAS(無鎖操作):Compare And Swap-樂觀鎖
悲觀鎖(JDK1.6之前的內建鎖):假設每一次執行同步代碼塊均會產生衝突,所以當線程獲取鎖成功,會阻塞其他嘗試獲取該鎖的線程;之前synchronized獲取鎖失敗,將線程掛起
樂觀鎖(Lock):假設所有線程訪問共享資源時不會出現衝突,既然不會出現衝突自然就不會阻塞其他線程。線程不會出現阻塞狀態。
CAS(無鎖操作),使用CAS叫做比較交換來判斷是否出現衝突,出現衝突就重試當前操作,直到不衝突爲止
(2)操作過程
一般來說,CAS交換過程:(V,O,N)
V:內存中地址存放的實際值
O:預期值(舊值)(我以爲它是啥值)
N:更新後的值(我期望它是啥值)
當執行CAS後,
(1)如果V == O,即舊值與內存中實際值相等,表示上次修改該值後沒有任何線程再次修改此值,此時可以成功的將內存中的值修改爲N;
(2)如果V != O,表示該內存中的值已經被其他線程做了修改,此時返回最新的值V,再次嘗試修改變量;
當多個線程使用CAS操作同一個變量時,只有一個線程會成功,併成功更新變量值,其餘線程均會失敗。失敗線程會重新嘗試或將線程掛起(阻塞)
元老級的內建鎖(synchronized)最主要的問題:當存在線程競爭的情況下會出現線程阻塞以及喚醒帶來的性能問題,對應互斥同步(阻塞同步),效率很低。
而CAS並不是武斷的將線程掛起,會嘗試若干次CAS操作,並非進行耗時的掛起與喚醒操作,因此稱爲非阻塞式同步
(3)CAS問題
-
ABA問題
解決思路:沿用數據庫的樂觀鎖機制,添加版本號1A->2B->3A
JDK1.5提供atomicAtomicStampedReference類來解決CAS的ABA問題
-
自旋(CAS)會浪費大量的處理器資源(CPU)
與線程阻塞相比,自旋會浪費大量的CPU資源。因爲此時線程仍處於運行狀態,只不過跑的是無用指令,期望在無用指令時,鎖能被釋放出來。
解決思路:自適應自旋
根據以往自旋等待時能否獲取到鎖,來動態調整自旋的時間(循環嘗試數量)
(1)如果在上一次自旋時獲取到鎖,則此次自旋時間稍微變長一點;
(2)如果在上一次自旋結束還沒有獲取到鎖,此次自旋時間稍微短一點;
-
公平性
處於阻塞狀態的線程無法立刻競爭被釋放的鎖;而處於自旋狀態的線程很有可能先獲取到鎖
內建鎖synchronized無法實現公平性
Lock體系可以實現公平鎖
4.2.4.2Java對象頭
JDK1.6之後對內建鎖做了優化(新增偏向鎖、輕量級鎖)
Java對象頭–Java對象的對象頭。
Java對象頭裏的markword裏默認的存放的對象的Hashcode、分代年齡和鎖標記位
4.2.4.3鎖的4種狀態
鎖狀態在對象頭mark word中
無鎖狀態0 01(第一個0特指無鎖狀態)
偏向鎖1 01(第一個1特指該鎖是偏向鎖)
輕量級鎖 00
重量級鎖(JDK1.6之前)10
這四種狀態隨着競爭狀態逐漸升級,鎖可以升級但不可以降級,爲了提高獲得鎖與釋放鎖的效率
-
無鎖:只有一個線程,不存在競爭
-
偏向鎖:JDK1.6之後默認的synchronized
最樂觀的鎖,從始至終只有一個線程請求一把鎖。
引入原因:HotSpot作者發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得,
爲了讓鎖的代價更低而引入了偏向鎖
偏向鎖獲取
當一個線程訪問同步代碼塊或者同步方法並獲取鎖時,會在對象頭和線程棧幀中的鎖記錄中記錄存儲偏向鎖的線程ID。偏向鎖複製對象頭中的線程ID到棧幀中
以後該線程再次進入同步塊時不再需要CAS來加鎖和解鎖,只需簡單測試一下對象頭的mark word中偏向鎖線程ID是否是當前線程ID
如果成功,表示線程已經獲取到鎖直接進入代碼塊運行。
如果測試失敗,檢查當前偏向鎖狀態是否爲0,
(1)如果爲0,採用CAS操作將偏向鎖字段設置爲1,並且更新自己的線程ID到mark word字段中。
(2)如果爲1,表示此時偏向鎖已經被別的線程獲取。則此線程不斷嘗試使用CAS(自旋)獲取偏向鎖或者將偏向鎖撤銷,升級爲輕量級鎖(升級概率較大)
偏向鎖的撤銷
偏向鎖使用一種等待競爭出現才釋放鎖的機制,當有其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放偏向鎖。
小tips:偏向鎖的撤銷開銷較大,需要等待線程進入全局安全點safepoint(當前線程在CPU上沒有執行任何有用字節碼)。
偏向鎖從JDK6之後默認開啓,但是它在應用程序啓動幾秒後才激活。
-XX:BiasedLockingStartupDelay = 0,將延遲關閉,JVM一啓動就激活偏向鎖
-XX:-UseBiasedLocking=false,關閉偏向鎖,程序默認進入輕量級鎖。
-
輕量級鎖
輕量級鎖:多個線程在不同時間段請求同一把鎖,也就是基本不存在鎖競爭。
針對這種狀況,JVM採用輕量級鎖來避免線程的阻塞以及喚醒。
加鎖:
1.線程在執行同步代碼塊之前,JVM先在當前線程的棧幀中創建用於存儲鎖記錄的空間,並將對象頭的mark word字段直接複製到此空間內(輕量級鎖複製的markword字段(包括對象),偏向鎖只複製鎖對象頭中的線程ID)
2.然後線程嘗試使用CAS將對象頭的markword替換爲指向鎖記錄的指針(指向當前線程),
3.如果成功,表示獲取到輕量級鎖;
如果失敗,表示其他線程競爭輕量級鎖,當前線程便使用自旋(CAS)不斷嘗試。
釋放:
解鎖時,會使用CAS將複製的mark word替換回對象頭
如果成功,表示沒有競爭發生
如果失敗,表示當前鎖存在競爭,進一步膨脹爲重量級鎖
-
重量級鎖
JDK1.6之前synchronized都是重量級鎖,將線程阻塞掛起
重量級鎖會阻塞、喚醒請求加鎖的線程。針對的是多個線程同一時刻競爭同一把鎖的情況,JVM採用自適應自旋,來避免線程在面對非常小的同步代碼塊時,仍會被阻塞以及喚醒
鎖總結:
重量級鎖:會阻塞、喚醒請求加鎖的線程。針對的是多個線程同一時刻競爭同一把鎖的情況,JVM採用自適應自旋,來避免線程在面對非常小的同步代碼塊時,仍會被阻塞以及喚醒。
輕量級鎖:採用CAS操作,將鎖對象的標記字段替換爲指向線程的指針,存儲着鎖對象原本的標記字段。針對的是多個線程在不同時間段申請同一把鎖的情況。此後運行中會繼續採用CAS操作來加鎖(因爲是多個線程,可能在下一個時間會有新的線程獲取鎖,所以需要在該線程下一次加鎖再次進行CAS操作)。
偏向鎖:只會在第一次請求時採用CAS操作,在鎖對象的mark work字段中記錄下當前線程的ID,此後運行中持有偏向鎖的線程不再有加鎖過程)。針對的是鎖僅會被同一線程持有。
4.2.4.4死鎖
死鎖一旦出現,整個程序就會中斷執行,過多的同步會造成死鎖,對於資源的上鎖一定要注意不要成“環”。
class Pen{
private String pen = "筆";
public String getPen() {
return pen;
}
}
class Book{
private String book = "本子";
public String getBook() {
return book;
}
}
public class DeadLock {
private static Pen pen = new Pen();
private static Book book = new Book();
public static void main(String[] args) {
new DeadLock().testDeadLock();
}
private void testDeadLock() {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (pen){
System.out.println(Thread.currentThread().getName()+":我有筆,就不給你");
synchronized (book){
System.out.println(Thread.currentThread().getName()+":把你的本子給我");
}
}
}
},"pen");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (book){
System.out.println(Thread.currentThread().getName()+":我有本子,就不給你");
synchronized (pen){
System.out.println(Thread.currentThread().getName()+":把你的筆給我");
}
}
}
},"book");
thread1.start();
thread2.start();
}
}
解釋:當thread1線程啓動後,鎖了pen對象,thread2線程啓動後,只能鎖book對象,需要等thread1釋放pen對象後,thread2才能鎖pen對象;需要等thread2釋放book對象後,thread1才能鎖book對象。
4.3ThreadLocal
4.3.1定義
用於提供線程局部變量,在多線程環境可以保證各個線程的變量獨立於其他線程裏的變量。
相當於線程的private static ,爲每一個線程創建一個單獨的變量副本
4.3.2ThreadLocal與同步機制的區別
ThreadLocal:保證多線程環境下數據的獨立性
同步機制:保證多線程環境下數據的一致性
4.3.3ThreadLocal的簡單實現
public class testThreadLocal {
private static String string;
private static ThreadLocal<String> threadStr = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
string = "thread-main";
threadStr.set("ThreadLocal-main");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadStr.get()); //1.out:null
string = "thread-A";
threadStr.set("ThreadLocal-A");
System.out.println(threadStr.get()); //2.out:ThreadLocal-A
}
});
thread.start();
thread.join();
System.out.println("=======================");
System.out.println(string); //3.out:thread-A
System.out.println(threadStr.get()); //4.out:ThreadLocal-main
}
}
//輸出1:此時thread.str爲new ThreadLocal<>()的副本,並且線程thread中並沒有賦值給threadStr
//輸出2:thread中threadStr被賦值爲ThreadLocal-A
//輸出3:變量string在線程中值已經改變
//輸出4:threadStr在主線程中被賦值爲ThreadLocal-main,即ThreadLocal類爲每一個線程創建一個變量副本
4.3.4給ThreadLocal類的對象設置默認參數
private static ThreadLocal<String> threadStr = ThreadLocal.withInitial(()-> {
return "hello";
});
4.3.5ThreadLocalMap詳解
每個線程的ThreadLocalMap屬於自己,ThreadLocalMap中維護的值也是屬於線程自己的。這就保證了ThreadLocal類型的變量在每個線程中是獨立的,在多線程環境下是不會相互影響的。
存儲對象Entry
Entry用於保存一個鍵值對
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry使用弱引用的原因:
避免ThreadLocal沒有被回收而導致的內存泄露
4.3.6ThreadLocal的防護措施
在調用ThreadLocal的get(),set(),remove()的時候都會清除當前線程中所有key爲null的value,這樣可以降低內存泄露的概率。所以我們在使用ThreadLocal的時候,每次用完ThreadLocal都調用remove()方法,清除數據,防止內存泄漏。
5.Lock體系
5.1面試題-synchronized和Lock
面1:Java中實現線程"鎖"的方式:synchronized和Lock
面2:synchronized和Lock的關係
共同點:
synchronized和Lock都是對象鎖,都支持可重入鎖
synchronized和Lock
區別:
1.synchronized是JVM級別的鎖,屬於Java中的關鍵字,使用synchronized,加鎖和解鎖都是隱式的
Lock是Java層面的鎖,加鎖和解鎖都需要顯示使用
2.Lock可以提供一些synchronized不具備的而功能,如響應中斷、隨時獲取、非阻塞式獲取、公平鎖、共享鎖(讀寫鎖)
3.synchronized的等待隊列只有一個;而同一個Lock可以擁有多個等待隊列(多個Condition對象隊列),可以進一步提高效率,減少線程阻塞與喚醒帶來的開銷(喚醒了不該喚醒的線程)
獲取一個lock鎖的Condition的隊列-lock.newCondition():產生一個新的Condition隊列
面3:到底用synchronized還是Lock?
1.若無特殊的應用場景,推薦使用synchronized,其使用方便(隱式的加減鎖),並且由於synchronized是JVM層面的實現,在之後的JDK還有對其優化的空間;
2.若要使用公平鎖、讀寫鎖、超時獲取鎖等特殊場景,纔會考慮使用Lock;
5.2產生原因
解決死鎖問題
死鎖發生的四個必要條件:只有當以下四個條件同時滿足時,程序纔會死鎖
-
互斥:共享資源X與Y只能被一個線程佔用(不能破壞)
-
佔有且等待:線程T1已經擁有共享資源X,在等待共享資源Y的時候不釋放X
-
不可搶佔:其他線程不能搶佔T1線程所持有的資源X
-
循環等待:線程A與線程B相互等待對方已佔有的資源
synchronized獲取鎖:阻塞式獲取鎖,獲取不到線程就一直阻塞下去
破壞不可搶佔條件的方法:
1.響應中斷 void lockInterruptibly() throws InterruptedException;
2.支持超時 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
3.非阻塞式獲取鎖,線程若獲取不到鎖,線程直接退出 boolean tryLock();
5.3Lock接口常用的方法
- void lock():阻塞式獲取鎖,相當於synchronized關鍵字
- boolean tryLock():非阻塞式獲取鎖,獲取成功繼續執行任務返回true;否則返回false線程直接退出
- void lockInterruptibly() throws InterruptException:獲取鎖時響應中斷
- boolean tryLock(long time,TimeUnit unit) throws InterruptException:獲取鎖時支持超時,在超時內或爲中斷的情況下能獲取鎖
- Condition newCondition():獲取綁定到該lock對象的等待隊列,每當調用一次就產生一個新的等待隊列
Lock體系使用的格式
try{
lock.lock();//加鎖
}finally{
lock.unlock();//解鎖
}
Lock實現的賣票問題
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyThread implements Runnable{
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
for(int i = 0;i < 100;i++){
try{
//等同於synchronized(this)
lock.lock();
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"還剩下"+(ticket--)+"票");
}
}finally {
lock.unlock();
}
}
}
}
public class testLock {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
Lock實現的響應中斷
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class MyThread1 implements Runnable{
private Lock lock = new ReentrantLock();
@Override
public void run() {
try{
while (true){
lock.lockInterruptibly();
}
}catch (InterruptedException e){
System.out.println("線程響應中斷");
return;
}finally {
lock.unlock();
}
}
}
public class TaskInterrupt {
public static void main(String[] args) throws InterruptedException {
MyThread1 mt = new MyThread1();
Thread thread = new Thread(mt);
thread.start();
Thread.sleep(3000);
thread.interrupt();
}
}
輸出:3秒後輸出響應中斷
Lock實現的支持超時獲取鎖問題
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class TaskTime implements Runnable{
private Lock lock = new ReentrantLock();
@Override
public void run() {
test();
}
public void test(){
try{
if(lock.tryLock(1,TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"獲取鎖成功");
Thread.sleep(2000);
}else{
System.out.println(Thread.currentThread().getName()+"獲取鎖失敗");
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class LockTime {
public static void main(String[] args) {
TaskTime taskTime = new TaskTime();
Thread thread1 = new Thread(taskTime,"線程A");
Thread thread2 = new Thread(taskTime,"線程B");
thread1.start();
thread2.start();
}
}
輸出:一個線程獲取鎖成功,一個獲取鎖失敗;在1秒內,假如線程A獲取鎖成功,睡2秒後釋放鎖,線程B無法在規定的1秒內獲取鎖,所以線程B獲取鎖失敗;
若將睡眠時間改爲0.5秒,假如線程B獲取鎖成功,休眠0.5秒後釋放鎖,線程A還可以在規定的1秒內獲取鎖;
5.4AQS
5.5Lock與AQS的關係
Lock提供獲取鎖成功與否的狀態給AQS,AQS根據此狀態來確定是否將線程置入AQS中
5.6公平鎖和非公平鎖
(只有Lock可以實現公平鎖,Lock默認非公平鎖)
公平鎖:等待時間最長的線程優先獲得鎖
非公平鎖:獲得鎖的線程不可預估
5.7讀寫鎖
共享鎖:同一時刻,可以有多個線程擁有鎖(WIFI密碼、百度網盤密碼)
讀寫鎖是共享鎖的一種實現,
讀鎖共享:多個線程可以同時拿到讀鎖進行訪問,當寫線程拿到寫鎖開始工作時,所有讀線程全部阻塞
寫鎖獨佔:任意一個時刻,只有一個線程可以拿到寫鎖
讀讀-不互斥
讀寫、寫寫-互斥
Lock中的ReentrantReadWriteLock實現讀寫鎖
5.8Condition接口
Lock體系的線程通信方式,類比Object wait()與notify()
常用方法:
await():釋放lock鎖,將線程置入等待隊列阻塞
signal():隨機喚醒一個處於等待狀態的線程
signalAll():喚醒所有等待線程
6.生產者與消費者模型
6.1wait()方法
死等直到被喚醒或中斷
6.1.1定義
使當前線程立刻停止運行,處於等待狀態(WAIT),並將當前線程置入鎖對象的等待隊列中,直到被通知(notify())或被中斷爲止。
6.1.2使用條件
只能在同步方法或同步代碼塊中使用,必須是內建鎖。
wait()調用後立刻釋放對象鎖。
wait()被喚醒的notify()必須是同一個鎖對象
- public final void wait()throws InterruptedException{}----死等,直到被喚醒或被中斷,調用了wait(),而後再調用interrupt()會拋出中斷異常
- public final native void wait(long timeout)throws InterruptedException;----超時等待。若在規定時間內未被喚醒,則線程退出。單位:毫秒
- public final voidwait(long timeout, int nanos) throws InterruptedException
在2的基礎上增加了納秒控制
6.2notify()方法
6.2.1定義
喚醒處於等待狀態的線程
6.2.2使用條件
必須在同步方法或同步代碼塊中使用,必須是內建鎖,用來喚醒等待該對象的其他線程。如果有多個線程等待,隨機挑選一個喚醒;
6.2.3notify()與wait()最大的不同
notify()方法調用後,當前線程不會立馬釋放對象鎖,要等待當前線程的notify()執行完畢後再釋放鎖(wait()調用後立馬釋放對象鎖)
6.3notifyAll()
喚醒所有處於等待狀態的線程
6.4線程由運行態->阻塞
(1)調用sleep(),立刻交出CPU,不釋放鎖
(2)線程調用阻塞式IO(BIO)方法
(3)線程獲取鎖失敗進入阻塞狀態
(4)線程調用wait()
(5)線程調用suspend(),將線程掛起
每個鎖對象都有兩個隊列。一個稱爲同步隊列,存儲獲取鎖失敗的線程。另一個稱爲等待隊列,存儲調用wait()等待的線程。
線程喚醒實際上是將處於等待隊列的線程移動到同步隊列中競爭鎖
7.線程池
7.1線程池優點
-
降低資源消耗
通過重複利用已創建的線程降低線程創建與銷燬帶來的損耗
-
提高響應速度
當任務到達時,無需等待線程創建就可以立即執行
-
提高線程的可管理性
使用線程池可以統一進行線程分配、調度與監控
7.2線程池的繼承關係
(Executor的框架圖)
-
ExecutorService(普通調度池接口)
- Future submit(Callable task|Runnable task)
- void execute(Runnable command);
版本:JDK1.5
-
ScheduledExcutorService(定時調度池核心接口)
- //延遲delay個時間單位後開始執行–可以傳入Runnable/Callable接口的對象
schedule(Runnable command,long delay, TimeUnit unit);
schedule(Callable callable,long delay, TimeUnit unit); - //延遲initialDelay個時間單位後開始執行,並且每隔period個時間單位就執行一次;–只能傳入Runnable接口的對象
scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
- //延遲delay個時間單位後開始執行–可以傳入Runnable/Callable接口的對象
7.3線程池執行流程
(ThreadPoopExecutor類創建的線程池)
I.判斷核心線程池是否已滿,如果未滿,創建一個新的線程執行任務;反之,判斷是否有空閒線程,有的話將任務分配給空閒線程,反之(即核心線程池已滿並且都在執行任務),執行步驟2(創建線程需要獲得全局鎖)
II.判斷工作隊列(阻塞隊列BlockingQueue)是否已滿。如果工作隊列未滿,將任務存儲在工作隊列中等待空閒線程調度。如果已滿,執行步驟3
III.判斷當前線程池線程數量是否已達到最大值,如果沒有,則創建新的線程來執行任務。否則,執行步驟4(創建線程需要獲得全局鎖)
IIII.調用飽和策略來處理此任務(默認爲拋出異常給用戶AbortPolicy)
7.4線程池的使用
7.4.1手工創建線程池
通過new一個ThreadPoolExecutor就可以實現自定義線程池
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
1)corePoolSize:核心線程池大小
當提交一個任務到線程池時,只要核心線程池未達到corePoolSize,創建新線程來執行任務;
調用preStartAllCoreThreads(),線程池會提前創建並啓動所有核心線程。
2)BlockingQueue<Runnable> workQueue**:任務隊列
保存等待執行任務的阻塞隊列。
- ArrayBlockingQueue:基於數組有界阻塞隊列,按照FIFO原則對元素進行排序
- LinkedBlockingQueue:基於鏈表的無界阻塞隊列,按照FIFO原則排列元素。吞吐量要高於ArrayBlockingQueue.內置線程池newFixedThreadPool(固定大小線程池)就採用此隊列。
- SynchronousQueue:一個不存儲元素的阻塞隊列(無界隊列),每個插入操作需要等待另一個線程的移除操作,否則插入操作一直處於阻塞狀態。吞吐量通常高於LinkedBlockingQueue.內置線程池newCachedThreadPool(緩存線程池)就採用此隊列
- PriorityBlockingQueue:具有優先級的阻塞隊列
3)maximumPoolSize:線程池最大數量
4)keepAliveTime:線程保持活動的時間(線程池的工作線程空閒後,保持存活的時間)
如果任務多並且每個任務執行的時間比較短,可以調大此參數來提高線程利用率。
5)RejectedExecutionHandler handler(飽和策略):
-AbortPolicy:無法處理新任務拋出異常(JDK默認採用此策略)
-CallerRunsPolicy:使用調用者線程(創建該線程池的線程)所在線程來處理任務
-DisCardOldestPolicy:丟棄隊列中最近的一個任務並執行當前任務
-DisCardPolicy:不處理任務,丟棄任務,也不報異常
注意:
(1)調用Future接口的get()方法會阻塞其他線程,直到取得當前線程執行完畢後的返回值;
(2)FutureTask可以保證多線程場景下,任務只會被執行一次,其他線程不再執行此任務;(多個線程規律交替執行)
(3)線程池中的線程被包裝爲Worker工作線程,具備可重複執行任務的能力;
7.4.2向線程池提交任務
execute():提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功
submit():提交需要返回值的任務,線程池會返回一個future類型的對象,由此對象的get()獲取返回值,
get():阻塞當前線程直到任務完成,處理線程異步時使用
線程池實現賣票問題:
import java.util.concurrent.*;
class CallableTest implements Callable<String>{
private int ticket = 20;
@Override
public String call() throws Exception {
for(int i = 0; i < 20;i++){
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+"還剩下"+(ticket--)+"票");
}
}
return "票賣完了。。";
}
}
public class threadPool {
public static void main(String[] args) {
ExecutorService executorService =
new ThreadPoolExecutor(2,3,60,
TimeUnit.SECONDS,new LinkedBlockingQueue<>());
CallableTest callableTest = new CallableTest();
for(int i = 0; i < 5;i++){
executorService.submit(callableTest);
}
executorService.shutdown();
}
}
7.5內置四大線程池
7.5.1固定大小線程池
FixedThreadPool
應用場景:用於爲了滿足資源管理的需求,而需要限制當前線程數量的應用,適用於負載較重的服務器
MyThread myThread = new MyThread();
ExecutorService executorService = Executors.newFixedThreadPool(4);
for(int i = 0;i < 5;i++){
Future future = executorService.submit(myThread);
}
Executors.newFixedThreadPool()返回值解析:
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
1.corePoolSize = maxPoolSize
2.採用無界阻塞隊列LinkedBlockingQueue,永遠不會使用飽和策略
7.5.2單線程池
SingleThreadExecutor
使用場景:需要保證順序的執行各個任務,並且在任意時間點,不會有多個線程是活動的
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
7.5.3緩衝線程池
CachedThreadPool-根據需求創建線程的線程池(大小無界的線程池)
使用場景:適用於執行很多的短期異步任務的小程序,或者負載較輕的服務器
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
corePoolSize = 0;
maximumPoolSizes = Integer.MAX_VALUE(無界)
線程池的工作隊列:沒有容量的SynchronousQueue
主線程提交任務的速度高於線程處理任務的速度的話,CachedThreadPool會一直創建新的線程,極端情況下,會因爲創建過多線程而耗盡CPU和內存資源
當任務提交速度>線程執行速度,會不斷創建線程
當線程的執行速度>任務提交速度,只會創建若干個有限線程
7.5.4定時調度池
newScheduledThreadPool()
主要用來在給定的延遲之後運行任務,或者定期執行任務,功能與Timer類似,但是Timer對應的是單個後臺線程,SchedualedThreadPoolExcecutor可以在構造函數中指定多個對應的後臺線程數
ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
ScheduledThreadPoolExecutor爲了實現週期性的執行任務,對ThreadPoolExecutor做了三方面的修改
i.獲取DelayQueue作爲任務隊列
ii.獲取任務的方式不同
iii.執行週期任務後,增加了額外的處理
7.6合理配置線程池
配置核心池以及最大線程池線程數量(N–電腦核數)
CPU密集型任務(大數運算):NCPU + 1
IO密集型任務(讀寫文件):2 * NCPU