@
目錄- 一、多執行緒概述
- 行程與執行緒
- 并行與并發
- 執行緒安全問題
- 共享記憶體不可見性問題
- synchronized 的記憶體語意:
- Volatile的理解:
- 二、實作多執行緒
- 方式1:繼承Thread類
- 方式2:實作Runnable介面
- 方式3:實作Callable介面,執行緒池
- 三、執行緒安全問題
- synchronized
- Lock類
- 執行緒安全的集合
- 四、練習案例
- 兩種匿名內部類方式創建執行緒
- 簡單面試題 01
- 回顧集合和IO的面試題
一、多執行緒概述
參考:https://www.cnblogs.com/zsql/p/11144688.html
行程與執行緒
行程與執行緒的區別
-
執行緒是程式執行的最小單位,而行程是作業系統分配資源的最小單位
-
一個行程由一個或多個執行緒組成,執行緒是一個行程中代碼的不同執行路線
-
行程之間相互獨立,但同一行程下的各個執行緒之間共享程式的記憶體空間 (包括代碼段,資料集,堆等) 及一些行程級的資源(如打開檔案和信號等),某行程內的執行緒在其他行程不可見
-
調度和切換:執行緒背景關系切換比行程背景關系切換要快得多

執行緒(堆疊+PC+TLS)
-
堆疊:用于存盤該執行緒的區域變數,這些區域變數是該執行緒私有的,除此之外還用來存放執行緒的呼叫堆疊禎,
我們通常都是說呼叫堆疊,其實這里的堆是沒有含義的,呼叫堆疊就是呼叫堆疊的意思,
那么我們的堆疊里面有什么呢?
我們從主執行緒的入口main函式,會不斷的進行函式呼叫,
每次呼叫的時候,會把所有的引數和回傳地址壓入到堆疊中, -
PC:是一塊記憶體區域,用來記錄執行緒當前要執行的指令地址 ,
Program Counter 程式計數器,作業系統真正運行的是一個個的執行緒,
而我們的行程只是它的一個容器,PC就是指向當前的指令,而這個指令是放在記憶體中,
每個執行緒都有一串自己的指標,去指向自己當前所在記憶體的指標,
計算機絕大部分是存盤程式性的,說的就是我們的資料和程式是存盤在同一片記憶體里的
這個記憶體中既有我們的資料變數又有我們的程式,所以我們的PC指標就是指向我們的記憶體的, -
緩沖區溢位
例如我們經常聽到一個漏洞:緩沖區溢位
這是什么意思呢?
例如:我們有個地方要輸入用戶名,本來是用來存資料的地方,
然后黑客把資料輸入的特別長,這個長度超出了我們給資料存盤的記憶體區,這時候跑到了
我們給程式分配的一部分記憶體中,黑客就可以通過這種辦法將他所要運行的代碼
寫入到用戶名框中,來植入進來,我們的解決方法就是,用用戶名的長度來限制不要超過
用戶名的緩沖區的大小來解決, -
TLS:
全稱:thread local storage
之前我們看到每個行程都有自己獨立的記憶體,這時候我們想,我們的執行緒有沒有一塊獨立的記憶體呢?答案是有的,就是TLS,
可以用來存盤我們執行緒所獨有的資料,
可以看到:執行緒才是我們作業系統所真正去運行的,而行程呢,則是像容器一樣他把需要的一些東西放在了一起,而把不需要的東西做了一層隔離,進行隔離開來,
并行與并發
并發:是指同一個時間段內多個任務同時都在執行,并且都沒有執行結束,并發任務強調在一個時間段內同時執行,而一個時間段由多個單位時間累積而成,所以說并發的多個任務在單位時間內不一定同時在執行 ,
并行:同一時刻上多個任務同時在執行 ,
在多執行緒編程實踐中,執行緒的個數往往多于CPU的個數,所以一般都稱多執行緒并發編程而不是多執行緒并行編程,
執行緒安全問題
多個執行緒同時操作共享變數1時,會出現執行緒1更新共享變數1的值,但是其他執行緒獲取到的是共享變數沒有被更新之前的值,就會導致資料不準確問題,

