一、多線程的基本概念
1.什麼是進程
- 一個進程中對應一個應用程序,例如:在windows操作系統啓動Word就表示啓動了一個進程。在java的開發環境下啓動JVM,就表示啓動了一個進程,現代的計算機都是支持多進程的,在同一個操作系統中,可以同時啓動多個進程。
2.多進程有什麼作用?
- 單進程計算機只能做一件事情
- 多進程的作用不是提高執行速度,而是提高CPU的使用率
- 進程和進程之間的內存是獨立的
3.什麼是線程
- 線程是一個進程中的執行場景,在一個應用程序中可以同時執行多個功能,每一個功能就對應一個線程。一個進程可以啓動多個線程
4.多線程有什麼作用?
- 多線程不是爲了提高執行速度,而是提高應用程序的使用率
- 線程和線程共享“堆內存和方法區內存”,棧內存是獨立的,一個線程一個棧
5.java程序的運行原理
- java命令會啓動java虛擬機,啓動JVM,等於啓動了一個應用程序,表示啓動一個進程。該進程會自動啓動一個“主線程”,然後主線程去調用某個類的main方法,所以main方法進行在主線程中。在此之前的所有程序都是單線程的。
6.簡單辨別線程的案例
package Thread;
public class test01 {
public static void main(String[] args) {
m1();
}
private static void m1() {
m2();
}
private static void m2() {
m3();
}
private static void m3() {
System.out.println("m3...");
}
}
- 從以上可以看出,以上程序只有一個線程(單線程),就是主線程
- main方法調用m1方法,再調用m2,再調用m3,一個線程一個棧,一個線程就是主線程,mian、m1、m2、m3這四個方法在同一個棧空間中(類似於數據結構中棧的棧頂、棧底),沒有啓動其他任何線程
二、線程的創建和啓動
- 實際上,java程序在運行中至少有兩個線程,主線程和垃圾回收機制(gc)
- 主線程:JVM啓動時會創建一個主線程,用來執行main()中的代碼
- gc:低級別的線程,用來回收垃圾對象
- 如果需要實現多個線程,可以自定義線程
- 兩種方式:
1.繼承Thread類
package Thread;
public class test02 {
public static void main(String[] args) {
Thread t = new Processor9();
t.start();
for (int i=0;i<10;i++){
System.out.println("main-->"+i);
}
}
}
class Processor9 extends Thread{
public void run(){
for (int i=0;i<10;i++){
System.out.println("run-->"+i);
}
}
}
- 步驟:
- 定義一個類,繼承Thread,Thread是線程的父類,提供了一些操作線程的方法
- 重寫父類中run()方法
- 創建線程類的實例,創建線程對象
- 啓動線程,線程對象調用start()方法,不能直接調用run()方法
- 爲了更好的方便理解,下面圖示:
- 首先是java虛擬機(JVM),啓動主線程,然後調用main()方法
- 主線程啓動,分配主線程的棧,第一次調用main()方法,會壓棧
- 堆內存裏面存放創建線程的對象,內存地址指向線程對象
- t調用start方法,在java虛擬機裏面另分配一塊棧空間(t線程棧)
- 再調用run方法,會在t線程棧壓入run方法棧幀
- run方法還會調用其他的方法,會繼續壓棧,也會在run方法中再次啓動一個線程,會再次分配一個棧空間
2.實現Runnable接口
package Thread;
public class test03 {
public static void main(String[] args) {
Processor10 p = new Processor10();
Thread t = new Thread(p);
t.start();
}
}
class Processor10 implements Runnable{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println("run--->"+i);
}
}
}
- 步驟:
- 定義一個自定義類,實現Runnable接口
- 實現 run()方法
- 創建該類的實例,創建線程對象
- 創建Thread類,將自己的線程類對象傳入
- 啓動線程
3.對比
- 繼承Thread:java是單一繼承,無法繼承多個類
- 實現Runnable:避免了單一繼承的問題,保留了類的繼承。適合多個線程去處理同一個資源(共享一個資源)
- 一般使用實現Runnable接口的方式
三、線程的生命週期
1.CPU時間片
- 對於單核系統,某個時間點只能操作一件事情
- CPU爲各個程序分配時間,稱爲時間片,該進程運行的時間(時間很短)
- 從表面看每個程序同時運行的,實際上在同一時間點只能執行一個程序
- 只是CPU在很短的時間內,在不同的程序之間切換,輪流執行每個程序,執行的速度很快,感覺上在同時執行
2.爲了更便於理解,下面是線程的生命週期圖示
- 步驟:
- new出來的線程(t1、t2、t3…),調用start()方法,進入就緒狀態
- 就緒狀態有權利獲取CPU時間片,拿到時間片之後,到運行狀態,run()方法執行,CPU時間片用完,再回到就緒狀態,等CPU時間片,拿到CPU時間片再運行,反覆如此,直到該線程的run()方法執行結束,整個線程就會銷燬
- 線程也有可能遇到阻塞事件,進入阻塞狀態,阻塞解除,進入到就緒狀態
- 所以線程的生命週期有5個狀態:新建、就緒、運行、阻塞、銷燬
3.方法
方法 |
含義 |
start() |
啓動線程,進入就緒狀態,有權利獲取CPU時間片 |
sleep() |
該方法是一個靜態方法,休眠線程,當線程執行該方法時,不搶奪CPU時間片,阻塞當前線程,騰出CPU,讓給其他線程,從運行到阻塞狀態。如果阻塞解除,進入到就緒狀態,繼續爭奪CPU時間片。從運行到阻塞 |
join() |
是一個成員方法,當前線程可以調用另一個線程的join方法,調用後當前線程會被阻塞不再執行,直到被調用的線程執行完畢,當前線程纔會執行。從運行到阻塞 |
yield() |
該方法是一個靜態方法,它與sleep()類似,只是不能由用戶指定暫停多長時間,並且yield()方法只能讓同優先級的線程有執行的機會 。從運行到阻塞 |
interrupt() |
中斷該線程的休眠狀態,使它不再休眠,進入到就緒狀態。如果一個線程的休眠狀態被中斷,會報異常錯誤信息,所以要寫異常錯誤處理機制。從休眠到就緒 |
四、線程的安全性問題
1.線程安全問題
- 多個線程同時訪問共享數據可能出現問題,稱爲線程的安全問題性
- 當多個線程同時訪問數據時,由於CPU的切換,導致一個線程只執行了一部分代碼,沒有執行完成,此時另一個線程又參與進來,導致共享數據發生異常(例如:銀行取款倉庫總共5000元,t1取1000元,t2取款的時候一定要等t1取完款,銀行倉庫更新爲4000元的時候,t2纔可以進行取款操作。如果銀行倉庫的錢還沒有更新,有可能還是5000元,t2取款的時候,數據就會出現異常)
2.同步線程和異步線程
- 同步線程:在多個線程同時執行時,一個線程要等待上一個線程執行完成後,纔開始執行(類似上廁所排隊)
- 異步線程:在多個線程同時執行時,不用等待上面的線程是否結束,多個線程一起執行,誰也不等誰
3.什麼時候要同步?爲什麼引入線程同步?什麼條件下要使用線程同步?
- 爲了數據的安全,儘管應用程序的使用率降低,但是爲了保證數據是安全的,必須加入線程同步機制
- 什麼條件下要使用線程同步?
- 必須是多線程環境
- 多線程環境共享同一個數據
- 共享的數據涉及到修改操作(提醒:查詢操作不需要使用線程同步)
4.解決線程安全問題
- 線程的同步機制(就是將異步線程變爲同步線程)
- synchronized + 鎖
- 同步方法(使用synchronized 關鍵字修飾成員方法,線程拿走的也是this的對象鎖)
public synchronized void withdraw(參數){......}
- 同步代碼塊(在成員方法裏面使用synchronized 修飾代碼塊)
synchronized(對象鎖) { 代碼塊 }
- 上述添加線程同步兩種方法的對比:
- 使用同步方法方式,整個成員方法都需要同步
- 使用同步代碼塊方式,會比較經濟,因爲該成員方法有可能會有別的代碼塊,不一定是同步代碼塊
- 鎖:稱爲對象鎖,每個對象都只帶一個鎖(標識),不同對象有不同的鎖
- 線程安全的還有像:vector、Hashtable、StringBuffer
5.代碼案例(線程同步+鎖)
package Thread;
public class ThreadTest02 {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
Account1 act = new Account1("actno-01",5000.0);
Processor1 p = new Processor1(act);
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.start();
t2.start();
long endTime = System.currentTimeMillis();
System.out.println("運行時間:"+(endTime-startTime));
}
}
class Processor1 implements Runnable{
Account1 act;
Processor1(Account1 act){
this.act = act;
}
@Override
public void run() {
act.withdraw(1000.0);
System.out.println("取款1000.0成功,餘額:"+act.getBalance());
}
}
class Account1{
private String actno;
private double balance;
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public Account1(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public Account1() {
}
public void withdraw(double money){
synchronized (this){
double after = balance - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
- 原理執行過程:
- 把需要同步的代碼,放到同步語句塊中
- t1線程和t2線程。t1線程執行到同步代碼塊時,遇到了synchronized 關鍵字,就會去找this的對象鎖,如果找到this對象鎖,則進入同步語句塊中執行程序,此時該對象不再擁有鎖。當同步語句塊中的代碼執行結束之後,t1線程釋放this的對象鎖
- 在t1線程執行同步語句塊的過程中,如果t2線程也過來執行以下代碼,也遇到synchronized 關鍵字,所以也去找this的對象鎖,但是該對象鎖被t1線程持有,t2線程會進入對象的鎖池中等待,直到鎖被歸還,此時需要鎖的線程去競爭
五、線程的通信
1.鎖池和等待池(根據上面線程的生命週期那張圖對應着看)
- 首先我們要明確每個對象都有鎖池和等待池
- 鎖池:
- 當線程無法獲取鎖,此時進入鎖池中
- 如果對象的鎖被釋放,鎖池中的多個線程競爭鎖
- 等待池:
- 當線程獲取鎖後,可以調用wait(),放棄鎖,進入等待池中
- 當其他線程調用notify,notifyAll方法,等待池中的線程將被喚醒,進入鎖池
- 在鎖池中繼續競爭鎖
2.方法
方法 |
含義 |
wait() |
放棄對象鎖 |
notify() |
隨機喚醒一個等待池中的線程 |
notifyAll() |
喚醒等待池中的所有線程 |
- 重點提醒:
- 這三個在Object類中定義的
- 這三個方法只能在synchronized 中使用,只有獲取了鎖的線程才能使用
- 等待和喚醒必須使用的是同一個對象
3.代碼案例
package Thread;
public class ThreadTest11 {
public static void main(String[] args) {
Object o = new Object();
Thread t1 = new MyT1(o);
Thread t2 = new MyT2(o);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class MyT1 extends Thread{
private Object o;
public MyT1(Object o) {
this.o = o;
}
public void run(){
System.out.println("t1的線程");
synchronized (o){
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("222");
}
}
class MyT2 extends Thread{
private Object o;
public MyT2(Object o){
this.o = o;
}
public void run(){
System.out.println("t2的線程");
synchronized (o){
o.notifyAll();
}
}
}
t1的線程
t2的線程
222
-----------------------
t2的線程
t1的線程
- 第一種結果的原理:
- t1線程遇到synchronized 關鍵字,執行代碼塊中的wait()方法,釋放當前鎖,讓出CPU,進入等待池中
- t2線程遇到synchronized 關鍵子,執行代碼塊中的notifyAll()方法,等待池中的線程將被喚醒,進入鎖池
- t1在鎖池中繼續競爭鎖,執行上一次沒有完成的代碼,會再輸出222
- 第二種結果的原理:
- t2線程遇到synchronized 關鍵字,執行代碼塊中的notifyAll()方法,但是等待池中沒有線程可以喚醒,所以輸出222
- t1線程線程遇到synchronized 關鍵字,執行代碼塊中的wait()方法,釋放當前鎖,讓出CPU,進入等待池中。由於沒有其它線程喚醒t1線程,t1線程一直在等待池中,程序一直不結束,一直在運行
六、面試題(寫一個死鎖的程序)
1.小故事
- 程序猿應該都知道哲學家進餐問題的故事
- 簡單來說就是有5位哲學家圍在一個圓桌上吃飯,但是每位哲學家只有一隻筷子,要想吃飯,必須要兩隻筷子才行。所以其中一位哲學家要依靠左邊的或者右邊的哲學家一起夾菜才能吃到,但是大家都不願意,於是大家都吃不到菜
2.死鎖原理
- 在線程中,比如t1線程已經得到其中一個鎖,但是我還需要另外一個t2線程的鎖才能運行。然而t2線程得到了其中的鎖,但是它也同時需要t1線程的鎖才能執行,於是大家都不能執行相應的代碼塊,一直在爭奪對方的鎖,一直處在死鎖當中,程序一直在運行當中,停不下來
3.代碼案例
package Thread;
public class ThreadTest07 {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread(new T1(o1,o2));
Thread t2 = new Thread(new T2(o1,o2));
t1.start();
t2.start();
}
}
class T1 implements Runnable{
Object o1;
Object o2;
T1(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class T2 implements Runnable{
Object o1;
Object o2;
T2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
如果對你有幫助,不如點個贊,也算是支持一下0.0
若有不正之處,請多多諒解並歡迎批評指正,不甚感激