多執行緒
思維導圖看天下:

1. 概述
并行與并發
并行 :指兩個或多個事件在同一時刻發生(同時發生)
并發 :指兩個或多個事件在同一個時間段內發生,(交替執行)
執行緒與行程
行程:是指一個記憶體中運行的程式,每個行程都有一個獨立的記憶體空間,一個應用程式可以同時運行多個行程
記憶:行程的英文為Process,Process也為程序,所以行程可以大概理解為程式執行的程序,
(行程也是程式的一次執行程序,是系統運行程式的基本單位; 系統運行一個程式即是一個行程從創建、運行到消亡的程序)
執行緒:行程中的一個執行單元,負責當前行程中程式的執行,一個行程中至少有一個執行緒,一個行程中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式,【java默認有兩個執行緒:main、GC】
行程與執行緒的區別:
- 行程:有獨立的記憶體空間,行程中的資料存放空間(堆空間和堆疊空間)是獨立的,至少有一個執行緒,
- 執行緒:堆空間是共享的,堆疊空間是獨立的,執行緒消耗的資源比行程小的多
2. 執行緒創建的五種方式
推薦使用Runnable介面的方式,因為Java是單繼承的,所以使用Thread有OPP單繼承局限性

2.1 背景介紹
執行緒類
Java使用 java.lang.Thread 類代表執行緒,所有的執行緒物件都必須是Thread類或其子類的實體
每個執行緒的作用是完成一定的任務,實際上就是執行一段程式流即一段順序執行的代碼
Java使用執行緒執行體來代表這段程式流,
2.2 ① 繼承Thread類
2.2.1 執行緒實作
1)實作步驟
- 繼承Thread類的子類,并重寫該類的run()方法(該run()方法的方法體就代表了執行緒需要完成的任務,因此run()方法稱為執行緒執行體)
- 創建Thread子類的實體,即創建了執行緒物件
- 呼叫執行緒物件的start()方法來啟動該執行緒
2)實作案例
自定義執行緒類:

主函式:

public static void main(String[] args) {
MyThread myThread = new MyThread("MyThread");
myThread.start();
for (int i = 0;i<1000;i++){
System.out.println("main"+i);
}
}
執行結果:

3)執行程序分析
程序:程式啟動運行main時候,java虛擬機啟動一個行程,主執行緒main在main()呼叫時候被創建
隨著呼叫Mt類的物件的start方法,另外一個新的執行緒也啟動了 ,這樣,整個應用就在多執行緒下運行,
運行時序圖:

記憶體結構:

4)呼叫start和run方法的區別

2.2.2 構造方法
- public Thread()
分配一個新的執行緒物件, - public Thread(String name)
分配一個指定名字的新的執行緒物件 - public Thread(Runnable target)
分配一個帶有指定目標新的執行緒物件 - public Thread(Runnable target,String name)
分配一個帶有指定目標新的執行緒物件并指定名字
2.2.3 常用方法
- public String getName() :獲取當前執行緒名稱,
- public void start() :導致此執行緒開始執行; Java虛擬機呼叫此執行緒的run方法
- public void run() :此執行緒要執行的任務在此處定義代碼,
- public static void sleep(long millis) :使當前正在執行的執行緒以指定的毫秒數暫停(暫時停止執行),
- public static Thread currentThread() :回傳對當前正在執行的執行緒物件的參考,
1)獲取執行緒名稱
-
可以使用Thread類中的方法getName,
String getName() 回傳該執行緒的名稱, -
可以先獲取當前正在執行的執行緒,再呼叫getName方法獲取執行緒名稱
static Thread currentThread() 回傳對當前正在執行的執行緒物件的參考