共享記憶體不可見性問題
- Java 記憶體模型規定,將所有的變數都存放在主記憶體中,當執行緒使用變數時,會把主記憶體里面的變數復制到自己的作業空間或者叫作作業記憶體,執行緒讀寫變數時操作的是自己作業記憶體中的變數 ,(如下圖所示)


上圖中所示是一個雙核 CPU 系統架構,每個核有自己的控制器和運算器,其中控制器包含一組暫存器和操作控制器,運算器執行算術邏輔運算,CPU的每個核都有自己的一級快取,在有些架構里面還有一個所有CPU都共享的二級快取, 那么Java記憶體模型里面的作業記憶體,就對應這里的 Ll或者 L2 快取或者 CPU 的暫存器
-
執行緒A首先獲取共享變數X的值,由于兩級Cache都沒有命中,所以加載主記憶體中X的值,假如為0,然后把X=0的值快取到兩級快取,執行緒A修改X的值為1,然后將其寫入兩級Cache,并且重繪到主記憶體,執行緒A操作完畢后,執行緒A所在的CPU的兩級Cache內和主記憶體里面的X的值都是1,
-
執行緒B獲取X的值,首先一級快取沒有命中,然后看二級快取,二級快取命中了,所以回傳X=1;到這里一切都是正常的,因為這時候主記憶體中也是X=l,然后執行緒B修改X的值為2,并將其存放到執行緒2所在的一級Cache和共享二級Cache中,最后更新主記憶體中X的值為2,到這里一切都是好的,
-
執行緒A這次又需要修改X的值,獲取時一級快取命中,并且X=l這里問題就出現了,明明執行緒B已經把X的值修改為2,為何執行緒A獲取的還是l呢?這就是共享變數的記憶體不可見問題,也就是執行緒B寫入的值對執行緒A不可見,
synchronized 的記憶體語意:
這個記憶體語意就可以解決共享變數記憶體可見性問題,進入synchronized塊的記憶體語意是把在synchronized塊內使用到的變數從執行緒的作業記憶體中清除,這樣在synchronized塊內使用到該變數時就不會從執行緒的作業記憶體中獲取,而是直接從主記憶體中獲取,退出synchronized塊的記憶體語意是把在synchronized塊內對共享變數的修改重繪到主記憶體,會造成背景關系切換的開銷,獨占鎖,降低并發性
Volatile的理解:
該關鍵字可以確保對一個變數的更新對其他執行緒馬上可見,當一個變數被宣告為volatile時,執行緒在寫入變數時不會把值快取在暫存器或者其他地方,而是會把值重繪回主記憶體,當其他執行緒讀取該共享變數時-,會從主記憶體重新獲取最新值,而不是使用當前執行緒的作業記憶體中的值,volatile的記憶體語意和synchronized有相似之處,具體來說就是,當執行緒寫入了volatile變數值時就等價于執行緒退出synchronized同步塊(把寫入作業記憶體的變數值同步到主記憶體),讀取volatile變數值時就相當于進入同步塊(先清空本地記憶體變數值,再從主記憶體獲取最新值),不能保證原子性
二、實作多執行緒
方式1:繼承Thread類
需求:我們要實作多執行緒的程式, 如何實作呢?
由于執行緒是依賴行程而存在的,所以我們應該先創建一個行程出來,
而行程是由系統創建的,所以我們應該去呼叫系統功能創建一個行程,
Java是不能直接呼叫系統功能的,所以,我們沒有辦法直接實作多執行緒程式, 但是呢? Java可以去呼叫C / C++寫好的程式來實作多執行緒程式,
由C/C++去呼叫系統功能創建行程,然后由Java去呼叫這祥的API, 然后提供一些類供我們使用,我們就可以實作多執行緒程式了,
那么Java提供的類是什么呢?
- Thread類
步驟 - A : 自定義類
MyThread繼承Thread - B : 重寫run () 方法
- 為什么是run ()方法呢?
- C :創建物件
- D : 啟動執行緒
注意:單獨呼叫 run() 方法其實和呼叫一個類的普通方法是沒有區別的,要想啟動執行緒實際上應該呼叫的是 start() 方法,這是為什么呢?
start() 和 run() 的區別?
- start() :
使該執行緒開始執行;Java 虛擬機呼叫該執行緒的 run() 方法,
用start方法來啟動執行緒,真正實作了多執行緒運行,這時無需等待run方法體中的代碼執行完畢而直接繼續執行后續的代碼,通過呼叫Thread類的 start()方法來啟動一個執行緒,這時此執行緒處于就緒(可運行)狀態,并沒有運行,一旦得到cpu時間片,就開始執行run()方法,這里的run()方法 稱為執行緒體,它包含了要執行的這個執行緒的內容,Run方法運行結束,此執行緒隨即終止,
- run() :
如果該執行緒是使用獨立的 Runnable 運行物件構造的,則呼叫該 Runnable 物件的 run 方法;否則,該方法不執行任何操作并回傳,
Thread 的子類應該重寫該方法,
run()方法只是類的一個普通方法而已,如果直接呼叫Run方法,程式中依然只有主執行緒這一個執行緒,其程式執行路徑還是只有一條,還是要順序執行,還是要等待run方法體執行完畢后才可繼續執行下面的代碼,這樣就沒有達到寫執行緒的目的,
總結:


兩個小問題
- 為什么要重寫 run()方法?
- 因為run()是用來封裝被執行緒執行的代碼
- run() 方法和 start() 方法的區別?
- run():封裝執行緒執行的代碼,直接呼叫,相當于普通方法的呼叫
- start():啟動執行緒;然后由
JVM呼叫此執行緒的run()方法
創建自定義執行緒代碼:
//定義一個執行緒類
public class MyThread extends Thread {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(i);
}
}
}
//呼叫方法執行執行緒
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
// my1.run();
// my2.run();
//void start() 導致此執行緒開始執行; Java虛擬機呼叫此執行緒的run方法
my1.start();
匿名內部類的方式創建一個執行緒
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多執行緒");
}
}).start();
獲取和設定執行緒物件名稱

//void setName(String name):將此執行緒的名稱更改為等于引數 name
my1.setName("高鐵");
my2.setName("飛機");
//Thread(String name)
MyThread my1 = new MyThread("高鐵");
MyThread my2 = new MyThread("飛機");
System.out.println(Thread.currentThread().getName());
執行緒優先級

執行緒控制


等待執行緒死亡
public class ThreadTest {
public static void main(String[] args) {
ThreadDemo th1=new ThreadDemo();
ThreadDemo th2=new ThreadDemo();
ThreadDemo th3=new ThreadDemo();
th1.setName("康熙");
th2.setName("四阿哥");
th3.setName("八阿哥");
th1.start();
try {
th1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//下面的兩個執行緒會等待康熙執行緒死亡以后才開始執行
th2.start();
th2.start();
}
}
守護執行緒
th1.setName("劉備");
th2.setName("張飛");
th3.setName("關羽");
th2.setDaemon(true);
th3.setDaemon(true);
th2.start();
th3.start();
th1.start();
//th2、th3 在th1死亡時,也會跟著死亡
禮讓執行緒
/*
* public static void yield():暫停當前正在執行的執行緒物件,并執行其他執行緒,
* 讓多個執行緒的執行更和諧,但是不能靠它保證一人一次,
*/
public class ThreadYield extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++){
System.out.println(getName() + ":" + i);
Thread.yield();//暫停當前正在執行的執行緒物件
}
}
}
public class TreadDemo {
public static void main(String[] args) {
ThreadYield th1 = new ThreadYield();
ThreadYield th2 = new ThreadYield();
th1.setName("呵呵");
th2.setName("哈哈");
th1.start();
th2.start();
}
}
JOJO:0
林青霞:0
JOJO:1
林青霞:1
JOJO:2
林青霞:2
JOJO:3
林青霞:3
JOJO:4
林青霞:4
JOJO:5
林青霞:5
JOJO:6
林青霞:6
JOJO:7
林青霞:7
林青霞:8
JOJO:8
林青霞:9
JOJO:9
林青霞:10
JOJO:10
林青霞:11
JOJO:11
林青霞:12
JOJO:12
林青霞:13
林青霞:14
JOJO:13
林青霞:15
JOJO:14
JOJO:15
林青霞:16
JOJO:16
林青霞:17
JOJO:17
JOJO:18
林青霞:18
JOJO:19
林青霞:19
Process finished with exit code 0
中斷執行緒
stop() 中斷執行緒很暴力,后面的程式都不走了
public boolean isInterrupted()
public void interrupt()
public static boolean interrupted()

