1、線程基礎(chǔ)、線程之間的共享和協(xié)作
1.1基礎(chǔ)概念
1.1.1 什么是進程和線程
進程是程序運行資源分配的最小單位;
線程是 CPU 調(diào)度的最小單位,必須依賴于進程而存在
1.1.2 CPU 核心數(shù)和線程數(shù)的關(guān)系
核心數(shù)、線程數(shù):目前主流 CPU 都是多核的。增加核心數(shù)目就是為了增加線程數(shù),因為操作系統(tǒng)是通過線程來執(zhí)行任務的,一般情況下它們是 1:1 對應關(guān)系,也就是說四核 CPU一般擁有四個線程。但 Intel 引入超線程技術(shù)后,使核心數(shù)與線程數(shù)形成 1:2 的關(guān)系。
1.1.3 時間片輪轉(zhuǎn)機制
1.1.4 并行和并發(fā)
舉個例子,如果有條高速公路 A 上面并排有 8 條車道,那么最大的并行車輛就是 8 輛。此條高速公路 A 同時并排行走的車輛小于等于 8 輛的時候,車輛就可以并行運行。CPU 也是這個原理,一個 CPU 相當于一個高速公路 A,核心數(shù)或者線程數(shù)就相當于并排可以通行的車道;而多個 CPU 就相當于并排有多條高速公路,而每個高速公路并排有多個車道。
當談論并發(fā)的時候一定要加個單位時間,也就是說單位時間內(nèi)并發(fā)量是多少。離開了單位時間其實是沒有意義的。
綜合來說:
并行:指應用能夠同時執(zhí)行不同的任務。例:吃飯的時候可以邊吃飯邊打電話,這兩件事情可以同時執(zhí)行。
并發(fā):指應用能夠交替執(zhí)行不同的任務,比如單 CPU 核心下執(zhí)行多線程。并非是同時執(zhí)行多個任務,如果你開兩個線程執(zhí)行,就是在你幾乎不可能察覺到的速度不斷去切換這兩個任務,已達到“同時執(zhí)行效果”,其實并不是的,只是計算機的速度太快,我們無法察覺到而已。
兩者區(qū)別:一個是同時執(zhí)行,一個是交替執(zhí)行。
1.1.5 高并發(fā)編程的意義、好處和注意事項
(1)充分利用 CPU 的資源
(2)加快響應用戶的時間
(3)可以使你的代碼模塊化,異步化,簡單化
1.1.6 多線程程序需要注意事項
(1)線程之間的安全性
同一個進程中的多線程,資源是共享的,也就是可以訪問同一個內(nèi)存地址的變量。如果多個線程同時執(zhí)行寫操作,則需要考慮線程同步,否則會影響線程安全。
(2)線程之間的死鎖:
形成死鎖的條件,缺一不可:
1、多操作者(M>=2個)情況下,爭奪多個資源(N>=2個,且N<=M)才會發(fā)生這種情況。很明顯,單線程自然不會有死鎖;
2、爭奪資源的順序不對,如果爭奪資源的順序是一樣的,也不會產(chǎn)生死鎖;
3、爭奪者拿到資源不放手。
學術(shù)化的定義
死鎖的發(fā)生必須具備以下四個必要條件。
1)互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內(nèi)某資源只由一個進程占用。如果此時還有其它進程請求資源,則請求者只能等待,直至占有資源的進程用畢釋放。
2)請求和保持條件:指進程已經(jīng)保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。
3)不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
4)環(huán)路等待條件:指在發(fā)生死鎖時,必然存在一個進程——資源的環(huán)形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1占用的資源;P1正在等待P2占用的資源,……,Pn正在等待已被P0占用的資源。
危害
1、線程不工作了,但是整個程序還是活著的;
2、沒有任何的異常信息可以供我們檢查;
3、一旦程序發(fā)生了發(fā)生了死鎖,是沒有任何的辦法恢復的,只能重啟程序,對正式已發(fā)布程序來說,這是個很嚴重的問題。
解決方案
關(guān)鍵是保證拿鎖的順序一致
兩種解決方式
1、內(nèi)部通過順序比較,確定拿鎖的順序;
2、采用嘗試拿鎖的機制。
為解決線程之間的安全性,引入了java中的鎖機制,而不小心產(chǎn)生的java線程死鎖的多線程問題,因為不同線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。
假設有兩個線程分別代表兩個饑餓的人,他們必須共享刀叉并輪流吃飯,都需要獲得刀和叉才能進行下一個操作。而線程A獲取到了刀,而線程B獲取到了叉,那線程A、B都會進入到阻塞狀態(tài),等待獲取對方擁有的鎖。
①引入lock,解決死鎖問題
public class TryLock {
private static Lock No13 = new ReentrantLock();//第一個鎖
private static Lock No14 = new ReentrantLock();//第二個鎖
//先嘗試拿No13 鎖,再嘗試拿No14鎖,No14鎖沒拿到,連同No13 鎖一起釋放掉
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while(true){
if(No13.tryLock()){
System.out.println(threadName +" get 13");
try{
if(No14.tryLock()){
try{
System.out.println(threadName +" get 14");
System.out.println("fisrtToSecond do work------------");
break;
}finally{
No14.unlock();
}
}
}finally {
No13.unlock();
}
}
//Thread.sleep(r.nextInt(3));
}
}
//先嘗試拿No14鎖,再嘗試拿No13鎖,No13鎖沒拿到,連同No14鎖一起釋放掉
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while(true){
if(No14.tryLock()){
System.out.println(threadName +" get 14");
try{
if(No13.tryLock()){
try{
System.out.println(threadName +" get 13");
System.out.println("SecondToFisrt do work------------");
break;
}finally{
No13.unlock();
}
}
}finally {
No14.unlock();
}
}
//Thread.sleep(r.nextInt(3));
}
}
private static class TestThread extends Thread{
private String name;
public TestThread(String name) {
this.name = name;
}
public void run(){
Thread.currentThread().setName(name);
try {
SecondToFisrt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("TestDeadLock");
TestThread testThread = new TestThread("SubTestThread");
testThread.start();
try {
fisrtToSecond();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
②保證拿鎖的順序一致
public class NormalDeadLock {
private static Object apple = new Object();//第一個鎖
private static Object orange = new Object();//第二個鎖
//第一個拿鎖的方法
private static void thread1Do() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (apple){
System.out.println(threadName+" get apple");
Thread.sleep(100);
synchronized (orange){
System.out.println(threadName+" get orange");
}
}
}
//第二個拿鎖的方法
private static void thread2Do() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (apple){
System.out.println(threadName+" get apple");
Thread.sleep(100);
synchronized (orange){
System.out.println(threadName+" get orange");
}
}
}
//子線程
private static class Thread2 extends Thread{
private String name;
public Thread2(String name) {
this.name = name;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
thread2Do();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//主線程
Thread.currentThread().setName("Thread1");
Thread2 thread2 = new Thread2("Thread2");
thread2.start();
thread1Do();
}
}
活鎖
兩個線程在嘗試拿鎖的機制中,發(fā)生多個線程之間互相謙讓,不斷發(fā)生同一個線程總是拿到同一把鎖,在嘗試拿另一把鎖時因為拿不到,而將本來已經(jīng)持有的鎖釋放的過程。
解決辦法:每個線程休眠隨機數(shù),錯開拿鎖的時間
線程饑餓
低優(yōu)先級的線程,總是拿不到執(zhí)行時間
(3)線程太多了會將服務器資源耗盡形成死機當機
線程數(shù)太多有可能造成系統(tǒng)創(chuàng)建大量線程而導致消耗完系統(tǒng)內(nèi)存以及cpu的過度切換,造成系統(tǒng)死機。解決的辦法是使用線程池。
1.2 java中的線程
1.2.1 線程的啟動與中止
啟動
啟動線程的方式有兩種:
thread源碼中有注釋寫明(There are two ways to create a new thread of execution.)
1、X extends Thread,然后 X.start
2、X implements Runnable;然后交給 Thread 運行
Thread 和 Runnable 的區(qū)別
Thread 才是 Java 里對線程的唯一抽象,Runnable 只是對任務(業(yè)務邏輯)
的抽象。Thread 可以接受任意一個 Runnable 的實例并執(zhí)行。
中止
(1)線程自然終止:run方法執(zhí)行完成,獲取是拋出了一個未處理的異常而導致線程提前結(jié)束
(2)top:暫停(suspend())、恢復(resume())和停止(stop()),這些api是過時的,不建議使用。原因是,以suspend()方法為例,在調(diào)用后,線程不會釋放已經(jīng)占有的資源,而是占著資源進入到睡眠狀態(tài),這樣就容易引發(fā)死鎖問題。同樣stop()方法在終結(jié)一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會,因此會導致程序可能工作在不確定狀態(tài)下。正是suspend()、resume()、stop()方法帶來的副作用,這些方法才會被注明不建議使用的過時方法。
(3)中斷
安全的中止,則是其他線程通過調(diào)用某個線程A的interrupt()方法對其進行中斷操作,中斷操作好比通知此線程A停止工作,但線程A可以完全不理會這種中斷請求,因而調(diào)用interrupt()方法的線程不一定會立即停止工作。一般需要線程通過檢查自身的中斷位,isInterrupted()方法判斷線程是否被通知中斷。
還有一個方法,Thread.interrupted(),同樣可以用來判斷線程是否被通知中斷,此方法和isInterrupted()方法的不同之處在于,調(diào)用此方法后,會同時將中斷標識改成false。
不建議自定義一個取消標志位來中止線程的運行。因為run方法里有阻塞調(diào)用時會無法很快檢測到取消標志,線程必須從阻塞調(diào)用返回后,才會檢查這個取消標志。這種情況下,使用中斷會更好,因為:一、一般的阻塞方法,如sleep等本身就支持中斷檢查;二、檢查中斷位的狀態(tài)和檢查取消標志位沒什么區(qū)別,用中斷位的狀態(tài)還可以避免聲明取消標志位,減少資源的消耗。
1.3 java中的線程的認識
1.3.1 run()和start()方法
Thread類是java里對線程概念的抽象??梢赃@樣理解,我們通過new Thread()其實只是new出了一個Thread的實例,還沒有操作系統(tǒng)中真正的線程掛起鉤來,只有執(zhí)行了start()方法,才實現(xiàn)了真正意義上的啟動線程。
start(),方法只允許調(diào)用一次,多次調(diào)用會拋出異常。調(diào)用start()方法會,并不會立即執(zhí)行該線程的run()方法,而是進入到可執(zhí)行狀態(tài),等待cpu分配。
run(),可以被單獨調(diào)用,但單獨調(diào)用的話,還是在調(diào)用此方法的線程中執(zhí)行。
1.3.2 其他線程相關(guān)方法
(1)yield()方法:讓出cpu,將線程狀態(tài)從運行狀態(tài)轉(zhuǎn)到可運行狀態(tài),不會釋放鎖
執(zhí)行yield()方法的線程進入到可運行狀態(tài)后,等待cpu的再次輪轉(zhuǎn)。與其他線程的被執(zhí)行的幾率是一樣的。
(2)join方法:
獲得執(zhí)行權(quán),將指定的線程加入當前線程,可以將兩個交替執(zhí)行的線程合并為順序執(zhí)行。比如在線程B中調(diào)用了線程A的jon()方法,知道線程A執(zhí)行完畢后,線程B才開始繼續(xù)執(zhí)行。
1.3.3 線程的優(yōu)先級
通過priority變量來控制優(yōu)先級,一般是在1~10之間,通過setPriority(int)方法來修改優(yōu)先級,默認優(yōu)先級是5。
1.3.4 守護線程
用戶線程和守護線程(守護線程在用戶線程之后結(jié)束)
守護線程的finally方法不一定會執(zhí)行,完全看操作系統(tǒng)的調(diào)度;用戶線程的finally一定會執(zhí)行。
線程的共享和協(xié)作(線程不安全)
synchronized:機制鎖
鎖的是某個具體的對象
代碼塊、方法上加鎖
類鎖:本質(zhì)上也是對象鎖,只是對象比較特殊,鎖的是每一個類在虛擬機中生成的class文件。
如果兩個線程的鎖對象不一樣,那這兩個線程是并行的。
Volatile:最輕量的同步機制
在主線程中修改了某個成員變量的數(shù)據(jù),如果需要子線程能夠感知到,在成員變量添加volatile關(guān)鍵字。
不能替代synchronized,只保證讀的準確性,不能保證寫的準確性,適用于一寫多讀的場景。
ThreadLocal:為每一個線程提供了變量的副本,只訪問自己的數(shù)據(jù),實現(xiàn)了線程的隔離
ThreadLocal獲取到當前線程所維護的ThreadLocalMap,Thread類中有一個ThreadLocalMap的成員變量,ThreadLocalMap是每一個線程所獨有的。ThreadLocalMap中有個entry數(shù)組 ,維護著當前線程所創(chuàng)建的多個ThreadLocal對象。
public class ThreadLocal<T> {
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 getMap(Thread t) {
return t.threadLocals;
}
}
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
static class ThreadLocalMap {
private Entry[] table;
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);//key
value = v;//value,每個線程獨有的副本
}
}
使用不當會引發(fā)內(nèi)存泄漏
強引用:代碼中普遍存在,直接new出來的對象,垃圾收集器永遠不會回收掉被引用的對象實例。
軟引用:被SoftRefence引用,要發(fā)生內(nèi)存溢出了,如果回收了,發(fā)現(xiàn)還是不夠,才進行回收。
弱引用:被WeakReference引用,只要發(fā)生了垃圾回收,弱引用所指在堆上的實例就一定會被回收。
虛引用:最弱的一種引用關(guān)系。唯一目的就是能在這個對象實例被收集器回收時收到一個系統(tǒng)通知。
key 使用強引用:引用 ThreadLocal 的對象被回收了,但是 ThreadLocalMap 還持有 ThreadLocal 的強引用,如果沒有手動刪除,ThreadLocal 的對象實例不會被回收,導致 Entry 內(nèi)存泄漏。
key 使用弱引用:引用的 ThreadLocal 的對象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使沒有手動刪除,ThreadLocal 的對象實例也會被回收。value 在下一次 ThreadLocalMap 調(diào)用 set,get,remove 都有機會被回收。
由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果都沒有手動刪除對應 key,都會導致內(nèi)存泄漏,但是使用弱引用可以多一層保障。
因此,ThreadLocal 內(nèi)存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果沒有手動刪除對應 key 就會導致內(nèi)存泄漏,而不是因為弱引用。
總結(jié):
JVM 利用設置 ThreadLocalMap 的 Key 為弱引用,來避免內(nèi)存泄露。
JVM 利用調(diào)用 remove、get、set 方法的時候,回收弱引用
當 ThreadLocal 存儲很多 Key 為 null 的 Entry 的時候,而不再去調(diào)用 remove、 get、set 方法,那么將導致內(nèi)存泄漏。
使用線程池+ ThreadLocal 時要小心,因為這種情況下,線程是一直在不斷的重復運行的,從而也就造成了 value 可能造成累積的情況。

錯誤使用 ThreadLocal 導致線程不安全
ThreadLocalMap 中保存的其實是對象的一個引用,而指向的是同一個對象。這樣的話,當有其他線程對這個引用指向的對象實例做修改時,其實也同時影響了所有的線程持有的對象引用所指向的同一個對象實例。
等待和通知
wait()和notify()/notifyAll()方法是object中的方法
必須在synchronized關(guān)鍵字中使用,一旦線程調(diào)用wait()方法,進入休眠狀態(tài)前,會先釋放鎖。而調(diào)用notify()/notifyAll()方法并不會釋放鎖,需要完成synchronized中的全部代碼,才會釋放鎖
notify 和 notifyAll的區(qū)別
盡可能用 notifyAll(),謹慎使用 notify(),因為 notify()只會喚醒一個線程,我們無法確保被喚醒的這個線程一定就是我們需要喚醒的線程。
調(diào)用yield()、sleep()、wait()、notify()方法對鎖有和影響?
yield()方法,釋放cpu的執(zhí)行權(quán),并不會釋放鎖;
sleep()方法,不會釋放當前線程持有的鎖;
wait()方法,釋放當前線程所持有的鎖,當被喚醒的時候,去競爭鎖,拿到鎖后,才去執(zhí)行wait方法后面的方法;
notify()/notifyAll()方法,也不會釋放鎖,需要同步代碼塊中代碼執(zhí)行完,所以一般notify()/notifyAll()方法都會放在同步代碼塊的最后一行。
推薦書籍:
1、Java核心技術(shù) 卷1 基礎(chǔ)知識(無基礎(chǔ)看)
2、Java并發(fā)編程實戰(zhàn)(比較晦澀,有基礎(chǔ)看)