//1.可以使用Thread類中的方法getName String name = getName(); System.out.println(name);//創建時, 指定了名稱,獲取的就是指定的名稱 //如果沒有指定名稱,獲取的就是Thread-0 //2.可以先獲取當前正在執行的執行緒 Thread currentThread = Thread.currentThread(); System.out.println(currentThread);//Thread[Thread-0,5,main] String name2 = currentThread.getName(); System.out.println(name2);//Thread-0
2)設定執行緒名稱
- 方法一:可以使用Thread類中的方法setName
void setName(String name) 改變執行緒名稱,使之與引數 name 相同,
MyThread myThread = new MyThread();
myThread.setName("myThreadName");
myThread.start();
- 方法二:添加一個帶參構造方法,引數傳遞執行緒的名稱;呼叫父類的帶參構造方法,把名字傳遞給父類,讓父親給兒子起名字
Thread(String name) 分配新的 Thread 物件,
public class MyThread extends Thread{
//定義指定執行緒名稱的構造方法
public MyThread(String name) {
super(name);
}

3)執行緒休眠
public static void sleep(long millis)
使當前正在執行的執行緒以指定的毫秒數暫停(暫時停止執行)睡醒了,繼續執行
/*程式在執行第二秒時, 會暫停2秒,2秒后,繼續執行后面程式*/
for (int i = 1; i <=60; i++) {
System.out.println(i);
/*讓程式睡眠1秒鐘 1秒=1000毫秒*/
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2.2.4 Thread構造方法底層原理——靜態代理
只針對有參且參是Runnable類的構造方法:public Thread(Runnable target)
由于Thread和target頂層都是Runnable介面,所以Thread是使用了靜態代理的方式代理引數target,
2.3 ② 實作Runnable介面
優勢:
- 避免單繼承的局限性
一個類繼承了Thread類就不能繼承其他的類
一個類實作了Runnable介面,還可以繼續繼承別的類,實作其他的介面 - 增強了程式的擴展性,降低程式的耦合度
使用Runnable介面把設定執行緒任務和開啟執行緒相分離
實作類當中,重寫run方法,設定執行緒任務
創建Thread類物件,呼叫 start方法,開啟新執行緒
如果一個類繼承Thread,則不適合資源共享,但是如果實作了Runable介面的話,則很容易的實作資源共享
2.3.1 實作步驟
1.創建一個RunnableImpl類實作Runnable介面
2.重寫Runnable介面中的run方法,設定執行緒任務
3.創建Runnable介面的實作類RunnableImpl的物件t
4.創建Thread類物件,構造方法中傳遞Runnable介面的實作類RunnableImpl的物件t
5.呼叫Thread類中的start方法,開啟新的執行緒,執行run方法
示例:
//實作Runnable介面
public class RunnableImpl implements Runnable{
//2.重寫Runnable介面中的run方法,設定執行緒任務
@Override
public void run() {
//新執行緒執行的代碼
for (int i = 0; i <20; i++) {
System.out.println(Thread.currentThread().getName()+"===>"+i);
}
}
}
public static void main(String[] args) {
//3.創建Runnable介面的實作類物件
RunnableImpl r = new RunnableImpl();
//4.創建Thread類物件,構造方法中傳遞Runnable介面的實作類物件
Thread t = new Thread(r);//列印20次i
//5.呼叫Thread類中的start方法,開啟新的執行緒,執行run方法
t.start(); //【一般16-18行簡寫為:new Thread(r,"執行緒名").start();】
//主執行緒開啟新執行緒之后繼續執行的代碼
for (int i = 0; i <20; i++) {
System.out.println(Thread.currentThread().getName()+"===>"+i);
}
}
2.3.2 構造方法
- Thread(Runnable target) 分配新的 Thread物件
- Thread(Runnable target, String name) 分配新的 Thread物件【推薦該方法,因為可以自定義執行緒名】
2.4 ③ 實作Callable介面
十分重要,但本篇只簡單介紹了一下,請去看下一篇JUC
2.4.1 實作步驟
- 實作Callable介面,需要回傳值型別
- 重寫call方法,需要拋出例外
- 創建目標物件
- 創建執行服務:ExecutorService ser = Executors.newFixedThreadPool(1); //1為開辟的執行緒池中執行緒的數量
- 提交執行Future
result1 = ser.submit(t1); //執行緒 - 獲取結果:boolean r1 = resut1.get() //指定執行緒的回傳結果
- 關閉服務:ser.shutdownNow()
代碼:
public class MyCallableImpl implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
PictureCatch t = new PictureCatch();
t.test(url,name);
System.out.println("下載了檔案名:"+name);
return true;
}
String url; //網址
String name; //保存的檔案名
MyCallableImpl(String url,String name){
this.url=url;
this.name=name;
}
public static void main(String[] args) {
MyCallableImpl t1 = new MyCallableImpl("https://img0.baidu.com/it/u=1151663768,725447312&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500","t4");
MyCallableImpl t2 = new MyCallableImpl("https://img0.baidu.com/it/u=1648512719,1593015989&fm=253&fmt=auto&app=120&f=JPEG?w=891&h=500","t5");
MyCallableImpl t3 = new MyCallableImpl("https://img2.baidu.com/it/u=863703859,746061395&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","t6");
ExecutorService ser = Executors.newFixedThreadPool(3);
Future<Boolean> result1 = ser.submit(t1);
Future<Boolean> result2 = ser.submit(t2);
Future<Boolean> result3 = ser.submit(t3);
try {
boolean r1 = result1.get();
boolean r2 = result2.get();
boolean r3 = result3.get();
System.out.println(r1);
System.out.println(r2);
System.out.println(r3);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
ser.shutdownNow();
}
class PictureCatch{
void test(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("獲取檔案出錯!");
}
}
}
}
2.5 ④ 執行緒池Executor
這里就不講了,放在了JUC并發編程那篇里詳細講解了
2.6 ⑤ Timer
使用 Timer 的方式如下:
public class MyTimer {
public static void main(String[] args) {
timer();
}
/**
* 指定時間 time 執行 schedule(TimerTask task, Date time)
*/
public static void timer() {
Timer timer = new Timer();
// 設定指定的時間time,此處為2000毫秒
timer.schedule(new TimerTask() {
public void run() {
System.out.println("執行定時任務");
}
}, 2000);
}
}
2.7 拓展:匿名內部類,實作Thread/Runnable多現程
2.7.1 匿名內部類
作用
把子類繼承父類,重寫父類的方法,創建子類物件,合成一步完成
把實作類實作介面,重寫介面庫的方法,創建實作類物件,合成一步完成
最終得要子類物件或實作類物件
格式
new 父類/介面(){
重寫父類/介面中的方法
};
2.7.2 Thread