執行緒的生命周期

方式2:實作Runnable介面
多執行緒的實作方案有兩種
- 繼承 Thread類
- 實作 Runnable介面
相比繼承 Thread類,實作Runnable介面的好處 - 避免了 Java單繼承的局限性
- 適合多個相同程式的代碼去處理同一個資源的情況,把執行緒和程式的代碼、資料有效分離,較好的體現了面向物件的設計思想
MyRunnable runnable = new MyRunnable();
Thread th1 = new Thread(runnable,"執行緒一");
Thread th2 = new Thread(runnable,"執行緒二");
th1.start();
th2.start();
/**
* 自定義runnable介面,
*/
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("自定義runnable介面下的run()方法執行了...");
}
}
方式3:實作Callable介面,執行緒池
三、執行緒安全問題
安全問題出現的條件
- 是多執行緒環境
- 有共享資料
- 有多條陳述句操作共享資料
同步的好處和弊端
- 好處:解決了多執行緒的資料安全問題
- 弊端:當執行緒很多時,因為每個執行緒都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程式的運行效率
怎么實作呢 ?
- 把多條陳述句操作共享資料的代碼給鎖起來,讓任意時刻只能有一個執行緒執行即可
- Java 提供了同步代碼塊的方式來解決
synchronized
方式一:synchronized 同步代碼塊
Object obj=new Object();
synchronized (obj) {
多條陳述句操作共享資料的代碼
}


Lock類
方式二:Lock類
- Lock是介面不能直接實體化,這里采用它的實作類
ReentrantLock來實體化 - 使用
try....finally代碼塊來包裹
private int ticket=100;
private Lock lock=new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "賣票" + ticket);
ticket--;
}
}finally {
lock.unlock();
}
執行緒安全的集合
回顧:StringBuffer、Vector、Hashtable集合

四、練習案例
兩種匿名內部類方式創建執行緒
public class TreadDemo02 {
public static void main(String[] args) {
// 繼承Thread類實作多執行緒
new Thread(){
@Override
public void run() {
for (int i = 0; i<20;i++){
System.out.println(Thread.currentThread().getName() + ": Thread : " + i);
}
}
}.start();
// 實作Runnable介面實作多執行緒
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i<20;i++){
System.out.println(Thread.currentThread().getName() + ": Runnable : " + i);
}
}
}){
//thread子類
}.start();
}
}

簡單面試題 01
1:多執行緒有幾種實作方案,分別是哪幾種?
兩種,
繼承Thread類
實作Runnable介面
擴展一種:實作Callable介面,這個得和執行緒池結合,
2:同步有幾種方式,分別是什么?
兩種,
同步代碼塊
同步方法
3:啟動一個執行緒是run()還是start()?它們的區別?
start();
run():封裝了被執行緒執行的代碼,直接呼叫僅僅是普通方法的呼叫
start():啟動執行緒,并由JVM自動呼叫run()方法
4:sleep()和wait()方法的區別
sleep():必須指時間;不釋放鎖,
wait():可以不指定時間,也可以指定時間;釋放鎖,
5:為什么wait(),notify(),notifyAll()等方法都定義在Object類中
因為這些方法的呼叫是依賴于鎖物件的,而同步代碼塊的鎖物件是任意鎖,
而Object代碼任意的物件,所以,定義在這里面,
回顧集合和IO的面試題
集合:
1: HashMap和Hashtable的區別,
2:Collection 和 Collections的區別,
3: List, Set, Map是否繼承自Collection介面?
4:說出ArrayList,Vector, LinkedList的存盤性能和特性?
5:你所知道的集合類都有哪些?主要方法?
IO:
1: java中有幾種型別的流?JDK為每種型別的流提供了一些抽象類以供繼承,請說出他們分別是哪些類?
2:什么是java序列化,如何實作java序列化?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/181811.html
標籤:Java
