進程與線程
在學習Java多線程之前,先簡單復習一下進程與線程的知識。
進程:進程是系統進行資源分配和調度的基本單位,可以將進程理解為一個正在執行的程序,比如一款游戲。
線程:線程是程序執行的最小單位,一個進程可由一個或多個線程組成,在一款運行的游戲中通常會有界面
更新線程、游戲邏輯線程等,線程切換的開銷遠小於進程切換的開銷。
圖1
在圖1中,藍色框表示進程,黃色框表示線程。進程擁有代碼、數據等資源,這些資源是共享的,3個線程都可
以訪問,同時每個線程又擁有私有的棧空間。
Java線程狀態圖
線程的五種狀態:
1)新建狀態(New):線程對象實例化后就進入了新建狀態。
2)就緒狀態(Runnable):線程對象實例化后,其他線程調用了該對象的start()方法,虛擬機便會啟
動該線程,處於就緒狀態的線程隨時可能被調度執行。
3)運行狀態(Running):線程獲得了時間片,開始執行。只能從就緒狀態進入運行狀態。
4)阻塞狀態(Blocked):線程因為某個原因暫停執行,並讓出CPU的使用權后便進入了阻塞狀態。
等待阻塞:調用運行線程的wait()方法,虛擬機會把該線程放入等待池。
同步阻塞:運行線程獲取對象的同步鎖時,該鎖已被其他線程獲得,虛擬機會把該線程放入鎖定池。
其他線程:調用運行線程的sleep()方法或join()方法,或線程發出I/O請求時,進入阻塞狀態。
5)結束狀態(Dead):線程正常執行完或異常退出時,進入了結束狀態。
Java線程實現
Java語言提供了兩種實現線程的方式:
1)通過繼承Thread類實現線程
public class ThreadTest { public static void main(String[] args){ Thread thread = new MyThread(); //創建線程 thread.start(); //啟動線程 } }
//繼承Thread類 class MyThread extends Thread{ @Override public void run() { int count = 7; while(count>0){ System.out.println(count); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; } } }
2)通過實現Runnable接口實現線程
public class ThreadTest { public static void main(String[] args){ Runnable runnable = new MyThread();
//將Runnable對象傳遞給Thread構造器 Thread thread = new Thread(runnable); thread.start(); } }
//實現了Runnable接口 class MyThread implements Runnable{ @Override public void run() { int count = 7; while(count>0){ System.out.println(count); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; } } }
兩種方式都覆寫了run()方法,run()方法內定義了線程的執行內容,我們只能通過線程的start()方法來
啟動線程,且start()方法只能調用一次,當線程進入執行狀態時,虛擬機會回調線程的run()方法。直
接調用線程的run()方法,並不會啟動線程,只會像普通方法一樣去執行。
其實,Thread類本身也實現了Runnable接口。這兩種方式都可以實現線程,但Java語言只支持單繼
承,如果擴展了Thread類就無法再擴展其他類,遠沒有實現接口靈活。
線程常用方法
1)Thread類
Thread():用於構造一個新的Thread。
Thread(Runnable target):用於構造一個新的Thread,該線程使用了指定target的run方法。
Thread(ThreadGroup group,Runnable target):用於在指定的線程組中構造一個新的Thread,該
線程使用了指定target的run方法。
currentThread():獲得當前運行線程的對象引用。
interrupt():將當前線程置為中斷狀態。
sleep(long millis):使當前運行的線程進入睡眠狀態,睡眠時間至少為指定毫秒數。
join():等待這個線程結束,即在一個線程中調用other.join(),將等待other線程結束后才繼續本線程。
yield():當前執行的線程讓出CPU的使用權,從運行狀態進入就緒狀態,讓其他就緒線程執行。
2)Object類
wait():讓當前線程進入等待阻塞狀態,直到其他線程調用了此對象的notify()或notifyAll()方法后,當
前線程才被喚醒進入就緒狀態。
notify():喚醒在此對象監控器上等待的單個線程。
notifyAll():喚醒在此對象監控器上等待的所以線程。
注:wait()、notify()、notifyAll()都依賴於同步鎖,而同步鎖是對象持有的,且每個對象只有一個,所以
這些方法定義在Object類中,而不是Thread類中。
3)yield()、sleep()、wait()比較
wait():讓線程從運行狀態進入等待阻塞狀態,並且會釋放它所持有的同步鎖。
yield():讓線程從運行狀態進入就緒狀態,不會釋放它鎖持有的同步鎖。
sleep():讓線程從運行狀態進入阻塞狀態,不會釋放它鎖持有的同步鎖。
線程同步
先看一個多線程模擬賣票的例子,總票數7張,兩個線程同時賣票:
public class ThreadTest{ public static void main(String[] args){ Runnable r = new MyThread(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); } } class MyThread implements Runnable{ private int tickets = 7; //票數
@Override public void run(){ while(tickets>0){ System.out.println("tickets:"+tickets); tickets--; try{ Thread.sleep(100); }catch(InterruptedException e){ e.printStackTrace(); } } } }
運行結果:
運行結果不符合我們的預期,因為兩個線程使用共享變量tickets,存在着由於交叉操作而破壞數據的可能性,
這種潛在的干擾被稱作臨界區,通過同步對臨界區的訪問可以避免這種干擾。
在Java語言中,每個對象都有與之關聯的同步鎖,並且可以通過使用synchronized方法或語句來獲取或釋放
這個鎖。在多線程協作時,如果涉及到對共享對象的訪問,在訪問對象之前,線程必須獲取到該對象的同步
鎖,獲取到同步鎖后可以阻止其他線程獲得這個鎖,直到持有鎖的線程釋放掉鎖為止。
public class ThreadTest{ public static void main(String[] args){ Runnable r = new MyThread(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); } } class MyThread implements Runnable{ private int tickets = 7; @Override public void run(){ while(tickets>0){ synchronized(this){ //獲取當前對象的同步鎖 if(tickets>0){ System.out.println("tickets:"+tickets); tickets--; try{ Thread.sleep(100); }catch(InterruptedException e){ e.printStackTrace(); } } } } } }
運行結果:
synchronized用法:
1)synchronized方法:如果一個線程要在某個對象上調用synchronized方法,那么它必須先獲取這個對象的
鎖,然后執行方法體,最后釋放這個對象上的鎖,而與此同時,在同一個對象上調用synchronized方法的其他
線程將阻塞,直到這個對象的鎖被釋放為止。
public synchronized void show(){ System.out.println("hello world"); }
2)synchronized靜態方法:靜態方法也可以被聲明為synchronized的,每個類都有與之相關聯的Class對象,
而靜態同步方法獲取的就是它所屬類的Class對象上的鎖,兩個線程不能同時執行同一個類的靜態同步方法,如
果靜態數據是在線程之間共享的,那么對它的訪問就必須利用靜態同步方法來進行保護。
3)synchronized語句:靜態語句可以使我們獲取任何對象上的鎖而不僅僅是當前對象上的鎖,也能夠讓我們定
義比方法還要小的同步代碼區,這樣可以讓線程持有鎖的時間盡可能短,從而提高性能。
private final Object lock = new Object(); public void show(){ synchronized(lock){ System.out.println("hello world"); } }