public static void main(String[] args) {
new Thread(){ //new 沒有名稱的類 繼承Thread
//重寫run方法,設定執行緒任務
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"==>"+i);
}
}
}.start();
}
2.7.3 Runnable

new Thread(new Runnable() { //new沒有名稱的類實作了Runnable介面
//重寫run方法,設定執行緒任務
@Override
public void run() { //實作介面當中run方法
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}).start();
3. 執行緒使用
3.1 六種執行緒狀態
- NEW(新建)
執行緒剛被創建,但是并未啟動,還沒呼叫start方法 - Runnable(可運行)
執行緒可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決于操 作系統處理器 - Blocked(鎖阻塞)
當一個執行緒試圖獲取一個物件鎖,而該物件鎖被其他的執行緒持有,則該執行緒進入Blocked狀 態;當該執行緒持有鎖時,該執行緒將變成Runnable狀態, - Waiting(無限等待)
一個執行緒在等待另一個執行緒執行一個(喚醒)動作時,該執行緒進入Waiting狀態,
進入這個 狀態后是不能自動喚醒的,必須等待另一個執行緒呼叫notify或者notifyAll方法才能夠喚醒, - Timed Waiting(計時等待)
同waiting狀態,有幾個方法有超時引數,呼叫他們將進入Timed Waiting狀態,
這一狀態 將一直保持到超時期滿或者接收到喚醒通知,帶有超時引數的常用方法有Thread.sleep 、 Object.wait - Teminated(被終止)
因為run方法正常退出而死亡,或者因為沒有捕獲的例外終止了run方法而死亡,


3.2 執行緒的常用操作
執行緒方法
| 方法 | 說明 |
|---|---|
| setPriority(int newPriority) | 更改執行緒的優先級 |
| static void sleep(long millis) | 在指定的毫秒數內讓當前正在執行的執行緒休眠 |
| void join() | 等待該執行緒終止 |
| static void yield() | 暫停當前正在執行的執行緒物件,并執行其他執行緒 |
| void interrupt() | 中斷執行緒,別用這個方式 |
| boolean isAlive() | 測驗執行緒是否處于活動狀態 |
3.2.1 執行緒停止
- 不推薦使用JDK提供的stop()、destory()方法,【已廢棄】
- 推薦執行緒自己停止下來
- 建議使用一個標志位進行終止變數,當flag=false(flag做while的條件),則終止執行緒運行
以下舉例是使用一個標志位falg來終止變數
public class ThreadStopDemo implements Runnable {
private boolean flag = true;
@Override
public void run() {
int i = 1;
while (flag) {
System.out.println("run..."+(i++));
}
}
//設定一個專門修改標志位的方法來停止執行緒
public void stop(){
flag = false;
}
public static void main(String[] args) {
ThreadStopDemo demo = new ThreadStopDemo();
new Thread(demo).start();
for (int i = 1; i <= 500; i++) {
System.out.println("main..."+i);
if (i == 300) {
demo.stop();
System.out.println("執行緒該停止了");
}
}
}
}
3.2.2 執行緒休眠_sleep
- sleep(long millis) 指定當前執行緒阻塞的毫秒數
- sleep存在例外InterruptException,所以需要拋出例外
- sleep時間達到后執行緒進入就緒狀態
- sleep可以模擬網路延時,倒計時等
- 每一個物件都有一個鎖,sleep不會釋放鎖
模擬倒計時+列印當前系統時間
public static void main(String[] args) {
//模擬倒計時
System.out.println("開始倒計時");
int num = 10;
while (true) {
System.out.println(num--);
Thread.sleep(1000);
if (num <= 0) {
break;
}
}
System.out.println("開始報時");
//列印當前系統時間
int count = 10;
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
while (true) {
System.out.println(LocalDateTime.now().format(dateTimeFormatter));
Thread.sleep(1000);
count--;
if (count <= 0) {
break;
}
}
}
3.2.3 執行緒禮讓_yield
禮讓不一定成功,因為cpu重新調度,可能會再次選到之前的執行緒
- Thread.yield();禮讓執行緒,讓當前正在執行的執行緒暫停,但不阻塞
- 將執行緒從記憶體中的運行狀態轉為就緒狀態并拿出記憶體
- 讓cup重新調度選擇執行緒進入記憶體,禮讓不一定成功,看cup調度
代碼演示:結果可能有三種:aabb,abab,abba
public class ThreadYieldDemo implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"開始!");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"結束!");
}
public static void main(String[] args) {
ThreadYieldDemo demo = new ThreadYieldDemo();
new Thread(demo, "a").start();
new Thread(demo, "b").start();
}
}
3.2.4 執行緒強制執行_join
- Join合并執行緒,待此執行緒執行完成后,再執行其他執行緒,其他執行緒阻塞(可以想象成插隊)
代碼演示:結果是正常排隊執行到200后,得等強制執行走完200次后,才會繼續執行正常排隊201...
//插隊執行緒
public class 執行緒強制執行 {
public static void main(String[] args) {
forceThread forceThread = new forceThread();
Thread thread = new Thread(forceThread, "強制執行緒");
for (int i = 0; i < 500; i++) {
System.out.println("正常排隊:"+i);
if (i==200){
thread.start();
thread.join();
}
}
}
}
class forceThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("強制執行——"+i);
}
}
}
3.2.5 執行緒狀態查看_getState
六種執行緒狀態看前面寫的
代碼演示:
public static void main(String[] args) {
Thread thread = new Thread(()->{
for (int i = 0; i < 20; i++) {
Thread.sleep(500);
}
},"執行緒");
System.out.println("執行緒start前的狀態:"+thread.getState());
thread.start();
System.out.println("執行緒start后的狀態:"+thread.getState());
while (!Thread.State.TERMINATED.equals(thread.getState())){
System.out.println("執行緒terminated之前的狀態:"+thread.getState());
Thread.sleep(500);
}
System.out.println("執行緒的狀態:"+thread.getState());
}
結果:
執行緒start前的狀態:NEW
執行緒start后的狀態:RUNNABLE
執行緒terminated之前的狀態:RUNNABLE
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:RUNNABLE
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:RUNNABLE
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:TIMED_WAITING
執行緒terminated之前的狀態:RUNNABLE
執行緒terminated之前的狀態:RUNNABLE
執行緒的狀態:TERMINATED
3.2.6 執行緒優先級_Priority
原始碼中所有執行緒的優先級默認為5
優先級的設定建議在start()調度前
優先級低只是意味著獲取調度的概率低,并不是優先級低就不會被呼叫了,這都是看cup的調度
- java提供一個執行緒調度器來監控程式中啟動后進入就緒狀態的所有執行緒,執行緒調度器按照優先級決定應該調度哪個執行緒
- 執行緒的優先級用數字表示,范圍從1~10【越大優先級最高】
- Thread.MIN_PRIORITY =1
- Thread.MAX_PRIORITY =10
- Thread.NORM_PRIORITY =5
- 使用以下方式改變或獲取優先級
- getPriority() setPriority(int x)
代碼演示:
public class 執行緒優先級 {
public static void main(String[] args) {
//列印主執行緒的優先級(也是所有執行緒默認的優先級)
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority,"執行緒1");
Thread t2 = new Thread(myPriority,"執行緒2");
Thread t3 = new Thread(myPriority,"執行緒3");
Thread t4 = new Thread(myPriority,"執行緒4");
Thread t5 = new Thread(myPriority,"執行緒5");
Thread t6 = new Thread(myPriority,"執行緒6");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(7);
t3.setPriority(Thread.MAX_PRIORITY);
t4.setPriority(4);
t5.setPriority(9);
t6.setPriority(2);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
}
}
3.2.7 守護執行緒_daemon
-
執行緒分為用戶執行緒和守護執行緒(daemon)
-
虛擬機需要確保用戶執行緒執行完畢
-
虛擬機不用等待守護執行緒執行完畢(如,后臺記錄操作日志,監控記憶體,垃圾回收等)
也就是說可以做到主執行緒結束了,但守護執行緒還沒結束
代碼演示:
public class 守護執行緒 {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread godThread = new Thread(god, "守護執行緒");
Thread youThread = new Thread(you, "普通執行緒");
godThread.setDaemon(true); //設定為守護執行緒
godThread.start();
youThread.start();
}
}
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("我是上帝,是你的守護執行緒");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("我是普通人,只有三萬多天的日子");
}
System.out.println("======一個月完美的結束了======");
}
}
3.2.8 執行緒存盤ThreadLocal
用于存盤一個執行緒專有的值【物件方法】
ThreadLocal類,來創建作業記憶體中的變數,它將我們的變數值存盤在內部(只能存盤一個變數),不同的變數訪問到ThreadLocal物件時,都只能獲取到自己執行緒所屬的變數,【每個執行緒的作業記憶體空間不同,所以執行緒之間相互獨立,互不相關】
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> local = new ThreadLocal<>(); //注意這是一個泛型類,存盤型別為我們要存放的變數型別
Thread t1 = new Thread(() -> {
local.set("lbwnb"); //將變數的值給予ThreadLocal
System.out.println("執行緒1變數值已設定!");
try {
Thread.sleep(2000); //間隔2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒1讀取變數值:");
System.out.println(local.get()); //嘗試獲取ThreadLocal中存放的變數
});
Thread t2 = new Thread(() -> {
local.set("yyds"); //將變數的值給予ThreadLocal
System.out.println("執行緒2變數值已設定!");
});
t1.start();
Thread.sleep(1000); //間隔1秒
t2.start();
}
//結果:lbwnb,就算t2也設定了值,但不影響t1的值
拓展:子類執行緒也獲得不了父類執行緒設定的值,但可以通過用InheritableThreadLocal方法來解決這個問題,(在InheritableThreadLocal存放的內容,會自動向子執行緒傳遞)
public static void main(String[] args) {
ThreadLocal<String> local = new InheritableThreadLocal<>();
Thread t = new Thread(() -> {
local.set("lbwnb");
new Thread(() -> {
System.out.println(local.get());
}).start();
});
t.start();
}
3.2.9 等待與喚醒
- 等待wait和喚醒notify、notifyall都需要在同步代碼內(鎖方法 or 鎖代碼塊)
- 等待和喚醒只能由鎖物件呼叫,(鎖代碼塊的鎖物件容易看出,鎖方法的鎖物件一般是this或方法所在的類)
public void wait() : 讓當前執行緒進入到等待狀態 此方法必須鎖物件呼叫.
public void notify() : 喚醒當前鎖物件上等待狀態的執行緒 此方法必須鎖物件呼叫.會繼續執行wait()方法之后的代碼
| 方法名 | 作用 |
|---|---|
| wait() | 表示執行緒一直等待,直到其他執行緒通知,與sleep不同,會釋放鎖 |
| wait(long timeout) | 指定等待的毫秒數 |
| notify() | 喚醒一個處于等待狀態的執行緒 |
| notifyAll() | 喚醒同一個物件上所有呼叫wait()方法的執行緒,優先級別高的執行緒優先調度 |
注意:均是Object類的方法,都只能在同步方法或者同步代碼塊中使用,否則會拋出例外IllegalMonitorStateException
示例:
顧客與老板執行緒:
創建一個顧客執行緒(訊息者):告訴老板要吃什么 呼叫wait方法,放棄cpu的執行,進入wating狀態(無限等待)
創建一個老板執行緒(生產者):花5秒做好 做好后 呼叫notify方法 喚醒顧客 開吃
注意
- 顧客與老板執行緒必須使用同步代碼塊包裹起來,保證等待和喚醒只能有一個在執行同步使用的鎖必須要保證唯一,
- 只有鎖物件才能呼叫wait和notify方法
顧客執行緒

