1 并發與并行
并發:指兩個或多個事件在同一個時間段內發生。
并行:指兩個或多個事件在同一時刻發生(同時發生)。
并發指的是在一段時間內宏觀上有多個程序同時運行:
- 在單 CPU 系統中,每一時刻只能有一道程序執行,即微觀上這些程序是分時的交替運行,只不過是給人的感覺是同時運行,那是因為分時交替運行的時間是非常短的。
- 在多個 CPU 系統中,則這些可以并發執行的程序便可以分配到多個處理器上(CPU),實現多任務并行執行,即利用每個處理器來處理一個可以并發執行的程序,這樣多個程序便可以同時執行。目前電腦市場上說的多核CPU,便是多核處理器,核越多,并行處理的程序越多,能大大的提高電腦運行的效率。
注意:單核處理器的計算機肯定是不能并行的處理多個任務的,只能是多個任務在單個CPU上并發運行。同理,線程也是一樣的,從宏觀角度上理解線程是并行運行的,但是從微觀角度上分析卻是串行運行的,即一個線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,我們把這種情況稱之為線程調度。
2 線程與進程
程序:指令和數據的有序集合,本身沒任何運行含義,是靜態概念
進程:是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。(進入內存中運行的程序,叫進程)
線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之為多線程程序。
簡而言之:一個程序運行后至少有一個進程,一個進程中可以包含多個線程
線程調度:
- 分時調度
? 所有線程輪流使用 CPU 的使用權,平均分配每個線程占用 CPU 的時間。
- 搶占式調度
? 優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那么會隨機選擇一個(線程隨機性);
? Java使用的為搶占式調度。
多線程程序并不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。
在程序運行時,即使沒有自己創建線程,后臺也會有多個線程,如主線程、gc線程
main()線程稱之為主線程,為系統的入口,用于執行整個程序
在一個進程中,如果開辟了多線程,線程的運行由調度器安排調度,調度器是與操作系統緊密相關的,先后順序是不能人為的干預
對同一份資源操作時,會出現資源搶奪問題,需加入并發控制
線程會帶來額外的開銷,如cpu調度時間,并發控制開銷
每個線程在自己的工作內存交互,內存控制不當會造成數據不一致
3 線程
3.1 Thread類
Java使用java.lang.Thread 類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。
構造方法:
- public Thread() :分配一個新的線程對象。
- public Thread(String name) :分配一個指定名字的新的線程對象。
- public Thread(Runnable target) :分配一個帶有指定目標新的線程對象。
- public Thread(Runnable target,String name) :分配一個帶有指定目標新的線程對象并指定名字。
常用方法:
- public String getName() :獲取當前線程名稱。
- public void start() :導致此線程開始執行; Java虛擬機調用此線程的run方法。
- public void run() :此線程要執行的任務在此處定義代碼。
- public static void sleep(long millis) :使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
- public static Thread currentThread() :返回對當前正在執行的線程對象的引用。
主線程
JVM執行main方法,main方法會進入到棧內存,JVM會找操作系統開辟一條 main方法通向CPU的執行路徑,CPU就可以通過這個路徑來執行main方法,而此路徑就稱為main(主)線程。
-
普通方法調用和多線程調用
1607496016082.png
3.2 創建線程
3.2.1 方式一:繼承Thread類
Java中通過繼承Thread類來創建并啟動多線程的步驟如下:
- 定義Thread類的子類,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,因此把run()方法稱為線程執行體。
- 創建Thread子類的實例,即創建了線程對象
- 調用線程對象的start()方法來啟動該線程
- 啟動線程后,不一定立即執行,由cpu調度安排(主線程與thread線程,穿插進行)
- 子類繼承Thread類具有多線程的能力(Thread類實現類Runnable接口)
- 啟動線程:子類對象.start()
- 不建議使用:避免OOP單繼承局限性
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "正在執行!" + i);
}
}
}
public class DemoTest {
public static void main(String[] args) {
MyThread thread = new MyThread("新的線程");
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main線程" + i);
}
}
}
3.2.2 方式二:實現Runnable接口
實現java.lang.Runnable接口,重寫run()方法。(Thread類也實現了Runnable接口)
步驟:
- 定義Runnable接口的實現類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
- 創建Runnable實現類的實例,并以此實例作為Thread的target來創建Thread對象,該Thread對象才是真正的線程對象。
- 調用線程對象的start()方法來啟動線程。
- 實現Runnable具有多線程能力
- 啟動線程:傳入目標對象+Thread對象.start()
- 推薦:避免單繼承局限性,方便同一個對象被多個線程使用
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.print(Thread.currentThread().getName()+ " " + i + ",");
}
}
}
public class DemoTest {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread tr = new Thread(mr, "小強");
Thread otr = new Thread(mr, "佩奇");
tr.start();
otr.start();
for (int i = 0; i < 10; i++) {
System.out.print("旺財" + i + ",");
}
}
}
/**
旺財0,旺財1,旺財2,旺財3,旺財4,旺財5,旺財6,旺財7,旺財8,旺財9,
佩奇 0,小強 0,佩奇 1,小強 1,佩奇 2,小強 2,佩奇 3,小強 3,佩奇 4,
小強 4,佩奇 5,小強 5,小強 6,小強 7,小強 8,小強 9,佩奇 6,佩奇 7,佩奇 8,佩奇 9,
*/
**所有的多線程代碼都在run方法里面
tips:Runnable對象僅僅作為Thread對象的target,Runnable實現類里包含的run()方法僅作為線程執行體。而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其target的run()方法。
3.2.3 方式三:匿名內部類方式實現線程創建
使用線程的內匿名內部類方式,可以方便的實現每個線程執行不同的線程任務操作。
使用匿名內部類的方式實現Runnable接口,重新Runnable接口中的run方法:
public class NoNameInnerClassThread {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("xiaoqinag" + i);
}
}
};
new Thread(runnable).start();
for (int i = 0; i < 20; i++) {
System.out.println("daqiang:"+i);
}
}
}
3.2.4 方式四:Lambda方式實現線程創建
public class LambdaThread {
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("xiaoqinag" + i);
}
}, "lambdaThread").start();
for (int i = 0; i < 20; i++) {
System.out.println("daqiang:"+i);
}
}
}
3.2.5 實現Callable接口
- 實現Callable接口
- 1.實現Callable接口,需要返回值類型
- 2.重寫call方法,需要拋出異常
- 3.創建目標對象t1
- 4.創建執行服務:
ExecutorService ser = Executors.newFixedThreadPool(1);
- 5.提交執行:
Future<Boolean> result1 = ser.submit(t1);
- 6.獲取結果:
boolean r1 = result1.get()
3.2.6 Thread和Runnable的區別
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。
實現Runnable接口比繼承Thread類所具有的優勢:
適合多個相同的程序代碼的線程去共享同一個資源。
可以避免java中的單繼承的局限性。
-
增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
- 實現Runnable接口的方式,把設置線程任務和開啟新線程進行分解(解耦)
- 實現類中重寫run方法:用來設置線程的任務;
- 創建Thread類對象,調用start方法:用來開啟新線程
線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。
擴充:在java中,每次程序運行至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在操作系統中啟動了一個進程。
3.3 多線程的原理
public class MyThread extends Thread{
/*
* 利用繼承中的特點
* 將線程名稱傳遞 進行設置
*/
public MyThread(String name){
super(name);
}
/*
* 重寫run方法
* 定義線程要執行的代碼
*/
public void run(){
for (int i = 0; i < 20; i++) {
//getName()方法 來自父親
System.out.println(getName()+i);
}
}
}
public class Demo {
public static void main(String[] args) {
System.out.println("這里是main線程");
MyThread mt = new MyThread("小強");
mt.start();//開啟了一個新的線程
for (int i = 0; i < 20; i++) {
System.out.println("旺財:"+i);
}
}
}
程序啟動運行main時候,java虛擬機啟動一個進程,主線程main在main()調用時候被創建。隨著調用mt的對象的start方法,另外一個新的線程也啟動了,這樣,整個應用就在多線程下運行。2個線程搶占CPU的執行資源
多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間
多個線程之間互不影響(在不同的棧空間)
3.4 守護(daemon)線程
- 線程分為用戶線程和守護線程
- 虛擬機必須確保用戶線程執行完畢
- 虛擬機不用等待守護線程執行完畢
- 用戶線程結束,守護線程也會停止
- 用戶線程變守護線程(setDaemon(true),默認為false)
public class DaemonThread {
public static void main(String[] args) {
//守護線程
Thread god = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName() + ":god...always");
}
}, "God thread");
god.setDaemon(true);
god.start();
//用戶線程
new Thread(() -> {
for (int i = 0; i < 36500; i++) {
System.out.println(Thread.currentThread().getName() + ":live" + i);
}
System.out.println("======GOODBYE WORLD=====");
}, "You thread").start();
}
}
4 線程安全
4.1 線程安全
如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
/** 模擬多窗口買票 */
public class Ticket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {//模擬出票
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣:" + ticket--);
}
}
}
}
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
/**
...
窗口1正在賣:3
窗口2正在賣:2
窗口3正在賣:1
窗口1正在賣:0
窗口2正在賣:-1
*/
線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
4.2 線程同步
當使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。
要解決上述多線程并發訪問一個資源的安全性問題,Java中提供了同步機制(synchronized)來解決。
線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面線程使用完畢,下一個線程再使用
實現同步機制的三種方式:(同步條件:隊列+鎖)
- 同步代碼塊。
- 同步方法。
- 鎖機制。
加鎖后存在的問題:
- 一個線程持有鎖會導致其他所有需要此鎖的線程掛起
- 在多線程競爭下,加鎖、釋放鎖,會導致比較多的上下文切換 和 調度延時,引起性能問題
- 如果一個優先級高的線程等待一個優先級低的線程釋放鎖,會導致優先級倒置,引起性能問題
4.2.1 同步代碼塊
同步代碼塊: synchronized 關鍵字可以用于方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
格式:
synchronized(同步鎖Obj){
//需要同步操作的代碼
}
同步鎖(同步監視器):
對象的同步鎖只是一個概念,可以想象為在對象上標記了一個鎖.
- 鎖對象obj 可以是任意類型,推薦使用共享資源作為同步監視器。
- 多個線程對象 要使用同一把鎖。
注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等著(BLOCKED)。
public class Ticket implements Runnable {
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {//鎖
if (ticket > 0) {
try {//模擬出票
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣:" + ticket--);
}
}
}
}
}
4.2.2 同步方法
同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等著。
格式
public synchronized void method(){
//可能會產生線程安全問題的代碼
}
同步鎖是誰?
- 對于非static方法,同步鎖就是this。
- 對于static方法,我們使用當前方法所在類的字節碼對象(類名.class)。
@Override
public void run() {
while (true) {
sellTicket();
}
}
/**
* 鎖對象 是 誰調用這個方法 就是誰
* 隱含 鎖對象 就是 this
*/
private synchronized void sellTicket() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣:" + ticket--);
}
}
4.2.3 鎖機制Lock
java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。
Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:
- public void lock() :加同步鎖。
- public void unlock() :釋放同步鎖。(放在finally中進行)
public class LockThread implements Runnable {
Lock lock = new ReentrantLock();//ReentrantLock 實現了 Lock 接口
@Override
public void run() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
4.2.4 synchronize && lock
- Lock是顯式鎖(手動開啟和關閉鎖),synchronize是隱式鎖,出了作用域自動釋放
- Lock只有代碼塊鎖,synchronize有代碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好,并且具有更好的擴展性(提供更多子類)
- 優先使用順序
- Lock > 同步代碼塊(已進入方法體,分配了相應資源) > 同步方法(在方法體外)
5 線程狀態
5.1 線程狀態概述
在API中java.lang.Thread.State 這個枚舉中給出了六種線程狀態
線程狀態 | 導致狀態發生條件 |
---|---|
NEW(新建) | 線程剛被創建,但是并未啟動。還沒調用start方法。 |
Runnable(可運行) | 線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決于操作系統處理器。 |
Blocked(鎖阻塞) | 當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。 |
Waiting(無限等待) | 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態后是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。 |
Timed Waiting(計時等待) | 同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被終止) | 因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。 |
Timed Waiting:A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
- 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單線程也可以調用,不一定非要有協作關系。
- 為了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣能保證該線程執行過程中會睡眠
- sleep與鎖無關,線程睡眠到期自動蘇醒,并返回到Runnable(可運行)狀態。
小提示:sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期后就開始立刻執行。
Blocked:A thread that is blocked waiting for a monitor lock is in this state.
- 受阻塞并且正在等待監視器鎖的某一線程的線程狀態。處于受阻塞狀態的某一線程正在等待監視器鎖,以便進入一個同步的塊/方法;
- 線程A與線程B代碼中使用同一鎖,如果線程A獲取到鎖,線程A進入到Runnable狀態,那么線程B就進入到Blocked鎖阻塞狀態。
Waiting : A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
- 一個正在無限期等待另一個線程執行一個特別的(喚醒)動作的線程處于這一狀態。
public class WaitingDemo {
public static Object obj = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (obj) {
try {
System.out.println(Thread.currentThread().getName()
+ "=== 獲取到鎖對象,調用wait方法,進入waiting狀態,釋放鎖對象");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "=== 從waiting狀態醒來,獲取到鎖對象,繼續執行了");
}
}
}
},"等待線程").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println(Thread.currentThread().getName()
+ "------- 等待3秒鐘");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println(Thread.currentThread().getName()
+ "------ 獲取到鎖對象,調用notify方法,釋放鎖對象");
obj.notify();
}
}
}
},"喚醒線程").start();
}
}
/**
等待線程=== 獲取到鎖對象,調用wait方法,進入waiting狀態,釋放鎖對象
喚醒線程------- 等待3秒鐘
喚醒線程------ 獲取到鎖對象,調用notify方法,釋放鎖對象
喚醒線程------- 等待3秒鐘
等待線程=== 從waiting狀態醒來,獲取到鎖對象,繼續執行了
等待線程=== 獲取到鎖對象,調用wait方法,進入waiting狀態,釋放鎖對象
喚醒線程------ 獲取到鎖對象,調用notify方法,釋放鎖對象
喚醒線程------- 等待3秒鐘
等待線程=== 從waiting狀態醒來,獲取到鎖對象,繼續執行了
等待線程=== 獲取到鎖對象,調用wait方法,進入waiting狀態,釋放鎖對象
喚醒線程------ 獲取到鎖對象,調用notify方法,釋放鎖對象
喚醒線程------- 等待3秒鐘
等待線程=== 從waiting狀態醒來,獲取到鎖對象,繼續執行了
等待線程=== 獲取到鎖對象,調用wait方法,進入waiting狀態,釋放鎖對象
...
*/
- 一個調用了某個對象的 Object.wait 方法的線程會等待另一個線程調用此對象的Object.notify()方法 或 Object.notifyAll()方法。其實waiting狀態并不是一個線程的操作,它體現的是多個線程間的通信,可以理解為多個線程之間的協作關系,多個線程會爭取鎖,同時相互之間又存在協作關系
- 當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運行)狀態中調用了wait()方法那么A線程就進入了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了notify()方法,那么就會將無限等待的A線程喚醒。注意是喚醒,如果獲取到鎖對象,那么A線程喚醒后就進入Runnable(可運行)狀態;如果沒有獲取鎖對象,那么就進入到Blocked(鎖阻塞狀態)。
線程個狀態之間的轉化:
public class StateThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("AAAAAAAAAAAAAAAAAA");
});
//NEW
Thread.State state = thread.getState();
System.out.println(state);
thread.start();
//RUNNABLE
state = thread.getState();
System.out.println(state);
while (state != Thread.State.TERMINATED) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//TIMED_WAITING
System.out.println(state);
state = thread.getState();
}
//TERMINATED
System.out.println(state);
}
}
5.2 線程的停止
- 線程運行完后自己停止
- 使用標志位控制線程停止(推薦)
- 不推薦jdk提供的stop、destroy方法(已廢棄)
public class StopThread implements Runnable {
//標志位
private boolean flag = true;
@Override
public void run() {
while (flag) {
System.out.println("thread....run...");
}
}
//對外提供改變標志位方法
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
StopThread target = new StopThread();
new Thread(target).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main.....run..." + i);
if (i == 988) {
target.stop();
System.out.println("thread....stop...");
}
}
}
}
5.3 線程休眠
- sleep(時間) 指定當前線程阻塞的毫秒數
- sleep存在異常 InterruptException
- sleep時間到達后線程進入就緒狀態
- sleep模擬網絡延時(放大問題發生的可能性)、倒計時等
- 每一個對象都有一個鎖,sleep不會釋放鎖(抱著鎖睡覺)
public class SleepThread implements Runnable {
private int ticketNum = 10;
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
break;
}
try {
//模擬網絡延時
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "張票");
}
}
public static void main(String[] args) {
TestTicketThread ticket = new TestTicketThread();
new Thread(ticket, "AA").start();
new Thread(ticket, "BB").start();
new Thread(ticket, "CC").start();
oneMinDown();
}
//倒計時
public static void oneMinDown() {
int num = 60;
for (int i = num; i > 0; i--) {
System.out.println(num--);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5.4 線程禮讓
- 禮讓線程yield,讓當前正在執行的線程暫停,但不阻塞
- 將線程從運行狀態轉為就緒狀態
- 讓cpu重新調度,禮讓不一定成功
public class YieldThread {
public static void main(String[] args) {
Runnable target = ()->{
System.out.println(Thread.currentThread().getName()+"-->start");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"-->end");
};
new Thread(target,"AA").start();
new Thread(target,"BB").start();
}
}
/*
禮讓成功 禮讓失敗
AA-->start AA-->start
BB-->start AA-->end
AA-->end BB-->start
BB-->end BB-->end
*/
5.5 線程強制執行
- join 合并線程,阻塞其他線程,待該線程執行完之后,再執行其他線程
- 類似插隊
public class JoinThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("thread...."+i);
}
});
thread.start();
for (int i = 0; i < 500; i++) {
if (i == 200) {
try {
//插隊
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main...." + i);
}
}
}
5.6 線程優先級
- Java提供一個線程調度器來監控程序中啟動后進入就緒狀態的所有線程,線程調度器按照優先級決定應該調度哪個線程來執行
- 線程優先級用數字表示,范圍1~10
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
- getPriority() 獲取優先級
- setPriority(int xxx) 設置優先級 優先級的設置在start()之前
- 優先級高低意味著獲得優先調度概率的高低,最終的調度還是看cpu
6 等待喚醒機制
6.1 線程間通信
概念:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。
- 為何要處理線程之間的通訊?
? 讓多線程在訪問同一份資源時按照一定的規律進行。
- 如何保證線程間通信有效利用資源:
? 多個線程在處理同一個資源,并且任務不同時,需要線程通信來幫助解決線程之間對同一個變量的使用或操作,避免對同一共享變量的爭奪————等待喚醒機制
6.2 等待喚醒機制
等待喚醒機制
- 是多個線程間的一種協作機制
- 在一個線程進行了規定操作后,就進入等待狀態(wait()), 等待其他線程執行完他們的指定代碼過后 再將其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待線程。
- wait/notify 就是線程間的一種協作機制。
等待喚醒中的方法
- wait:線程不再活動,不再參與調度,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。它還要等著別的線程執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到調度隊列(ready queue)中
- wait(long timeout):等待指定的毫秒數
- notify:則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位后,等候就餐最久的顧客最先入座。
- notifyAll:則釋放所通知對象的 wait set 上的全部線程,優先級別高的線程優先調度。
注意:
哪怕只通知了一個等待的線程,被通知線程也不能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功后才能在當初調用 wait 方法之后的地方恢復執行。
總結如下:
- 如果能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;
- 否則,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態
調用wait和notify方法需要注意的細節
- wait方法與notify方法必須要由同一個鎖對象調用。因為:對應的鎖對象可以通過notify喚醒使用同一個鎖對象調用的wait方法后的線程。
- wait方法與notify方法是屬于Object類的方法的。因為:鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的。
- wait方法與notify方法必須要在同步代碼塊或者是同步函數中使用,否則會拋出異常IIlegalMonitorStateException。因為:必須要通過鎖對象調用這2個方法。
6.3 生產者與消費者問題
等待喚醒機制其實就是經典的“生產者與消費者”的問題。
生產者和消費者共享同一個資源,并且生產者和消費者之間相互依賴,互為條件
- 對于生產者,沒有生產產品之前,要通知消費者等待。而生產產品之后,又要馬上通知消費者消費
- 對于消費者,在消費之后,要通知生產者已經結束消費,需要生產新的產品以供消費
解決方法:線程同步+線程通訊
6.3.1 信號燈法(通過標志位)
- 包子鋪線程生產包子,吃貨線程消費包子。當包子沒有時(包子狀態為false),吃貨線程等待,包子鋪線程生產包子(即包子狀態為true),并通知吃貨線程(解除吃貨的等待狀態),因為已經有包子了,那么包子鋪線程進入等待狀態。接下來,吃貨線程能否進一步執行則取決于鎖的獲取情況。如果吃貨獲取到鎖,那么就執行吃包子動作,包子吃完(包子狀態為false),并通知包子鋪線程(解除包子鋪的等待狀態),吃貨線程進入等待。包子鋪線程能否進一步執行則取決于鎖的獲取情況。
//資源
public class Baozi {
private String pier;
private String xianer;
private boolean flag = false;//包子資源,是否存在
public Baozi() {
}
public Baozi(String pier, String xianer) {
this.pier = pier;
this.xianer = xianer;
}
public String getPier() {
return pier;
}
public void setPier(String pier) {
this.pier = pier;
}
public String getXianer() {
return xianer;
}
public void setXianer(String xianer) {
this.xianer = xianer;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
//消費者
public class ChiHuo extends Thread {
private Baozi bz;
public ChiHuo(String name, Baozi bz) {
super(name);
this.bz = bz;
}
@Override
public void run() {
while (true) {
synchronized (bz) {
if (bz.isFlag() == false) {
try {
bz.wait();//吃貨等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃貨正在吃" + bz.getPier() + bz.getXianer() + "包子!");
System.out.println("包子吃完了!");
bz.setFlag(false);
bz.notify();//喚醒包子鋪
}
}
}
}
//生產者
public class BaoZiPu extends Thread {
private Baozi bz;
public BaoZiPu(String name, Baozi bz) {
super(name);
this.bz = bz;
}
@Override
public void run() {
int count = 0;
while (true) {
synchronized (bz) {
if (bz.isFlag()) {
try {
bz.wait();//包子鋪停止生產
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("包子鋪開始做包子");
if (count % 2 == 0) {
bz.setPier("冰皮");
bz.setXianer("五仁");
} else {
bz.setPier("薄皮");
bz.setXianer("牛肉大蔥");
}
count++;
bz.setFlag(true);
System.out.println("包子造好了:" + bz.getPier() + bz.getXianer());
System.out.println("吃貨來吃包子吧");
bz.notify();//喚醒吃貨吃包子
}
}
}
}
public class BaoZiTest {
public static void main(String[] args) {
Baozi bz = new Baozi();
BaoZiPu bzp = new BaoZiPu("包子鋪", bz);
ChiHuo ch = new ChiHuo("吃貨", bz);
bzp.start();
ch.start();
}
}
/*
包子鋪開始做包子
包子造好了:冰皮五仁
吃貨來吃包子吧
吃貨正在吃冰皮五仁包子!
包子吃完了!
包子鋪開始做包子
包子造好了:薄皮牛肉大蔥
吃貨來吃包子吧
吃貨正在吃薄皮牛肉大蔥包子!
包子吃完了!
包子鋪開始做包子
*/
6.3.2 管程法
生產者:負責生產數據的模塊(可能是方法、對象、線程、進程)
消費者:負責處理數據的模塊(可能是方法、對象、線程、進程)
緩沖區:消費者不能直接使用生產者的數據,他們之間有個“緩沖區”,生產者將生產好的數據放入緩沖區,消費者從緩沖區拿出數據
//生產者、消費者、產品、容器
public class PCThread {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Provider(container).start();
new Consumer(container).start();
}
}
//生產者
class Provider extends Thread {
SynContainer container;
public Provider(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("生產者生產第" + i + "只雞");
container.push(new Chicken(i));
}
}
}
//消費者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消費者消費第" + container.pop().id + "只雞");
}
}
}
//資源 雞
class Chicken {
int id;
public Chicken(int id) {
this.id = id;
}
}
//緩沖區 容器
class SynContainer {
//容器大小
Chicken[] chickens = new Chicken[10];
//容器計數器
int count;
//生產者放入產品
public synchronized void push(Chicken chicken) {
//容器滿了,生產者停止生產,等待消費者消費
if (count == chickens.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//沒滿,則放入產品
chickens[count] = chicken;
count++;
//通知消費者消費
this.notifyAll();
}
//消費者消費產品
public synchronized Chicken pop() {
//判斷能否消費
if (count == 0) {
//消費者等待生產者生產
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//可以消費
count--;
Chicken chicken = chickens[count];
//吃完了通知生產者生產
this.notifyAll();
return chicken;
}
}
7 線程池
7.1 概述
線程池:其實就是一個容納多個線程的容器,其中的線程可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建線程而消耗過多資源。
- 降低資源消耗。減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
- 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
- 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)。
- corePoolSize:核心池的大小
- maximumPoolSize:最大線程數
- keepAliveTime:線程沒有任務時最多保持多長時間后會終止
7.2 線程池的使用
Java里面線程池的頂級接口是java.util.concurrent.Executor ,但是嚴格意義上講Executor 并不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService 。
-
在java.util.concurrent.Executors 線程工廠類里面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors工程類來創建線程池對象。
- public static ExecutorService newFixedThreadPool(int nThreads) :返回線程池對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)
-
使用線程池對象
void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行Runnable
-
<T> Future<T> submit(Callable<T> task)
:執行任務,有返回值,一般用來執行Callable- Future接口:用來記錄線程任務執行完畢后產生的結果。線程池創建與使用。
void shutdown():關閉連接池
-
使用線程池中線程對象的步驟:
- 創建線程池對象。
- 創建Runnable接口子類對象。(task)
- 提交Runnable接口子類對象。(take task)
- 關閉線程池(一般不做)。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一個教練");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教練來了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教練回到了游泳池");
}
}
public class ThreadPoolDemo {
public static void main(String[] args) {
// 創建線程池對象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
// 創建Runnable實例對象
MyRunnable r = new MyRunnable();
//自己創建線程對象的方式
// Thread t = new Thread(r);
// t.start(); ‐‐‐> 調用MyRunnable中的run()
// 從線程池中獲取線程對象,然后調用MyRunnable中的run()
service.submit(r);
// 再獲取個線程對象,調用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法調用結束后,程序并不終止,是因為線程池控制了線程的關閉。
// 將使用完的線程又歸還到了線程池中
// 關閉線程池
//service.shutdown();
}
}