Java多線程學習筆記
【注】這是早期的筆記,不系統,參考新的筆記
平時記錄
【搜索課程】極客時間 網絡編程實戰
※,IO多路復用、同步、異步、阻塞和非阻塞的區別 【清晰】
阻塞、非阻塞、多路IO復用,都是同步IO,異步必定是非阻塞的,所以不存在異步阻塞和異步非阻塞的說法。即使用select、poll、epool,都不是異步。IO多路復用只能表面上稱為異步阻塞IO,而非真正的異步IO。所以一般劃分為同步IO。
獲取IO數據,分為兩個階段,一是套接字緩沖區準備階段;二是數據拷貝階段(內核將數據從socket緩沖區拷貝到用戶空間)。阻塞IO和非阻塞IO,主要區別在于第一個階段。也即是阻塞IO,在套接字緩沖區沒準備好的情況下,會一直等待。而非阻塞IO,在套接字緩沖區沒準備好時,會立即返回。
※,同步vs異步,阻塞vs非阻塞 https://www.jianshu.com/p/73661ad3513d
(1)同步和異步
? 同步和異步描述的是一種消息通知的機制,主動等待消息返回還是被動接受消息。同步io指的是調用方通過主動等待獲取調用返回的結果來獲取消息通知,而異步io指的是被調用方通過某種方式(如,回調函數)來通知調用方獲取消息。
(2)阻塞非阻塞
? 阻塞和非阻塞描述的是調用方在獲取消息過程中的狀態,阻塞等待還是立刻返回。阻塞io指的是調用方在獲取消息的過程中會掛起阻塞,知道獲取到消息,而非阻塞io指的是調用方在獲取io的過程中會立刻返回而不進行掛起。
教程:參考此視頻
※,多線程:
卍,進程與線程:
- 進程:程序的基本執行實體。
- 線程: 操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。
多進程與多線程的區別:本質的區別在于每個進程都擁有自己的一整套變量,而線程則共享數據。共享數據會導致不安全,但好處就是線程間的通信更有效,更容易。
多線程如何提高工作效率:參考下圖....把本來可以摸魚的10分鐘利用起來

卍,并發與并行:
并發:在同一時刻,有多個指令在單個CPU上交替執行。
并行:在同一時刻,有多個指令在多個CPU上同時執行。
并發和并行可以同時存在,如下圖:

卍,多線程的實現方式:三種
- 繼承Thread類的實現方式
public class Test { public static void main(String[] args) { /** * 多線程的第一種啟動方式:繼承Thread類 * 1. 自定義一個類繼承Thread * 2. 重寫run方法 * 3. 創建子類對象,并啟動線程 */ MyThread myThread1 = new MyThread(); MyThread myThread2 = new MyThread(); myThread1.setName("線程1"); myThread2.setName("線程2"); // 兩個線程會隨機交替執行 myThread1.start(); myThread2.start(); } } class MyThread extends Thread { @Override public void run() { // 書寫線程要執行的代碼 String name = this.getName();//繼承的Thread的方法 for (int i = 0; i < 10; i++) { System.out.println(name + " HelloWorld"); } } } - 實現Runnable接口的方式進行實現
public class Test { public static void main(String[] args) { /** * 多線程的第二種啟動方式:實現Runnable接口 * 1. 自定義一個類實現Runnable接口 * 2. 重寫run方法 * 3. 創建自己的類的對象 * 4. 創建一個Thread類的對象,并開啟線程 */ // 創建MyRunTask對象,表示多線程要執行的任務 MyRunTask myRunTask = new MyRunTask(); // 創建線程對象 Thread t1 = new Thread(myRunTask); Thread t2 = new Thread(myRunTask); t1.setName("線程1"); t2.setName("線程2"); //開啟線程,兩個線程會隨機交替執行 t1.start(); t2.start(); } } class MyRunTask implements Runnable { //書寫線程要執行的代碼 @Override public void run() { //想要用Thread類中的方法首先要獲取到Thread對象 Thread thread = Thread.currentThread(); for (int i = 0; i < 10; i++) { System.out.println(thread.getName() + " HelloWorld"); } } } - 利用Callable接口和Future接口方式實現
public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { /** * 多線程的第三種啟動方式:利用Callable接口和Future接口 * 特點:可以獲取到多線程運行的結果 * * 1. 創建一個類MyCallable實現Callable接口 * 2. 重寫call方法(是有返回值的,表示多線程運行的結果) * * 3. 創建MyCallable的對象(表示多線程要執行的任務) * 4. 創建FutureTask的對象(作用:管理多線程運行的結果) * 5. 創建Thread類的對象,并啟動(表示線程) * * 6. 使用FutureTask對象獲取多線程運行結果 * */ // futureTasks用于存放每個線程的運算結果 List<FutureTask<Integer>> futureTasks = new ArrayList<>(); for (int i = 0; i < 5; i++) { /** * 創建MyCallable對象,表示多線程要執行的任務 * * 這里把Callable的初始化放在循環里,則每次都會創建一個新的Callable對象,多個線程操作的是各自的不同的Callable對象 * 如果放在循環外,則多個線程操作的是同一個Callable對象。 * 可以通過Callable的方法call()中打印this對象看到結果 */ MyCallable myCallable = new MyCallable(i + 10); /** * 創建FutureTask對象,用于管理多線程的運行結果 * 注意:這行代碼只有main線程會執行,并不涉及到多線程。又因為ArrayList是有序的,存的時候和取的時候元素順序是一致的 * 所以最終從futureTasks中獲取結果的時候,肯定是依次打印sum(10), sum(11),...,sum(14)的結果 */ FutureTask<Integer> futureTask = new FutureTask<>(myCallable); futureTasks.add(futureTask); // 創建線程的對象 Thread t = new Thread(futureTask); t.start(); System.out.println(Thread.currentThread().getName()); } // 獲取多線程運行的結果 for (FutureTask<Integer> futureTask : futureTasks) { Integer sum = futureTask.get(); System.out.println(sum);// 結果肯定是依次打印55 66 78 91 105 } } }
多線程三種實現方式對比:
| 實現方式 | 優點 | 缺點 |
| 繼承Thread類 | 編程比較簡單,可以直接使用Thread類中的方法 | 可擴展性較差,不能再繼承其他的類 |
| 實現Runnable接口 | 擴展性強,實現該接口的同時還可以繼承其他的類 | 編程相對復雜,不能直接使用Thread類中的方法 |
| 實現Callable接口(可以獲取多線程運行結果) |
卍,Thread類中的常見成員方法:
| 方法名稱 | 說明 |
| String getName() | 返回此線程的名稱 |
| void setName(String name) | 設置線程的名稱(構造方法也可以設置線程名稱) |
| static Thread currentThread() | 獲取當前線程的對象 |
| static void sleep(long millis) | 讓線程休眠指定的時間,單位為毫秒 |
| void setPriority(int newPriority) | 設置線程的優先級。 |
| final int getPriority() | 獲取線程的優先級 |
| final void setDaemon(boolean on) | 設置為守護線程 |
| public static void yield() | 出讓線程/禮讓線程 |
| public void join() | 插入線程/插隊線程 |
Thread.currentThread() //當Java虛擬機啟動之后,會自動的啟動多條線程。其中一條線程就叫做main線程。他的作用就是去調用main方法,并執行里面的代碼。
設置線程優先級:
- 計算機當中,線程的調度有兩種方式
- 搶占式調度:多個線程搶奪CPU的執行權。CPU執行哪個線程是不確定的,執行多長時間也是不確定的。提現一個隨機性。
- 非搶占式調度:所有線程輪流執行,執行時間也差不多。
- Java中采用的是搶占式調度。線程優先級從1-10,默認為5.優先級越高,搶占到CPU的概率是越高的。運行同樣任務的兩個線程,設置了優先級后也不能保證優先級高的一定比優先級低的先執行完畢,但是優先級高的線程先運行完的概率是高的。
守護線程:當其他的非守護線程執行完畢之后,守護線程會陸續結束(不會立即結束)。應用場景舉例:qq聊天過程中發送文件,聊天是一個線程,發送文件是一個線程。當聊天窗口關閉后,發送文件的線程也沒必要繼續了,這時可以把發送文件的線程設置為守護線程。
出讓線程:出讓當前CPU的執行權,但是出讓之后當前線程可能又搶到了CPU的執行權。所以這個方法也是概率性的,盡可能讓兩個線程均勻分布。
插入線程:thread.join() //表示把 thread這個線程插入到當前線程之前,也就是thread線程中的任務全部執行完畢后才會執行當前線程的任務。可以使用Thread.sleep()方法模擬測試。
卍,線程的生命周期:如下圖

解釋圖中的一些信息:
- 創建線程對象,運行start()方法之后,變為就緒狀態。就緒狀態含義是:可以搶奪CPU了,但是因為還沒搶到CPU,所以沒有執行代碼的權限。
- 問題答案:不會立即執行,因為sleep睡眠時間到了之后變為就緒狀態,需要去搶CPU,只有搶到CPU之后才會執行代碼。
卍,線程安全問題:參考此視頻。
多線程可以提高效率,但是提高效率的同時,也會帶來一個問題:不安全。因為線程之間是共享數據的,所以可能會導致共享數據和期望的不一樣。
CPU執行每一句代碼都需要時間,在此行代碼實際生效的過程中,CPU可能處于等待時間(比如等待IO,等待內存中創建對象等等),等待的過程中CPU的執行權就有可能被別的線程搶奪!總結就是:線程在執行代碼的時候,CPU的執行權隨時可能會被其他的線程搶走!線程執行時,具有隨機性。
示例:三個窗口共同銷售100張票,模擬窗口賣票程序。對于共享數據staticTicket的操作就出現了問題。
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//三個窗口共同銷售100張票,模擬窗口賣票程序
TicketThread ticketThread1 = new TicketThread("窗口1");
TicketThread ticketThread2 = new TicketThread("窗口2");
TicketThread ticketThread3 = new TicketThread("窗口3");
ticketThread1.start();
ticketThread2.start();
ticketThread3.start();
}
}
class TicketThread extends Thread {
private int ticket;
// 表示這個類的所有對象都共享staticTicket數據。
private static int staticTicket;
TicketThread() {
}
public TicketThread(String name) {
super(name);
}
@Override
public void run() {
/**
* Thread[窗口1,5,main]
* Thread[窗口3,5,main]
* Thread[窗口2,5,main]
* 這里的this指的就是各個ticketThread線程
*/
System.out.println(this);
// sellTicket();
sellStaticTicket();
}
/**
* 此種賣票方式會出現如下現象:兩種現象出現的原因是相同的
* 1. 相同的票出現多次:線程在執行代碼的時候,CPU的執行權隨時可能會被其他的線程搶走
* 2. 出現了超出范圍的票
*/
private void sellStaticTicket() {
for (; ; ) {
if (staticTicket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 第一個線程自增后還沒來的及執行下一行打印,CPU的執行權就被另一個線程給搶走了,第二個線程又將staticTicket自增了一次,
* 下次第一個線程再次搶到CPU執行權時就會打印自增2次后的值。所以就出現了重復票和超出范圍的票
*/
staticTicket++;
System.out.println(String.format("%s正在賣%s張票", getName(), staticTicket));
} else {
break;
}
}
}
/**
* 此種賣票方式:三個窗口各自賣100張,總共賣了300張票!
* 原因:因為這里沒有涉及到多個線程共享的數據,ticket是每個線程獨有的數據,所以每個線程各自運行,互不影響!
*/
private void sellTicket() {
for (; ; ) {
if (ticket < 100) {
ticket++;
System.out.println(String.format("%s正在賣%s張票", getName(), ticket));
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
線程安全問題的解決方法:同步代碼塊:把操作共享數據的代碼鎖起來。
原理:把操作共享數據的代碼加鎖,鎖默認是開的,當一個線程A進去之后鎖就關閉。線程A在鎖內部代碼塊中時,就算其他線程搶到了CPU的執行權也無法進入鎖住的代碼塊(因為鎖現在是關著的),只能在鎖外面等著。只有當線程A把鎖住的代碼塊執行完然后從鎖中出來了,鎖才會再次打開。這時候各個線程再次搶奪CPU的執行權,搶到的才能進入鎖內部,進入后鎖又鎖住了。依次循環。
格式:synchronized (鎖對象) {操作共享數據的代碼/同步代碼塊}
- 鎖默認打開,有一個線程A進去了,鎖自動關閉。線程A將里面的代碼全部執行完畢后從同步代碼塊中出來,鎖自動打開。
- 鎖對象很隨意,但是一定要是唯一的的!這樣不同的線程使用的才是同一把鎖,才能實現 同步代碼塊中的代碼是被各個線程輪流執行的 目的。如果鎖不是唯一的,每個線程都用各自的鎖,那么鎖也就失去了意義!
- 比如鎖可以是類中的一個靜態成員變量 static Object object = new Object(); synchronized(object){...}
- 經常使用的是當前類的字節碼對象,比如賣票例子中可以如下使用, synchronized (TicketThread.class){....}
使用同步代碼塊解決賣票問題的代碼:
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//三個窗口共同銷售100張票,模擬窗口賣票程序
TicketThread ticketThread1 = new TicketThread("窗口1");
TicketThread ticketThread2 = new TicketThread("窗口2");
TicketThread ticketThread3 = new TicketThread("窗口3");
ticketThread1.start();
ticketThread2.start();
ticketThread3.start();
}
}
class TicketThread extends Thread {
private int ticket;
private static int staticTicket;
TicketThread() {
}
public TicketThread(String name) {
super(name);
}
@Override
public void run() {
/**
* Thread[窗口1,5,main]
* Thread[窗口3,5,main]
* Thread[窗口2,5,main]
*/
System.out.println(this);
// sellTicket();
sellStaticTicket();
}
/**
* synchronized同步代碼塊的細節:
* 1. 同步代碼塊不能放到循環外面,否則就是一個線程(如線程A)進入同步代碼塊,鎖關閉,
* 然后賣完100張票出來(此時線程A的代碼全部執行完畢,此線程就會終止),鎖打開,
* 然后其他線程(如線程B)再進入同步代碼塊,發現staticTicket=100,直接break,然后線程B的代碼全部執行結束,線程終止。
* 然后線程C再進入,和B一樣終止。所有線程都終止,main線程也會終止,程序結束!
* 2. 鎖對象要是唯一的,通常是 類的字節碼文件對象,也就是類的字節碼文件在內存中的地址(也就是JVM內存中方法區中的一個地址)
*/
private void sellStaticTicket() {
// synchronized (TicketThread.class) { // 同步代碼塊不能放到循環外面
for (; ; ) {
synchronized (TicketThread.class) { // 同步代碼塊的正確位置
System.out.println(Thread.currentThread().getName());
if (staticTicket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 第一個線程自增后還沒來的及執行下一行打印,CPU的執行權就被另一個線程給搶走了,第二個線程又將staticTicket自增了一次,
* 下次第一個線程再次搶到CPU執行權時就會打印自增2次后的值。所以就出現了重復票和超出范圍的票
*/
staticTicket++;
System.out.println(String.format("%s正在賣%s張票", getName(), staticTicket));
} else {
break;
}
}
}
}
/**
* 此種賣票方式:三個窗口各自賣100張,總共賣了300張票!
* 因為不涉及多個線程的共享數據,所以這種方式加不加鎖結果都是一樣,當然加鎖會影響效率(更慢了)
*/
private void sellTicket() {
for (; ; ) {
synchronized (TicketThread.class) {
if (ticket < 100) {
ticket++;
System.out.println(String.format("%s正在賣%s張票", getName(), ticket));
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
同步方法:就是把synchronized關鍵字加到方法上。

上圖注釋:
同步方法的鎖對象有兩種情況:
- 此同步方法是非靜態的:鎖對象為this。問題來了:鎖對象要求是唯一的,為什么this可以作為鎖對象?
- 原因是多線程的第二種使用方式(實現Runnable接口)和第三種使用方式(實現Callable接口)中,多線程需要運行的代碼實際就是Runnable接口或Callable接口的實現類中書寫的,然后把這個實現類對象傳入不同的線程中(即new Thread(Runnable),其中Callable接口還需要通過Runnable的實現類FutureTask中轉一下再傳入Thread的構造方法中)。this指的是Runnable或Callable的實現類對象。如果這個Runnable和Callable接口的實現類只實例化了一次,那么this對于多個線程來講就是唯一的!也就是說只有使用第二種或第三種方式的多線程同步方法才能生效,使用第一種繼承Thread類的方式使用多線程同步方法是不生效的(因為這時this指的是Thread繼承類的實例化對象,有多個!)
- 此同步方法是靜態的:鎖對象為當前類的字節碼文件對象。
??非靜態同步方法的代碼示例:
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyRunTask myRunTask = new MyRunTask();
Thread t1 = new Thread(myRunTask);
Thread t2 = new Thread(myRunTask);
Thread t3 = new Thread(myRunTask);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
// 這里是先用同步代碼塊的方式書寫
class MyRunTask implements Runnable {
/**
* 注意:這里不再需要使用靜態變量了,因為MyRunTask只會被實例化一次
* 每個線程操作的都是這個ticket變量,所以這個ticket變量就是各個線程的共享數據。
*/
int ticket;
@Override
public void run() {
//書寫線程要執行的代碼
for (; ; ) {
synchronized (this) { //這里this只有一個,可以作為鎖對象
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(String.format("%s 正在賣第 %d 張票", Thread.currentThread().getName(), ticket));
} else {
break;
}
}
}
}
}
// 將上面的同步代碼塊變為同步方法
class MyRunTask implements Runnable {
/**
* 注意:這里不再需要使用靜態變量了,因為MyRunTask只會被實例化一次
* 每個線程操作的都是這個ticket變量,所以這個ticket變量就是各個線程的共享數據。
*/
int ticket;
@Override
public void run() {
//書寫線程要執行的代碼
for (; ; ) {
if (m()) break;
}
}
// 非靜態同步方法,鎖對象為this。
private synchronized boolean m() {
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(String.format("%s 正在賣第 %d 張票", Thread.currentThread().getName(), ticket));
} else {
return true;
}
return false;
}
}
經典面試題:String 和 StringBuilder和StringBuffer的區別
- String類對象是不可變字符串,因為源碼中是用字節數組保存字符串的:private final byte[] value。private和final決定了String類對象的字符串不可變。每次對
String變量進行修改的時候其實都等同于生成了一個新的String對象。 - StringBuilder和StringBuffer對象是可變的字符串。
- StringBuilder是線程不安全的,JDK1.5之后出現的,StringBuffer是線程安全的。
- 單線程下StringBuilder的效率理論上講應該也比StringBuffer要高,因為一個有synchronized修飾,一個沒有,用synchronized就要加鎖,獲取鎖和釋放鎖都需要時間。
- 單線程和多線程下StringBuffer的區別:考察的是synchronized的四種鎖的狀態。參考此文。
※,Lock鎖:synchronized的鎖是JVM虛擬機自動加和釋放的。如果想手動添加和釋放鎖就需要使用JDK5提供的Lock鎖。

public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new TicketThread("窗口1");
Thread t2 = new TicketThread("窗口2");
Thread t3 = new TicketThread("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class TicketThread extends Thread {
private static int staticTicket;
TicketThread() {
}
public TicketThread(String name) {
super(name);
}
/**
* 這里的鎖必須使用static修飾,不然每個線程都會有自己的鎖,就相當于沒加鎖。
*/
static Lock lock = new ReentrantLock(true);
@Override
public void run() {
for (; ; ) {
/**
* 搶奪到CPU執行權的線程A會拿到這把鎖,然后進入鎖內部執行代碼。在線程A執行鎖內部代碼的時候,CPU的執行權被另外的線程B搶到了
* 但是線程B依然要在鎖外面等著。等線程A執行完鎖內部的代碼就會從鎖里出來,同時將鎖釋放。然后各個線程再次搶奪CPU執行權,搶到的拿到鎖
* 不停循環。
*/
lock.lock();
try {
if (staticTicket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 第一個線程自增后還沒來的及執行下一行打印,CPU的執行權就被另一個線程給搶走了,第二個線程又將staticTicket自增了一次,
* 下次第一個線程再次搶到CPU執行權時就會打印自增2次后的值。所以就出現了重復票和超出范圍的票
*/
staticTicket++;
System.out.println(String.format("%s 正在賣 %s 張票", getName(), staticTicket));
} else {
break; //最后一次會執行break,在break之前還要去執行以下finally中的語句,這就保證了鎖一定會被釋放
}
} catch (Exception e) {
e.printStackTrace();
} finally {
/**
* 這里將釋放鎖的代碼寫在try catch finally里面是有原因的。
* 關鍵在于for循環中的break有可能直接跳過這行釋放鎖的代碼,導致有線程始終無法拿到鎖,導致程序無法停止。
* 為了保證釋放鎖的代碼無論任何時候都可以被執行,所以將其寫在finally中
*/
lock.unlock();
}
}
}
}
卍,死鎖
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new TicketThread("線程A");
Thread t2 = new TicketThread("線程B");
t1.start();
t2.start();
}
}
class TicketThread extends Thread {
private static int staticTicket;
TicketThread() {
}
public TicketThread(String name) {
super(name);
}
static final Object objectA = new Object();
static final Object objectB = new Object();
@Override
public void run() {
for (; ; ) {
if ("線程A".equals(Thread.currentThread().getName())) {
synchronized (objectA) {
System.out.println("線程A拿到A鎖,準備拿B鎖");
synchronized (objectB) {
System.out.println("線程A拿到B鎖,順利執行完一輪");
}
}
}
if ("線程B".equals(Thread.currentThread().getName())) {
synchronized (objectB) {
System.out.println("線程B拿到了B鎖,準備拿A鎖");
synchronized (objectA) {
System.out.println("線程B拿到了A鎖,順利執行完一輪");
}
}
}
}
}
}
卍,多線程中的生產者和消費者(又叫做等待喚醒機制)
生產者和消費者模式是十分經典的多線程協作機制。可以讓多線程的運行打破隨機運行的規則,讓多個線程輪流執行。
生產者和消費者涉及到了2個線程,分別稱為生產者(制造數據)和消費者(消費數據)。讓兩個線程打破隨機運行規則的核心思想是:利用第三方來控制線程的運行。

| 方法名稱 | 說明 |
| void wait() | 當前線程等待,直到被其他線程喚醒 |
| void notify() | 隨機喚醒單個線程 |
| void notifyAll() | 喚醒所有線程 |
生產者和消費者(等待喚醒機制)代碼實現方式一:
/**
* Desk的作用:控制生產者和消費者的執行
* TODO:可以優化的點:Desk中的變量都可以使用靜態的,這樣就不需要實例化Desk對象了
*/
public class Desk {
/**
* 0 代表桌子上無面條;1 代表桌子上有面條
* 定義為int類型方便擴展,可以控制2個以上的線程的運行
*/
int noodle;
// 總共10碗面條,消費完終止程序
int count = 10;
public Desk() {
}
public Desk(int noodle) {
this.noodle = noodle;
}
//
final Object lock = new Object();
}
public class Consumer extends Thread {
Desk desk;
public Consumer(Desk desk, String threadName) {
super(threadName);
this.desk = desk;
}
@Override
public void run() {
/**
* 多線程的代碼書寫套路
* 1. 循環
* 2. 同步代碼塊
* 3. 判斷共享數據是否到了末尾(到了末尾)
* 4. 判斷共享數據是否到了末尾(未到末尾,執行核心邏輯)
*/
for (; ; ) {
synchronized (desk.lock) {
// 共享數據即線程什么時候會停止,在這里是10碗面條:desk.count
if (desk.count == 0) {
break;
} else {
/**
* 先判斷桌子上是否有面條
* 如果沒有等待
* 如果有,開吃。
* 吃的總數-1
* 吃完之后喚醒生產者繼續生產
* 修改面條的狀態
*/
if (desk.noodle == 0) {
// 等待
// System.out.println(getName() + "在等待");
try {
/**
* 必須用鎖來調用wait()方法。
* 將當前線程與鎖進行綁定
*/
desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
desk.noodle = 0;
/**
* 喚醒這把鎖綁定的所有線程
* 位置隨意
*/
desk.lock.notifyAll();
desk.count--;
System.out.println(getName() + "消費了一碗面條,還能再吃" + desk.count + "碗");
}
}
}
}
}
}
public class Producer extends Thread {
Desk desk;
public Producer(Desk desk, String threadName) {
super(threadName);
this.desk = desk;
}
@Override
public void run() {
for (; ; ) {
synchronized (desk.lock) {
if (desk.count == 0) {
break;
} else {
/**
* 判斷桌子上是否有食物
* 如果有,等待
* 如果沒有,生產食物
* 修改桌子上的食物狀態
* 喚醒等待的消費者開吃
*/
if (desk.noodle == 1) {
//等待
// System.out.println(getName() + "線程在等待");
try {
/**
* 必須用鎖來調用wait()方法。
* 將當前線程與鎖進行綁定
*/
desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(getName() + "線程生產了一碗面條");
desk.noodle = 1;
/**
* 喚醒這把鎖綁定的所有線程
*/
desk.lock.notifyAll();
}
}
}
}
}
}
public class DemoTest {
public static void main(String[] args) throws InterruptedException {
Desk desk = new Desk(1);
new Producer(desk, "生產者").start();
new Consumer(desk, "消費者").start();
}
}
生產者和消費者(等待喚醒機制)代碼實現方式二:阻塞隊列的方式,不需要利用第三方,也不需要自己加鎖釋放鎖,也不需要手動喚醒或讓線程等待。阻塞隊列內部邏輯都有已經實現!

阻塞隊列的繼承結構:
實現了四個接口:Iterable, Collection,Queue,BlockingQueue
兩個實現類:
- ArrayBlockingQueue: 底層是數組,有界。必須指定數組長度
- LinkedBlockingQueue:底層是鏈表,但不是真正的無界,最大為int的最大值。
使用阻塞隊列成生產者和消費者(等待喚醒機制) 代碼:
public class Consumer extends Thread {
ArrayBlockingQueue<Integer> arrayBlockingQueue;
public Consumer(String name, ArrayBlockingQueue<Integer> arrayBlockingQueue) {
super(name);
this.arrayBlockingQueue = arrayBlockingQueue;
}
@Override
public void run() {
/**
* 多線程代碼套路:
* 1. 循環
* 2. 同步代碼塊
* 3. 判斷共享數據是否到了末尾(到了)
* 4. 判斷共享數據是否到了末尾(未到,執行核心邏輯)
*
* 這里因為使用了阻塞隊列,已經有鎖了,所以不需要自己加鎖了
*/
for (int i = 0; i < 10; i++) {
try {
// 不斷的從阻塞隊列中消費數據
Integer take = arrayBlockingQueue.take();
System.out.println(Thread.currentThread().getName() + "消費了一條數據: " + take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Producer extends Thread {
ArrayBlockingQueue<Integer> arrayBlockingQueue;
public Producer(String name, ArrayBlockingQueue<Integer> arrayBlockingQueue) {
super(name);
this.arrayBlockingQueue = arrayBlockingQueue;
}
@Override
public void run() {
/**
* 多線程代碼套路:
* 1. 循環
* 2. 同步代碼塊
* 3. 判斷共享數據是否到了末尾(到了)
* 4. 判斷共享數據是否到了末尾(未到,執行核心邏輯)
*
* 這里因為使用了阻塞隊列,已經有鎖了,所以不需要自己加鎖了
*/
for (int i = 0; i < 12; i++) {
try {
// 不斷的把數據放到阻塞隊列中
arrayBlockingQueue.put(1);
System.out.println(Thread.currentThread().getName() + "生產了一條數據");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class DemoTest {
public static void main(String[] args) {
/**
* 需求:使用阻塞隊列成生產者和消費者(等待喚醒機制) 代碼
* 細節:
* 生產者和消費者必須使用同一個阻塞隊列!
*/
ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
Producer producer = new Producer("生產者", arrayBlockingQueue);
Consumer consumer = new Consumer("消費者", arrayBlockingQueue);
producer.start();
consumer.start();
}
}
卍,線程的狀態:

Thread.State內部枚舉定義了線程的六種狀態。


圖解:Java的虛擬機當中沒有定義線程的運行狀態,此處是為了便于理解添加的。之所以不定義運行狀態的原因如下:
- 當線程搶到多CPU的執行權時,JVM虛擬機就會把當前的線程交給操作系統去管理,虛擬機自己就不管了。既然不管了就沒必要定義運行狀態了。
卍,多線程練習
/**
* 同時開啟2個線程,共同獲取1-100之間的數字。
* 要求:輸出所有的奇數。
*/
public class PrintOdd {
public static void main(String[] args) {
//注意這里使用了匿名內部類,不能用lambda表達式代替!lambda表達式只能用于函數式接口的**方法**!
Runnable runnable = new Runnable() {
int i = 1;
@Override
public void run() {
/**
* 多線程代碼套路:
* 1. 循環
* 2. 同步代碼塊
* 3. 判斷共享數據是否到了末尾(到了)
* 4. 判斷共享數據是否到了末尾(未到,執行核心邏輯)
*
*/
for (; ; ) {
synchronized (this) {
if (i > 100) {
break;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
i++;
}
}
}
}
};
Thread t1 = new Thread(runnable, "線程1");
Thread t2 = new Thread(runnable, "線程2");
t1.start();
t2.start();
}
}
※,線程池
卍,線程池的主要核心原理:
- 1. 創建一個池子
- 2. 提交任務時,池子會創建新的線程對象,任務執行完畢,線程歸還給池子下回再次提交任務時,不需要創建新的線程,直接復用已有的線程即可
- 3. 但是如果提交任務時,池子中沒有空閑線程,也無法創建新的線程,任務就會排隊等待
卍,線程池的代碼實現
JDK1.5提供了java.util.concurrent.Executors 工具類,通過調用方法返回不同類型的線程池對象。
| 方法名稱 | 說明 |
| public static ExecutorService newCachedThreadPool() | 創建一個沒有上限的線程池(上限是int的最大值) |
| public static ExecutorService newFixedThreadPool(int nThreads) | 創建有上限的線程池 |
/**
* 往線程池中提交任務,主要有兩種方法,execute()和submit()。
* execute()用于提交不需要返回結果的任務。
* submit()用于提交一個需要返回果的任務。該方法返回一個Future對象,通過調用這個對象的get()方法,我們就能獲得返回結果。
* get()方法會一直阻塞,直到返回結果返回。另外,我們也可以使用它的重載方法get(long timeout, TimeUnit unit),這個方法也會阻塞,但是在超時時間內仍然沒有返回結果時,將拋出異常TimeoutException。
*
*/
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
System.out.println("threadName: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
/**
* 在線程池使用完成之后,我們需要對線程池中的資源進行釋放操作,這就涉及到關閉功能。我們可以調用線程池對象的shutdown()和shutdownNow()方法來關閉線程池。
* 這兩個方法都是關閉操作,又有什么不同呢?
*
* shutdown()會將線程池狀態置為SHUTDOWN,不再接受新的任務,同時會等待線程池中已有的任務執行完成再結束。
* shutdownNow()會將線程池狀態置為SHUTDOWN,對所有線程執行interrupt()操作,清空隊列,并將隊列中的任務返回回來。
* 另外,關閉線程池涉及到兩個返回boolean的方法,isShutdown()和isTerminated,分別表示是否關閉和是否終止。
*/
pool.shutdownNow();
}
卍,自定義線程池: 也就是Executors工具類封裝方法的底層實現


卍,線程池設置為多大比較合適

圖解:
CPU密集型運算: 線程池大小 = 最大并行數 + 1 // 4核8線程的CPU最大并行數就是8。當然實際以下面Java代碼得到的為準。
// 獲取JVM可以利用的CPU線程數。有的操作系統不會把所有線程都給同一個軟件使用
int processors = Runtime.getRuntime().availableProcessors();
※,多線程中的其他擴展知識:參考百度網盤存儲的黑馬視頻文件夾
- volatile
-
volatile與synchronized的區別:
-
volatile只能修飾實例變量和類變量,而synchronized可以修飾方法,以及代碼塊。
-
volatile保證數據的可見性,但是不保證原子性(多線程進行寫操作,不保證線程安全);而synchronized是一種排他(互斥)的機制(因此有時我們也將synchronized這種鎖稱之為排他(互斥)鎖),synchronized修飾的代碼塊,被修飾的代碼塊稱之為同步代碼塊,無法被中斷可以保證原子性,也可以間接的保證可見性。
-
-
- 原子性
- 原子類
-
CAS和Synchronized都可以保證多線程環境下共享數據的安全性。那么他們兩者有什么區別?
Synchronized是從悲觀的角度出發:總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)。因此Synchronized我們也將其稱之為悲觀鎖。jdk中的ReentrantLock也是一種悲觀鎖。
CAS是從樂觀的角度出發: 總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據。CAS這種機制我們也可以將其稱之為樂觀鎖。
- 并發工具類
- ConcurrentHashMap
- CountDownLatch
- CyclicBarrier
- Semaphore
- Exchanger
教程:Java多線程編程實戰指南
卍,第11章:
11.1
※,Linux內核工具perf可以查看程序運行過程中的緩存未命中情況?!udo perf stat -e cache-references,cache-misses java -jar test.jar·
※,·lscpu`查看處理器的高速緩存層次。
11.2
11.3
卍,第1章:
1.7. 線程的監視

卍,第二章
※,共享變量的含義:共享變量一定是多個線程都會訪問到的變量。例如:多個線程運行一個類的同一個實例時,這個實例的屬性就是共享變量。多個線程運行同一個類的不同實例時,這個類的屬性不是共享變量。
※,原子性保證 要么執行完畢,要么未執行??梢娦员WC可以獲取執行完畢的值而不是未執行的值。
※,
卍,第三章
※,線程安全問題從根源上講是硬件(如寫緩沖器)和軟件(編譯器)問題。但是從應用程序的角度來看,線程安全問題的產生是由于多線程應用程序缺乏某種東西:線程同步機制。線程同步機制是一套用于協調線程間的數據訪問(Data access)及活動(Activity)的機制,該機制用于保障線程安全以及實現這些線程的共同目標。第三章講解協調線程間共享數據訪問的相關關鍵字和api,第五章講解協調線程間活動的相關api。從廣義上講,Java平臺提供的線程同步機制包括:
- 鎖
- volatile關鍵字
- final關鍵字
- static關鍵字
- 一些相關的api,如Object.wait(),Object.notify()等。
※,讀鎖和寫鎖
- 任何線程讀取變量的時候,其他線程都無法更新這些變量。一個線程更新共享變量的時候,其他任何線程都無法訪問該變量。
| 獲得條件 | 排他性 | 作用 | |
| 讀鎖 | 相應的寫鎖未被任何線程持有 |
對讀線程是共享的, 對寫線程是排他的 |
允許多個多線程可以同時讀取共享變量, 并保障讀線程讀取共享變量期間沒有其他任何線程能夠更新這些共享變量 |
| 寫鎖 |
該寫鎖未被其他任何線程持有, 并且相應的讀鎖未被任何線程持有 |
對讀線程和寫線程都是排他的 | 使得寫線程能夠以獨占的方式訪問共享變量。 |
- 讀寫鎖內部實現比內部鎖和其他顯示鎖要復雜的多,因此讀寫鎖只有在以下兩個條件同時滿足時才適用,否則使用讀寫鎖會得不償失(開銷)。
- 1. 只讀操作比寫(更新)操作要頻繁的多。
- 2. 讀線程持有鎖的時間比較長。
※,
※,
※,
※,
※,
※,
※,
※,
浙公網安備 33010602011771號