老板執行緒

Object obj = new Object();
new Thread(){
@Override
public void run() {
synchronized (obj){
System.out.println("告訴老板要吃餃子");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("做好===開始吃餃子");
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (obj){
try {
Thread.sleep(3000);
System.out.println("老板餃子已經做好");
obj.notify();//喚醒當前鎖物件上的等待執行緒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
3.2.10 小結
-
進入計時等待狀態的兩種方式
-
使用sleep(long m)方法,在毫秒值結束后,執行緒睡醒,進入Runnable/Blocked狀態(抱著鎖睡覺,不放鎖)
-
使用wait(long m)方法wait方法如果在毫秒值結束之后,還沒有被喚醒,就會自動醒來,進入Runnable/Blocked狀態(等待的時候會釋放鎖)
-
-
兩種喚醒的方法
-
public void notify()
隨機喚醒1個 -
public void notifyall()
喚醒鎖物件上所有等待的執行緒.
-
4. 執行緒安全
4.0 執行緒同步機制
多個執行緒操作同一個資源
并發:同一個物件被多個執行緒同時操作
處理多執行緒問題時,多個執行緒訪問同一個物件,并且某些執行緒還想修改這個物件,這時候我們就需要執行緒同步,執行緒同步其實就是一個等待機制,多個需要同時訪問此物件的執行緒進入這個物件的等待池形成佇列,等待前面的執行緒時候完畢,下一個執行緒再使用,
執行緒同步
- 由于同一行程的多個執行緒共享同一塊存盤空間,在帶來方便的同時,也帶來了訪問沖突問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排它鎖,獨占資源,其他執行緒必須等待,使用后釋放鎖即可,存在以下問題
- 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起
- 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的背景關系切換和調度延時,引起性能問題
- 如果一個優先級高的執行緒等待一個優先級低的執行緒釋放鎖會導致優先級倒置,引起性能問題
4.1 什么是執行緒安全
多執行緒訪問了共享的資料,就會產生執行緒的安全
舉例:
- 多個視窗,同時賣一種票. 如果不進行控制, 可以會出現賣重復的現象
- 多個視窗,同時在銀行同一賬戶取錢,銀行不進行控制就會虧錢
- ArrayList執行緒不安全
4.1.1 買票問題
解決措施:可鎖代碼塊可鎖方法,后面的解決方案是以買票問題為例
代碼演示:
//買票問題
public class UnsafeTicket implements Runnable{
private static Boolean falg = true;
private int ticket =10;//票數
public static void main(String[] args) {
UnsafeTicket demo1 = new UnsafeTicket();
new Thread(demo1,"小紅").start();
new Thread(demo1,"小明").start();
new Thread(demo1,"黃牛").start();
}
@Override
public void run() {
while (falg){
buy();
}
}
//買票
public void buy(){
//沒票了就停止執行緒
if (ticket<=0) {
falg = false;
return;
}
//還有票就繼續買
System.out.println(Thread.currentThread().getName()+"買到了第"+ticket+"張票");
ticket--;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
結果:會出現三個人同時去了第5張票、也可能會出現有人取0 -1張票
4.1.2 銀行取錢問題
解決措施:使用代碼塊鎖account
代碼演示:
//銀行取錢問題
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "結婚基金");
Bank drawMoney1 = new Bank(account, 50, "新一");
Bank drawMoney2 = new Bank(account, 100, "小蘭");
drawMoney2.start();
drawMoney1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卡內余額:" + account.money);
}
}
//賬戶
class Account{
int money;//賬戶內的錢
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//銀行
class Bank extends Thread{
Account account;//操縱的賬戶
int drawingMoney;//取了多少錢
public Bank(Account account, int drawingMoney,String who) {
super(who);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
drawing();
}
//取錢
private void drawing() {
if (this.account.money - drawingMoney < 0) {
System.out.println("余額不足," + Thread.currentThread().getName() + "取錢失敗");
return;
}
//sleep可以提高問題的發生的概率!
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.account.money = this.account.money - drawingMoney;
System.out.println(Thread.currentThread().getName() + "取了" + drawingMoney);
}
}
結果:

4.1.3 ArrayList問題
解決措施:鎖代碼塊
代碼演示:
public class UnsafeArrayList {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
int x = 10;
list.add(x++);
},"list執行緒不安全").start();
}
System.out.println(list.size());
}
}
結果:9997,少了三個,是因為前面插入資料的時候有三個下標被重復賦值,導致有三次賦值被覆寫了,
4.2 解決執行緒安全
鎖類模板 和 鎖用該類模板創建出來的物件 兩者之間互不影響!
4.2.1 synchronized鎖代碼塊
同步代碼塊synchronized的格式:
synchronized(鎖物件obj){
出現安全問題的代碼(訪問了共享資料的代碼)
}
注意
1.鎖物件可以是任意物件 new Person new Student ...(一般是鎖變化的物件,需要增刪改的物件)
2.必須保證多個執行緒使用的是同一個鎖物件
3.鎖物件的作用:把{}中代碼鎖住,只讓一個執行緒進去執行
1)鎖實體物件
適用于使用同一個Runnable物件創建多個執行緒的情況,不適用于多個Runnable物件分別創建多個執行緒的情況
作用范圍是物件實體,不可跨物件,所以多個執行緒不同物件實體訪問此方法,互不影響,無法產生互斥,由于本題搶票中是多個執行緒使用同一個Runnable物件,所以得到的鎖是同一個物件產生的obj,可以實作執行緒隔離,但銀行例子中是多個執行緒分別使用不同的Runnable物件,所以使用鎖實體物件是沒用的,
示例

