注:本文為本人學習過程中的筆記
1.導入
1.進程和線程
我們希望我們的程序可以并發執行以提升效率,此時引入了多進程編程。可是創建進程等操作開銷太大,于是就將進程進一步拆分成線程,減少開銷。進程與進程之間所涉及到的資源是相互獨立的,不會相互干擾。至于線程之間具體是怎么調度的,我們很難知道,這主要是操作系統隨機調度。
進程是操作系統資源分配的基本單位
線程是操作系統調度執行的基本單位
進程是存在父子關系的,而線程不存在
2.多線程代碼的簡單寫法
1.創建線程
1.重寫Thread中的run方法
public class Test{public static void main(String[] args){Thread t1 = new MyThread;}
}
public class MyThread extends Thread{public void run() {....}
}
2.重寫Runnable接口中的run方法?
public class Test implements Runnable{public static void main(String[] args){Runnable myRunnable = new MyRunnable();Thread t1 = new Thread(myRunnable);}
}
public class MyRunnable{public void run(){...}
}
3.使用lamda表達式?
public class Test{public static void main(String[] args){Thread t1 = new Thread(() -> {public void run(){...} });}
}
2.一些方法
new Thread()
a.Thread()? ? ? ? 直接創建線程對象
b.Thread(Runnable target)? ? ? ? 使用Runnable對象創建線程對象
c.Thread(String name)? ? ? ? 創建線程對象并命名
d.Thread(Runnable target, String name)? ? ? ? 使用Runnable對象創建線程對象并命名
run()
run方法是線程的入口方法,進入線程自動就會調用,不需要我們調用
start()
這是啟動線程的方法
sleep()?
這個方法要通過Thread類來調用,效果是使當前線程休眠一定時間。在括號里可以設置休眠的時間,單位是毫秒。
Thread.sleep(1000);
使用這個方法時會拋出InterruptedException異常?
因為線程的調度是不可控的,所以這個方法只能保證實際休眠時間大于等于參數設置的時間。代碼調用sleep,相當于讓當前線程讓出cpu資源,后續時間到的時候就需要操作系統內核把這個線程重新調度到cpu上才能繼續執行。
sleep(0)是一種特殊寫法,意味著讓當前線程立即放棄cpu資源等待操作系統重新調度
getId()
Java中會給每個運行的線程分配id,獲取id
getName()
獲取名字
getState
獲取狀態
getPriority
獲取優先級
isDaemon()
判斷是否是后臺線程
Java中存在后臺線程和前臺線程,后臺線程隨著進程的開啟而開啟,關閉而關閉,不影響進程的狀態,我們創建的線程和main線程是前臺線程,可以通過setDaemon方法修改
setDaemon()
在括號里填寫true或者false來設置線程是否是后臺線程
isAive()
判斷線程是否存活
Java代碼中創建的Thread對象和系統中的線程是一一對應關系。但是,Thread對象的生命周期和系統中的生命周期是不同的,可能存在Thread對象還存貨,但是系統中的線程已經銷毀的情況
interrupt()
關閉線程
調用這個方法時,會修改isInterruptted方法內部的標志位將其設為true。如果我們在使用了sleep方法并且喚醒了該方法,那sleep方法就會把isInterruptted的標志位設置為false,這時sleep會拋出Interruptted異常,我們可以修改try-catch語句中的代碼,達到我們想要的效果而不是直接關閉線程
isInterruptted()
判斷線程是否關閉
Thread.currentThread()
這是一個靜態方法,在哪個線程中調用就能獲得哪個線程的應用
join()
join能夠要求多個線程結束的先后順序。比如在main線程中調用t.join就會使main線程等待t線程先結束。只要t線程不結束,主線程的join就會一直的等待下去。我們可以在括號里設置最大等待時間,當到達時間join就不會再等待,繼續執行下面的代碼
wait() / notify()
這兩個方法是用來協調線程之間的執行邏輯的順序。雖然我們不能干預調度器的調度順序,但是我們可以讓后執行的線程進行等待,等到先執行的線程執行完了,通知當前線程,繼續執行。
在Java標準庫中,每個產生阻塞的方法都會拋出InterrupttedException異常,會被interrupt方法喚醒,wait也是一樣
wait和join的區別
join也是等,當join是等另一個線程徹底執行完才繼續執行
wait是等另一個線程執行到notify才繼續走,不需要等另一個線程執行完
應用場景
當多個線程競爭一把鎖的時候,獲取鎖的線程如果釋放了,其他哪個線程能拿到這把鎖是不確定,我們不能控制操作系統怎么調度,當我們可以使用wait和notify語句來控制這個順序
使用方法
使用wait時,線程會釋放鎖,所以我們要使用wait時線程必須獲取鎖,否則會報錯
wait的等待最關鍵的一點就是先釋放鎖,給其他線程獲取鎖的機會,并且阻塞等待。如果其他線程做完了必要的工作,調用notify喚醒這個wait線程,wait就會解除阻塞,重新獲取到鎖,然后繼續執行
synchronized (locker) {locker.wait();
}synchronized(locker) {locker.notify();
}
這其中的鎖對象必須時同一個鎖對象才能產生效果,如果多個線程在同一個鎖對象上wait,進行notify的時候是隨機喚醒其中一個線程,一次notify喚醒一個wait。wait也可以在括號填寫超時時間,不死等。
wait和sleep的區別
wait有等待時間,可以用notify喚醒。sleep也有等待時間,可以使用interrupt提前喚醒
wait必須要搭配鎖使用,先加鎖,才能用wait,sleep不需要
如果都是在synchronized內部使用,wait會釋放鎖,而sleep不會釋放鎖
notifyAll()
使用這個方法可以一次喚醒所有相關的wait
3.小工具
1.jconsole
這個小工具在jdk的bin目錄下,使用這個工具可以連接java程序,從而觀察線程的信息
3.線程狀態
NEW
安排了工作,還未開始行動
也就是new了Thread對象,還沒start
TERMINATED
工作完成了
內核中的線程已經結束了但是Thread對象還在
RUNNABLE
可工作的,又可以分成正在工作中和即將開始工作
a.線程正在cpu上執行
b.線程隨時可以去cpu上執行
TIMED_WAITTING
表示排隊等著其他事情,有超時時間
當我們調用join方法,線程就會進入這個狀態。
WAITING
表示排隊等待其他事情,沒有超時時間,死等
BLOCKED
表示排隊等待其他事情
這個比較特殊,是由于鎖導致的阻塞
總圖
4.線程安全
一段代碼,如果再多線程并發執行的情況下,出現bug,就稱為線程不安全
1.線程安全問題產生的原因
Java中的一行代碼對應著cpu上的多條指令,并不是原子的,所以當cpu隨機調度的時候就可能出現一些問題,也就會產生線程安全問題,以下是產生線程安全問題的原因
a.(根本原因)操作系統對于線程的調度是隨機的,搶占式執行
b.多個線程同時修改一個變量
c.修改操作不是原子的
d.內存可見性,jvm會優化代碼
e.指令重排序,jvm會優化代碼
2.如何解決線程安全問題
a.針對問題a我們是無法解決的,因為搶占式執行是操作系統的底層設定
b.針對問題b,這個問題和代碼結構相關,我們可以調整代碼結構,規避一些線程不安全的代碼。但是這樣的方案是不夠通用的,有些情況下,需求上就是需要多線程同時修改一個變量
c.針對問題c,我們可以使用加鎖操作,這也是Java中解決線程安全問題最主要的方案,通過加鎖操作我們可以將不是原子的操作打包成原子的操作。
d.使用volatile關鍵字
jvm對于我們的代碼會自動的進行一些優化,比如說如果我們寫出這樣的一個語句
public class Test{int count = 0;public static void main(String[] args) throws InterrupttedException{Thread t1 = new Thread(() -> {while(count == 0) {}});t1.start();Thread.sleep(30000);count = 1;}
}
當我們啟動這個代碼時,線程t1中的循環會被一直執行下去,這是為什么呢?明明我們在主線程中都修改了count的值
因為這個時候jvm優化了我們的代碼,在t1線程中,while語句不斷地從內存中讀取count的值,這個操作的開銷是比較大的,此時jvm會將count的值保留在cpu寄存器中,直接讀取寄存器的count值,這就導致了我們修改了count的值而不會被讀取到
此時就可以使用volatile關鍵字修飾count這個變量讓jvm不優化我們的代碼
注意:在Java的官方文檔中這樣寫道,每個線程,有一個自己的“工作內存”,同時這些線程共享一個“主內存”。當一個線程循環進行上述讀取變量操作的時候,就會把主內存中的數據,拷貝到......
這里提到的“工作內存”就是我們所說的cpu寄存器,cpu上還有緩存,因為Java是跨平臺的語言,設計者不希望程序員有學習硬件知識的成本,所以將其抽象為“工作內存的概念”。
e.使用volatile關鍵字
volatile關鍵字不僅可以解決內存可見性問題,還可以不讓jvm進行指令重排序。
3.鎖
加鎖/解鎖本身是系統提供的api,很多編程語言都對這樣的api進行了封裝,大多數的封裝風格都是采取lock和unlock兩個函數,Java中采用的是synchronized關鍵字
synchronized
synchronized(鎖對象) {//進入代碼塊相當于加鎖...
}//離開代碼塊相當于解鎖
括號里需要我們填寫加鎖的對象,這個鎖的類型不重要,重要的是是否有幾個線程嘗試針對同一個鎖對象進行加鎖。只有兩個線程針對同一個鎖對象加鎖,才能產生互斥效果。即一個線程獲取鎖之后,另一個線程為了也能獲取鎖只能阻塞等待第一個線程中的鎖釋放出來?
synchronized的變種寫法
synchronized可以對方法進行加鎖
當要加鎖的方法是被static修飾時,synchronized修飾static方法就相當于針對類對象進行加鎖
死鎖
產生死鎖的原因
1.互斥
當兩個線程爭奪同一個鎖時會產生互斥,這是鎖的基本特性
2.不可剝奪
當一個線程獲得鎖之后,這個鎖是不能被搶走的,只能等待它釋放出來,這也是鎖的基本特性
3.請求和保持
當一個線程已經獲取一把鎖之后,繼續請求其他的鎖
4.循環等待
當一個線程已經獲取一把鎖之后,繼續請求其他鎖,且有另一個線程也在獲得鎖的情況下請求相同的鎖,形成循環
解決死鎖的辦法
針對問題1和問題2是無法解決的,這是鎖的基本特性
針對問題3
我們可以避免鎖嵌套
針對問題4
可以約定加鎖的順序,使爭奪鎖的過程形不成循環
2.Java中線程安全的東西
String
系統api沒有提供修改String的方法,導致String天然就是線程安全的
5.多線程代碼案例
1.單例模式
單例模式是指在某個類,某個程序中只允許有唯一一個實例,不允許有多個實例,不允許new多次。
1.應用場景
比如我們有一個100G的數據庫要加載到內存中方便讀取,由于這個數據庫數據非常多,創建等操作需要的開銷都非常大,此時我們希望只創建一次即可,否則將會產生非常多的額外的開銷也可能導致服務器的內存不夠用。
2.兩種寫法
懶漢模式和餓漢模式是存在缺陷的,可以通過反射來創建實例,但是反射本身屬于非常規的手段,一般編寫代碼的時候不使用
1.餓漢模式
餓是指盡早創建實例
1.寫法
class SingleTon{private static SingleTon instance;//靜態成員的初始化是在類加載的時候就觸發的,往往程序一啟動類就會加載public static SingleTon getInstance(){return instance;}//后續統一使用getInstance這個方法來獲取實例private SingleTon{}//由于構造方法是私有的,所以在類外new instance都會編譯失敗
}
2.線程安全問題
由于餓漢模式中的instance在程序啟動的時候就創建好了,所以我們后續的操作都只涉及到讀操作,不會產生線程安全問題。?
2.懶漢模式
懶是指盡可能晚地創建實例,延遲創建
1.寫法
class SingleTonLazy{private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//懶漢模式創建實例的時機是第一次使用的時候而不是程序啟動的時候instance = new SingleTonLazy();}return instance;}private SingleTonLazy{}
}
2.線程安全問題?
懶漢模式這里getInstance里面給instance賦值這個操作,等號是原子的,但是當它和if語句組合在一起的時候,就變得不是原子的了,此時我們就可以給if語句和內部的賦值語句加鎖,讓它變成原子的。
class SingleTonLazy{Object locker = new Object();private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){synchronized(locker){if(instance == null){instance = new SingleTonLazy();}return instance;}}private SingleTonLazy{}
}
這里加鎖之后,我們使用getInstance就是線程安全的了。可是,這又引出了一個新的問題,當我們第一次調用過getInstance語句之后,instance就創建好了,我們之后再調用getInstance都只需要使用return就好,可是我們在這里加了鎖,如果有多個線程同時調用這個方法,就會產生阻塞,影響程序的效率。?這個時候,我們就可以再加一個if語句
class SingleTonLazy{Object locker = new Object();private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//這個if是來判斷是否需要加鎖synchronized(locker){if(instance == null){//這個if是來判斷是否需要創建instanceinstance = new SingleTonLazy();}return instance;}}}private SingleTonLazy{}
}
進行了這么多處理之后,代碼依然存在一些問題,instance = new SingleTonLazy()這個操作可能涉及指令重排序問題,jvm可能會更改指令的順序,new這個操作涉及到申請內存空間,初始化對象,將內存地址賦值給引用變量。如果這個new操作先把內存地址賦值給引用變量,再進行變量初始化的話,這時另一個線程使用getInstance方法時,instance這個實例就已經存在了,可是還沒有初始化,這又構成了線程安全問題,此時我們就可以使用volatile關鍵字來修飾instance,阻止jvm進行指令重排序
class SingleTonLazy{Object locker = new Object();private static volatile SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//這個if是來判斷是否需要加鎖synchronized(locker){if(instance == null){//這個if是來判斷是否需要創建instanceinstance = new SingleTonLazy();}return instance;}}}private SingleTonLazy{}
}
這樣我們的代碼就是線程安全的了。?
2.阻塞隊列
阻塞隊列其實就是一種更復雜的隊列,它是線程安全的
1.特性
a.隊列為空時,嘗試出隊列,出隊列操作就會阻塞,阻塞到其他線程添加元素為止
b.隊列為滿時,嘗試入隊列,入隊列操作也會阻塞,阻塞到其他線程取走元素為止
2.應用場景
1.生產者消費者模型
簡單來說就是生產者生產產品,消費者消費產品,他們在一個消費場所進行這些操作,而這個消費場所就是阻塞隊列
1.該模型的優點
1.解耦合
這里的解耦合不一定是兩個線程之間,也可以是兩個服務器之間。
如果是A直接訪問B,此時A和B的耦合就會更高。編寫A的代碼的時候,多多少少會有一些和B相關的邏輯,編寫B的代碼的時候,也會有一些A的相關邏輯。?
此時添加一個阻塞隊列,讓A和隊列交互,讓B和隊列交互,這樣A和B之間就解耦合了。A和B是業務服務器,所以經常會涉及到改動,而阻塞隊列并不會經常修改,所以A,B分別和阻塞隊列耦合是沒什么問題的。阻塞隊列非常重要,有時甚至會把隊列單獨部署成一個服務,稱為“消息隊列”。
2.削峰填谷
在實際的應用場景中,服務器接收到的請求并不是穩定的,有時候會很多,有時又很少,當沒有阻塞隊列,兩個服務器直接進行交互時
當A遇到一波流量激增,此時它會把每個請求都轉發給B,B也會承擔一樣的壓力,此時就很容易把B給搞掛了。?
一般來說A這種上游的服務器,尤其是入口的服務器,干的活更簡單,單個請求消耗的資源較少,而像B這種下游的服務器,通常承擔更重的任務量(復雜的計算/存儲工作),單個請求消耗的資源更多。
如果有阻塞隊列的話,壓力就可以由阻塞隊列來承擔,B可以不關心數據量的多少,按照自己的節奏慢慢處理隊列里的數據即可。由于大量的請求一般都是突發的,時間也不長,所以B可以趁著峰值過去了繼續消費數據,利用波谷的時間,來消費之前積壓的數據。
2.該模型的缺點
1.引入隊列之后,整體的結構會更復雜,此時就需要更多的機器進行部署,生產環境的結構也會更加復雜,管理起來更麻煩
2.效率會有影響
3.使用
Java標準庫中提供了阻塞隊列,我們可以直接使用
1.put()/take()
阻塞隊列繼承了Queue接口,所以我們可以使用offer()和pull()來存取數據,但是想要達到阻塞效果的話必須使用take()和put()方法。?