public class TicketRunnableImpl implements Runnable {
//定義共享的票源
private int ticket = 100;
private Object obj = new Object(); //鎖物件
//執行緒任務:賣票
@Override
public void run() {
synchronized (obj){
while (ticket > 0) {
/*為了提高執行緒安全問題出現的幾率
讓執行緒睡眠10毫秒,放棄cpu的執行權*/
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
}
public static void main(String[] args) {
UnsafeTicket demo1 = new UnsafeTicket();
new Thread(demo1,"小紅").start();
new Thread(demo1,"小明").start();
new Thread(demo1,"黃牛").start();
}
}
總結:
同步監視器的執行程序(同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this,就是這個物件本身,或者是class)
- 第一個執行緒訪問,鎖定同步監視器,執行其中的代碼
- 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問,處于阻塞狀態,一直等待
- 第一個執行緒訪問完畢,解鎖同步監視器
- 第二個執行緒訪問,發現同步監視器沒有鎖,然后鎖定并訪問
2)鎖類
適用于使用同一個Runnable物件創建多個執行緒的情況,也適用于多個Runnable物件分別創建多個執行緒的情況
雖然是通過物件訪問的此方法,但是加鎖的代碼塊是類級別的跨物件的,所以鎖的范圍是針對類,多個執行緒訪問互斥,
public class SynchronizedDemo {
// 代碼塊鎖(類):鎖的應用物件是User類,可以稱之為類鎖
public void method2() {
synchronized (User.class) {
// TODO 業務邏輯
}
}
public static void main(String[] args) {
SynchronizedDemo obj1 = new SynchronizedDemo();
SynchronizedDemo obj2 = new SynchronizedDemo();
new Thread(() ->{
obj1.method2(); //代碼塊鎖,后面是類,多執行緒訪問互斥
}).start();
new Thread(() ->{
obj2.method2();
}).start();
}
}
4.2.2 synchronized鎖方法
鎖的是this,也就是主方法里呼叫該方法的物件
同步方法解決執行緒安全的格式:
修飾符 synchronized 回傳值型別 方法名(引數串列){
出現安全問題的代碼(訪問了共享資料的代碼)
}
使用步驟
1.創建一個方法,方法的修飾符添加上synchronized
2.把訪問了共享資料的代碼放入到方法中
3.呼叫同步方法
1)鎖普通方法(物件鎖)
適用于使用同一個Runnable物件創建多個執行緒的情況,不適用于多個Runnable物件分別創建多個執行緒的情況
普通方法作用范圍是物件實體,不可跨物件,所以多個執行緒不同物件實體訪問此方法,互不影響,無法產生互斥,由于本題搶票中是多個執行緒使用同一個Runnable物件,所以得到的鎖是同一個類物件this,可以實作執行緒隔離,但銀行例子中是多個執行緒分別使用不同的Runnable物件最后鎖的this也是不同類物件的this,所以使用鎖普通方法是沒用的,
示例

@Override
public void run() {
ticketMethods();
}
public synchronized void ticketMethods(){
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
鎖物件是誰???
鎖物件為this

public void ticketMethods(){
synchronized(this){
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
}
2)鎖靜態方法(類鎖)
適用于使用同一個Runnable物件創建多個執行緒的情況,也適用于多個Runnable物件分別創建多個執行緒的情況
靜態方法是通過類訪問,是類級別的跨物件的,所以鎖的范圍是針對類,多個執行緒訪問互斥,
示例:變化的量記得也要static

public class TicketRunnableImpl implements Runnable {
//定義共享的票源
private static int ticket = 100;
private Object obj = new Object(); //鎖物件
//執行緒任務:賣票
@Override
public void run() {
ticketMethods();
}
public static synchronized void ticketMethods(){
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
}
}
}
鎖物件是誰???
對于static方法,我們使用當前方法所在類的位元組碼物件(類名.class)

4.2.3 Lock鎖
概述
- 從jdk5.0開始,java提供了更強大的執行緒同步機制——通過顯示定義同步鎖物件來實作同步,同步鎖使用Lock物件充當
- java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具,鎖提供了對共享資源的獨占訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前先獲得Lock物件
- ,ReetrantLock(可重入鎖)類實作了Lock,它擁有與synchronized相同的并發性和記憶體語意,在實作執行緒安全的控制中,比較常見的是ReetrantLock,可以顯示加鎖、釋放鎖
Lock介面中的方法
void lock() 獲取鎖,
void unlock() 釋放鎖,//如果有try/catch的話,一般unlock是放在finally里
使用步驟
1.在成員位置創建一個Lock介面的實作類物件ReentrantLock
2.在可能會出現安全問題的代碼前,呼叫lock方法獲取鎖物件
3.在可能會出現安全問題的代碼后,呼叫unlock方法釋放鎖物件
示例

public class TicketRunnableImpl implements Runnable {
//定義共享的票源
private int ticket = 100;
//1.在成員位置創建一個Lock介面的實作類物件ReentrantLock
Lock l = new ReentrantLock();
//執行緒任務:賣票
@Override
public void run() {
while (true) {
l.lock();
if (ticket > 0){
try {
Thread.sleep(10);
//賣票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket + "張票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3.在可能會出現安全問題的代碼后,呼叫unlock方法釋放鎖物件
l.unlock(); //無論程式是否例外,都會把鎖物件釋放,節約記憶體提高程式的效率
}
}
}
}
}
4.2.4 synchronized與Lock對比
- Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖)synchronized是隱式鎖,除了作用域自動釋放
- Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費較少的時間來調度執行緒,性能更好,并且具有更好的擴展性(提供更多的子類)
- 優先使用順序:Lock > 同步代碼塊(已經進入了方法體,分配了回應資源)> 同步方法(在方法體之外)
4.2.5 判斷鎖的物件是誰
8鎖現象:
1)標準情況下,一個物件 兩個同步方法 第一個執行緒先拿到鎖 誰先執行
2)一個物件 兩個同步方法 第一個執行緒先拿到鎖 第一個方法延遲4S 誰先執行
3)一個物件 一個同步方法一個普通方法 第一個執行緒先拿到鎖 誰先執行
4)兩個物件 兩個同步方法 第一個執行緒先拿到鎖 第一個方法延遲4S 誰先執行
5)一個物件 兩個靜態同步方法 第一個執行緒先拿到鎖 第一個方法延遲4S 誰先執行
6)兩個物件 兩個靜態同步方法 第一個執行緒先拿到鎖 第一個方法延遲4S 誰先執行
7)一個物件 一個靜態同步方法一個普通同步方法 第一個執行緒先拿到鎖 第一個方法延遲4S 誰先執行
8)兩個個物件 一個靜態同步方法一個普通同步方法 第一個執行緒先拿到鎖 第一個方法延遲4S 誰先執行
package com.ambition;
import java.util.concurrent.TimeUnit;
/**
* 同一個物件 兩個執行緒 兩個同步方法 誰先執行?
**/
public class Question1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> phone.sendMsg()).start();
// 延遲的目的是控制哪個執行緒先拿到鎖
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> phone.call()).start();
}
}
class Phone {
// synchronized 鎖的物件是方法的呼叫者
// 兩個方法用的是同一個鎖 誰先拿到誰先執行
public synchronized void sendMsg() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("發短信");
}
public synchronized void call() {
System.out.println("打電話");
}
}
小結:
- 對于普通同步方法,鎖是當前new實體物件,
- 對于static 靜態同步方法,鎖是當前類模板的Class物件,
5. 生產者與消費者
5.1 問題介紹與分析
1.執行緒通信
- 應用場景:生產者和消費者問題
- 假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中的產品取走消費
- 如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產并等待,直到倉庫中的產品被消費者取走為止
- 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費并等待,直到倉庫中再次放入產品為止

2.執行緒通訊-分析
- 這個一個執行緒同步問題,生產者和消費者共享同一個資源,并且生產者和消費者之間相互依賴,互為條件,
- 對于生產者,沒有生產產品之前,要通知消費著等待,而生產了產品之后,有需要馬上通知消費者消費
- 對于消費者,在消費之后,要通知生產者已經結束消費,需要生產新的產品以供消費
- 在生產者消費者問題中,僅有synchronized是不夠的,就需要用到之前講的等待與喚醒,
- synchronized可以阻止并發更新同一個共享資源,實作了同步
- synchronized不能用來實作不同執行緒之前的訊息傳遞(通信)
5.2 解決方法
5.2.1 管程法
生產者——快取區——消費者
并發協作模型”生產者/消費者模式“-->管程法
- 生產者:負責生產資料的模塊(可能是方法,物件,執行緒,行程)
- 消費者:負責處理資料的模塊(可能是方法,物件,執行緒,行程)
- 緩沖區:消費者不能直接使用生產者的資料,他們之間有個“緩沖區”
生產者將生產好的資料放入緩沖區,消費者從緩沖區拿出資料
代碼:
//餐廳模式:生產者————廚師、消費者————顧客
public class 管程法 {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Cousumer(container).start();
}
}
/**
* 生產者
*/
class Productor extends Thread {
/**
* 緩沖區
*/
private SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
container.push(new Chicken(i));
// System.out.println("生產了" + i + "只雞");
}
}
}
/**
* 消費者
*/
class Cousumer extends Thread {
/**
* 緩沖區
*/
private SynContainer container;
public Cousumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
Chicken pop = container.pop();
// System.out.println("消費了" + pop.getId() + "只雞");
}
}
}
/**
* 雞(食物)
*/
class Chicken {
/**
* 雞的編號
*/
private int id;
public Chicken(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
/**
* 緩沖區
*/
class SynContainer {
/**
* 緩沖區的容器大小(十只雞)
*/
private Chicken[] chickens = new Chicken[10];
/**
* 計數器
*/
private int count = 0;
/**
* 生產者往容器中放入產品
*/
public synchronized void push(Chicken chicken) {
//如果容器滿了,生產者就需要等待消費者消費
if (count == 10) {
//生產者開始等待消費者消費
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果沒有滿,生產者往容器中繼續放入產品
chickens[count] = chicken;
count++;
System.out.println("生產了" + chicken.getId() + "只雞");
//生產者通知消費者消費
this.notifyAll();
//模擬生產者要休息一下下
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 消費者消費容器中的產品
*/
public synchronized Chicken pop() {
//消費者判斷容器中是否有產品
if (count == 0) {
//消費者等待生產者生產
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消費者開始消費容器中的產品
count--;
Chicken chicken = chickens[count];
System.out.println("消費了" + chicken.getId() + "只雞");
//消費者通知生產者繼續生產
this.notifyAll();
return chicken;
}
}
5.2.2 信號燈法(常用)
flag標志位來告訴消費者繼續/停止消費,告訴生產者繼續/停止生產
并發協作模型”生產者/消費者模式“-->信號燈法
public class 信號燈法 {
public static void main(String[] args) {
TV tv = new TV();
new Actor(tv).start();
new Audience(tv).start();
}
}
//演員
class Actor extends Thread{
TV tv = new TV();
public Actor(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
if (i % 2 == 0) {
tv.push("快樂大本營");
} else {
tv.push("抖音");
}
}
}
}
//聽眾
class Audience extends Thread{
TV tv = new TV();
public Audience(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
tv.pop();
}
}
}
//電視節目
class TV {
String name;//節目名稱
boolean flag=true; //標志位 T生產 F觀看
//生產節目
public synchronized void push(String name){
//判斷要不要生產
//不生產就等待
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生產
this.name=name;
flag=!flag;
System.out.println("我生產了"+name);
//生產完就喚醒觀眾
this.notifyAll();
}
//消費節目
public synchronized void pop(){
//判斷有沒有節目看
//沒有節目看就等待
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有節目就消費
flag=!flag;
System.out.println("我看完了"+name);
//看完就讓演員再演
this.notifyAll();
}
}
本文來自博客園,作者:不吃紫菜,遵循CC 4.0 BY-SA著作權協議,
轉載請附上原文出處鏈接:https://www.cnblogs.com/buchizicai/p/17277623.html及本宣告;
本文著作權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/548888.html
標籤:其他
上一篇:基于ZYNQ的OV5640攝像頭的sobel算子邊緣檢測
下一篇:三天吃透MySQL面試八股文
