目錄
1.預備知識
1.1 馮諾依曼體系結構:
1.2 現代CPU主要關心指標(和日常開發密切相關的)
1.3 計算機中,一個漢字占幾個字節?
1.4?Windows和Linux的區別
1.5 PCB的一些關鍵要點
2.線程和進程
2.1 創建線程的寫法
2.2 thread的幾個常見屬性
2.3 休眠當前進程
3.線程的狀態
4.多線程帶來的風險-線程安全
4.1 線程安全問題產生原因
4.2 synchronized關鍵字
4.3?解決死鎖的方法
4.4 Java標準庫中的線程安全類
5.volatile關鍵字
5.1 volatile能保證內存可見性
5.2 volatile不保證原子性
6. wait和notify
1.預備知識
1.1 馮諾依曼體系結構:
CPU,存儲器(內存、外存/硬盤),輸入/輸出設備
內存和硬盤區別:
- 內存訪問速度快,硬盤速度慢
- 內存空間小,硬盤空間大
- 內存成本高,硬盤成本低
- 內存數據掉電后丟失,硬盤數據掉電后持續存儲
有的設備既是輸入設備又是輸出設備,比如觸摸屏,網卡(上網時和網線連接部分的硬件設備,集成在主板上)...
1.2 現代CPU主要關心指標(和日常開發密切相關的)
- CPU的頻率
基頻/默頻
睿頻/加速頻率
- CPU的核心數
大小核
CPU的基本工作流程:讀取指令、解析指令、執行指令
1.3 計算機中,一個漢字占幾個字節?
取決于字符集(漢字怎樣編碼)
- gbk(中國大陸上曾經廣泛使用的)。Windows10/11簡體中文版默認gbk.使用VS寫代碼打印漢字strlen,結果是2;
- utf8(當下全世界最流行的編碼方式)
- 一個漢字占3字節。utf8本身是變長編碼(1-4);
- unicode(Java的char就是使用unicode)一個漢字2字節
- Java的String就不一定
1.4?Windows和Linux的區別
最直接的區別:兩個系統提供的API(系統函數)不同
比如:Windows的Sleep(ms) =>#include <Windows.h>
Linux的sleep(s)? usllep(us) =>#include <unistd.h>
Windows一般是使用圖形化界面操作;Linux一般是命令行操作(命令)
不同的操作系統之間不兼容,java卻有“跨平臺”特性,不需要任何修改,就可以在不同的系統上完成同樣的功能,原因在于Java虛擬機,不同的主流系統都有各自的Java 虛擬機,Windows有Windows JVM,Linux有Linux JVM,這些JVM是不同的程序,但是上層支持的Java字節碼是一致的。
進程是操作系統中,資源分配的基本單位。
1.5 PCB的一些關鍵要點
- pid(進程id)進程的身份標識符
- 內存指針(一組指針)
- 進程需要知道要執行的指令的地址
- 指令依賴的數據在哪里
- 文件描述符表
- 進程狀態
- 進程優先級
- 進程上下文
- 進程的記賬信息(統計每個進程在CPU上運行了多久,如果某個進程很久沒有得到CPU資源,就給此進程多一些資源)
在一個CPU核心上,按照分時復用,執行多個進程這樣的方式,稱為“并發執行”;(人看起來是同時執行,微觀上,其實是一個CPU在串行執行,切換速度極快)
在多個CPU核心上,同時執行多個進程這樣的方式,稱為“并行執行”。(實際上就是“同時執行”)
現代CPU在運行這些進程的時候,并發和并行是同時存在的。
因為需要并發執行,所以操作系統需要進行進程的快速切換,即“進程調度”。
線程是CPU上調度執行的基本單位。
2.線程和進程
線程(Thread):
- 概念:線程是進程內部的執行單元,是操作系統能夠進行運算調度的最小單位。
- 特點:
- 輕量級:線程的創建、切換和銷毀相對較快。
- 共享資源:線程可以共享進程的資源。
- 并發執行:多個線程可以同時執行,提高程序的并發性能。
進程(Process):
- 概念:進程是程序的一次執行過程,是系統進行資源分配和調度的基本單位。
- 特點:
- 獨立性:每個進程有獨立的地址空間、狀態和資源。
- 資源分配:進程擁有自己的資源,如內存、CPU 等。
- 隔離性:進程之間相互隔離,互不干擾。
進程是操作系統資源分配的基本單位;線程是操作系統調度執行的基本單位。
區別:
- 獨立性:進程是獨立的執行實體,每個進程有自己的地址空間、資源和狀態;而線程是進程中的一部分,共享進程的地址空間和資源。
- 資源分配:進程分配資源(如內存);線程共享進程的資源。
- 調度:進程調度涉及到進程的切換;線程調度更細粒度,切換速度快。
- 進程的創建和銷毀開銷較大,而線程更靈活、高效。
- 進程是包含線程的. 每個進程?少有?個線程存在,即主線程。
2.1 創建線程的寫法
- 1. 繼承Thread,重寫run
package thread;class MyThread extends Thread{@Overridepublic void run(){// run相當于線程的入口(新線程啟動就自動執行)while(true){System.out.println("<UNK>");}}
}
public class Demo1 {public static void main(String[] args) {Thread t=new MyThread();while(true){t.start();//真正在系統中創建出一個線程(JVM調用操作系統的API完成線程創建操作)}}
}
- 2. 實現Runnable,重寫run(能夠更好的解耦合)
package thread;class MyRunnable implements Runnable{@Overridepublic void run() {while(true){System.out.println("<UNK1>");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
public class Demo2 {public static void main(String[] args) throws InterruptedException {Runnable runnable = new MyRunnable();Thread t = new Thread(runnable);t.start();while(true){System.out.println("<UNK2>");Thread.sleep(1000);}}
}
- 繼承 Thread 類, 直接使? this 就表?當前線程對象的引?.
- 實現 Runnable 接?, this 表?的是 MyRunnable 的引?. 需要使? Thread.currentThread()
- 3. 匿名內部類創建 Thread ?類對象
package thread;public class Demo3 {public static void main(String[] args) {Thread t=new Thread(){// 匿名內部類// 1.創建一個Thread子類,是匿名的// 2.{}里面編寫子類的定義代碼,子類里面的屬性、方法、重寫父類的方法// 3.創建了這個匿名內部類的實例,并把實例的引用賦值給t@Overridepublic void run(){while(true){System.out.println("hello thread");try{Thread.sleep(2000);}catch (InterruptedException e){throw new RuntimeException(e);}}}};t.start();while(true){System.out.println("hello main");try{Thread.sleep(2000);}catch (InterruptedException e){throw new RuntimeException(e);}}}
}
- 4. 匿名內部類創建 Runnable ?類對象
package thread;public class Demo4 {public static void main(String[] args) {Runnable runnable = new Runnable() {@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread thread = new Thread(runnable);thread.start();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
- 5. lambda 表達式創建 Runnable ?類對象(本質上是“匿名函數”,主要用途是作為“回調函數”)
package thread;public class Demo5 {public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();while (true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
2.2 thread的幾個常見屬性
- ID是線程的唯一標識,不同線程不會重復
- 名稱是各種調試工具用到
- 狀態表示線程當前所處的一個情況
- JVM會在一個進程的所有前臺線程結束后,才會結束運行
- 是否存活,即run方法是否運行結束了
守護進程(Deamon):
前臺線程的存在能夠影響到進程繼續存在;
后臺線程的存在不影響進程結束,這些是JVM自帶的線程,即使他們繼續存在,如果進程要結束了,他們也隨之結束。
package thread;public class Demo7 {public static void main(String[] args) {Thread t = new Thread(()->{while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//線程默認是前臺線程t.setDaemon(true);//在start之前進行,將此線程設為后臺線程,無力阻止進程結束t.start();for (int i = 0; i < 3; i++) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
package thread;public class Demo8 {public static void main(String[] args) {Thread t = new Thread(()->{for(int i=0;i<3;i++){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//這個結果一定是false//此時還沒有調用start,沒有真正創建線程System.out.println(t.isAlive());t.start();while(true){System.out.println(t.isAlive());try {Thread.sleep(1000);} catch (InterruptedException e) {}}}
}
每個Thread對象,都只能start一次
每次想創建一個新的線程,都得創建一個新的Thread對象(不能重復利用)
package thread;public class Demo9 {public static void main(String[] args) {Thread t=new Thread(()->{System.out.println("hello thread");});t.start();t.start();//拋出異常}
}
Thread對象和內核中的線程一一對應,可能出現內核中的線程已經結束銷毀了,但是Thread對象還在。?
package thread;public class Demo10 {public static void main(String[] args) throws InterruptedException {boolean isFinished=false;Thread t =new Thread(()->{while (!isFinished) {//報錯//lambda里面,希望使用外面的變量,觸發“變量捕獲”這樣的語法。// lambda是回調函數,操作系統真正創建出線程之后才會執行。//很有可能,后續線程創建好了之后,當前main里的方法都執行完了,對應的isFinished就銷毀了//為了解決問題,Java把被捕獲的變量拷貝一份,拷貝給lambda//外面的變量是否銷毀,就不影響lambda里面的執行了System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread is finished");});t.start();Thread.sleep(3000);isFinished = true;}
}
package thread;public class Demo11 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{//這是在lambda中(即在t線程的入口方法中)調用的//返回結果是t
// System.out.println("t:"+Thread.currentThread().getName());while(!Thread.currentThread().isInterrupted()){//靜態方法,哪個線程調用,獲取到的就是哪個線程的Thread引用System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {
// break;//針對上述代碼,//正常來說,調用Interrupt方法就會修改isInterrupted方法內部的標志位,設為true//由于上述代碼把sleep喚醒了,//這種提前喚醒的情況下,sleep就會在喚醒之后把isInterrupted標志位設置為false//因此在這樣的情況下,如果繼續執行到循環條件判定,就會發現能繼續執行
// throw new RuntimeException(e);}}System.out.println("thread exit");});t.start();Thread.sleep(3000);System.out.println("main線程嘗試終止t線程");t.interrupt();//這個代碼是在main中調用的,返回結果是main
// System.out.println("main:"+Thread.currentThread().getName());}
}
- 如果線程因為調? wait/join/sleep 等?法?阻塞掛起,則以 InterruptedException 異常的形式通 知,清除中斷標志 。當出現 InterruptedException 的時候, 要不要結束線程取決于 catch 中代碼的寫法. 可以選擇忽略這個異常, 也可以跳出循環結束線程.
- 否則,只是內部的?個中斷標志被設置,thread 可以通過Thread.currentThread().isInterrupted() 判斷指定線程的中斷標志被設置,不清除中斷標志這種?式通知收到的更及時,即使線程正在 sleep 也可以?上收到。
package thread;public class Demo12 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{for (int i = 0; i < 3; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t線程結束");});t.start();Thread.sleep(2000);//雖然可以通過sleep休眠的時間控制線程結束的順序,但是這樣的設定并不科學//通過設置時間的方式不一定靠譜System.out.println("main線程結束");}
}
在main線程中調用t.join,主線程等待t先結束,main線程就會“阻塞等待”
join提供的參數指定“超時時間”,即等待的最大時間
2.3 休眠當前進程
線程調度不可控,因此只能保證實際休眠時間大于等于參數設置的休眠時間。
代碼調用sleep,相當于當前進程讓出CPU資源,后續時間到了,需要操作系統內核,再把這個線程調到CPU上,才能繼續執行。(不是立即執行)
sleep(0)是使用sleep的特殊寫法,意味著當前線程立即放棄CPU資源,等待操作系統重新調度。
3.線程的狀態
- NEW:安排了工作,還未開始行動。new了Thread對象,還沒start.
package thread;public class Demo13 {public static void main(String[] args) {Thread t=new Thread(()->{System.out.println("hello thread");});System.out.println(t.getState());//NEWt.start();}
}
- TERMINATED:工作完成了。內核中的線程已經結束了,但是Thread對象還在.
package thread;public class Demo13 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{System.out.println("hello thread");});System.out.println(t.getState());//NEWt.start();Thread.sleep(1000);System.out.println(t.getState());//TERMINATED}
}
- RUNNABLE:可工作的。又可分為正在工作和即將開始工作.
就緒:線程正在CPU上執行;線程隨時可以去CPU上執行。
- TIMED_WAITING:指定時間的阻塞(阻塞的時間有上限,sleep時間到了就回到RUNNABLE)
package thread;public class Demo13 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{while(true){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});System.out.println(t.getState());//NEWt.start();Thread.sleep(1000);System.out.println(t.getState());//TIMED_WAITING}
}
package thread;public class Demo14 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();t.join(6000*1000);//main線程所處的狀態為TIMED_WAITING}
}
- WAITING:死等,沒有超時時間的阻塞等待。(線程執行完才能回到RUNNABLE)
package thread;public class Demo14 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();t.join();//main線程的狀態為WAITING}
}
- BLOCKED:是一種特殊的阻塞,由于鎖導致的阻塞。
4.多線程帶來的風險-線程安全
package thread;public class Demo15 {private static int count=0;public static void main(String[] args) {Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count++;}});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}});t1.start();t2.start();System.out.println(count);//0//main先執行打印}
}
加入t.join之后的結果:(在t1和t2執行完后打印)
package thread;public class Demo15 {private static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count++;}System.out.println("t1結束");});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}System.out.println("t2結束");});t1.start();t2.start();t1.join();t2.join();//兩個線程誰先join無所謂//總的阻塞時間是t1和t2較長的時間//區別在于是分兩個join各自阻塞一會//還是在一個join全部阻塞完System.out.println(count);//63060(多線程并發執行引起的問題)}
}
上述代碼是由于多線程的并發執行代碼引起的bug,稱為“線程安全問題”,或者叫做“線程不安全”。
如果代碼在多線程并發執行的環境下也不會出現類似上述的bug,就稱代碼“線程安全”。
實現預期結果:(串行執行)
package thread;public class Demo15 {private static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count++;}System.out.println("t1結束");});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}System.out.println("t2結束");});t1.start();t1.join();t2.start();t2.join();//兩個線程誰先join無所謂//總的阻塞時間是t1和t2較長的時間//區別在于是分兩個join各自阻塞一會//還是在一個join全部阻塞完System.out.println(count);//100000}
}
count++實際對應3個CPU指令:
- load,把內存中的值(count變量)讀取到CPU寄存器
- add,把指定寄存器中的值進行+1操作(結果還是在寄存器中)
- save,把寄存器中的值寫回到內存中
CPU執行這三條指令的過程中,隨時可能觸發線程的調度切換
4.1 線程安全問題產生原因
- 1. [根本]操作系統對于線程的調度是隨機的,搶占式執行
- 2. 多個線程同時修改同一個變量(出現了中間結果相互覆蓋的情況)
解決方法:和代碼的結構直接相關,調整代碼結構,規避一些線程不安全的代碼。
有些情況下,需求就是需要多線程修改同一個變量。
- 3. 修改操作,不是原子的。
如果修改操作只對應一個CPU指令,就認為是原子的,CPU不會出現“一條指令執行一半”的情況。
解決方法:加鎖。通過加鎖,讓不是原子的操作,打包成一個原子的操作。
加鎖操作,不是把線程鎖死到CPU上,禁止線程被調度走;而是禁止其他線程重新加這個鎖,避免其他線程在當前線程執行過程中插隊。
【事務的4個特性:原子性,一致性,持久性,隔離性】
Java中的String就是采取“不可變”特性確保線程安全。(String沒有提供public的修改方法)
String的final用來實現“不可繼承”。
兩個線程,針對同一個對象加鎖,才會產生互斥效果。(一個線程加鎖,另一個線程就阻塞等待,等第一個線程釋放鎖才有機會)
package thread;public class Demo15 {private static int count=0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1=new Thread(()->{for(int i=0;i<50000;i++){synchronized (locker){count++;}}System.out.println("t1結束");});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){synchronized (locker){count++;}}System.out.println("t2結束");});t1.start();t2.start();t1.join();t2.join();//兩個線程誰先join無所謂//總的阻塞時間是t1和t2較長的時間//區別在于是分兩個join各自阻塞一會//還是在一個join全部阻塞完System.out.println(count);//100000}
}
- 4. 內存可見性問題引起的線程不安全
- 5. 指令重排序引起的線程不安全
Java中使用synchronized+代碼塊,很少使用lock+unlock函數的方式,是因為unlock容易遺漏。
lock和unlock中間如果有return或者異常處理,后面的unlock會執行不到。
synchronized就避免了這種情況。
package thread;class Counter{public int count=0;public void add(){synchronized (this){count++;}}public int get(){return count;}
}
public class Demo18 {public static void main(String[] args) throws InterruptedException {Object locker=new Object();Counter counter=new Counter();Thread t1=new Thread(()->{for(int i=0;i<50000;i++){counter.add();}});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.get());}
}
注意:?
public void add(){synchronized (this){count++;}}//可以變形為:synchronized public void add(){count++;}
StringBuffer和Vector這些對象方法上就是帶有synchronized(針對this加鎖)
有一種特殊情況:
static修飾的方法不存在this,此時,synchronized修飾static方法,相當于針對類對象加鎖:
public synchronized static void func(){synchronized(Counter.class){}}
synchronized修飾普通方法,相當于是給this加鎖;
synchronized修飾靜態方法,相當于是給類對象加鎖。
4.2 synchronized關鍵字
synchronized的特性:互斥;可重入
- 對一個線程連續加鎖兩次,會出現死鎖:
Thread t1=new Thread(()->{for(int i=0;i<50000;i++){synchronized (locker){synchronized (locker){//阻塞等待,等到前一次的鎖被釋放,第二次加鎖的阻塞才會解除counter.add();}}}});
synchronized的可重入特性可以解決:
當某個線程針對一個鎖加鎖成功后,后續該線程再次針對這個鎖進行加鎖,不會觸發阻塞,而是直接往下走。但是如果是其他線程嘗試加鎖就會正常阻塞。
可重入鎖的實現原理,關鍵在于讓鎖對象內部保存當前是哪個線程持有這把鎖。
后續有線程針對這個鎖加鎖的時候,對比一下鎖持有者的線程是否和當前加鎖的線程是同一個。
如何自己實現一個可重入鎖?
- 在鎖內部記錄當前是哪個線程持有的鎖,后續每次加鎖,都進行判定
- 通過計數器,記錄當前加鎖的次數,從而確定何時真正進行解鎖
- 兩個線程兩把鎖,每個線程獲取到一把鎖之后,嘗試獲取對方的鎖會引起死鎖:
package thread;public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1=new Object();Object locker2=new Object();Thread t1=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1線程兩個鎖都獲取到");}}});Thread t2=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2線程兩個鎖都獲取到");}}});t1.start();t2.start();t1.join();t2.join();}
}
如果不加sleep,有可能t1把locker1和locker2都拿到了,t2還沒開始,自然無法構成死鎖。
- 死鎖的第三種情況:N個線程M把鎖
一個經典的模型:哲學家就餐問題
構成死鎖的四個必要條件:
- 鎖是互斥的。一個線程拿到鎖之后,另一個線程再次嘗試獲取鎖,必須要阻塞等待。
- 鎖是不可搶占的。
- 請求和保持。一個線程拿到鎖1之后,不釋放鎖1的前提下獲取鎖2
- 循環等待。多個線程多把鎖之間的等待過程構成了“循環”
4.3?解決死鎖的方法
- 1.破壞“請求和保持”
代碼中加鎖不要嵌套:
package thread;public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1=new Object();Object locker2=new Object();Thread t1=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}}synchronized (locker2){System.out.println("t1線程兩個鎖都獲取到");}});Thread t2=new Thread(()->{synchronized (locker2){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}}synchronized (locker1){System.out.println("t2線程兩個鎖都獲取到");}});t1.start();t2.start();t1.join();t2.join();}
}
- 2.破壞“循環等待”
約定好加鎖的順序:(先獲取序號小的,后獲取序號大的)
package thread;public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1=new Object();Object locker2=new Object();Thread t1=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1線程兩個鎖都獲取到");}}});Thread t2=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2線程兩個鎖都獲取到");}}});t1.start();t2.start();t1.join();t2.join();}
}
4.4 Java標準庫中的線程安全類
數據結構集合類自身沒有進行任何加鎖限制,線程不安全:
ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder
但是還有一些是線程安全的,使用了一些鎖機制來控制:
Vector(不推薦使用),HashTable(不推薦使用),ConcurrentHashMap(推薦),StringBuffer
代碼中使用鎖,意味著代碼可能因為鎖的競爭產生阻塞,從而程序的執行效率降低。
String雖然沒有加鎖,但是不涉及“修改”,仍然是線程安全的。
5.volatile關鍵字
5.1 volatile能保證內存可見性
線程安全問題。一個線程讀取,一個線程修改,修改線程修改的值并沒有被讀線程讀取到。
package thread;import java.util.Scanner;public class Demo21 {private static int flg=0;public static void main(String[] args) {Thread t1=new Thread(()->{while(flg==0){}System.out.println("t1線程結束");});Thread t2=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("請輸入flg的值:");flg=sc.nextInt();});t1.start();t2.start();}
}
在t1線程中while循環里面,JVM執行讀flg的操作,發現始終是0(用戶輸入時間相較于讀取時間太長),于是把讀取內存的操作優化為讀取寄存器的操作,后續load不再重新讀內存,直接從寄存器中取。當用戶輸入值修改flg,此時t1線程就感知不到了。
package thread;import java.util.Scanner;public class Demo21 {private static int flg=0;public static void main(String[] args) {Thread t1=new Thread(()->{while(flg==0){try{Thread.sleep(1);//加了sleep之后,sleep消耗的時間相比于上面load flg的操作,高了很多}catch(InterruptedException e){throw new RuntimeException(e);}}System.out.println("t1線程結束");});Thread t2=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("請輸入flg的值:");flg=sc.nextInt();});t1.start();t2.start();}
}
針對內存可見性問題,也不能指望sleep解決,因為sleep大大影響到程序的效率。
因此,在語法中,引入volatile關鍵字來修飾某個變量,此時編譯器對變量的讀取操作,就不會優化成讀寄存器。
package thread;import java.util.Scanner;public class Demo21 {private volatile static int flg=0;public static void main(String[] args) {Thread t1=new Thread(()->{while(flg==0){}System.out.println("t1線程結束");});Thread t2=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("請輸入flg的值:");flg=sc.nextInt();});t1.start();t2.start();}
}
JMM Java內存模型
每個線程有一個自己的“工作內存”,同時這些線程共享一個“主內存”。當一個線程循環進行上述讀取變量操作的時候,就會把主內存中的數據,拷貝到該線程的工作內存中,后續另一個線程修改,也是先修改自己的工作內存,拷貝到主內存中。由于第一個線程仍然在讀自己的工作內存,因此感知不到主內存的變化。
5.2 volatile不保證原子性
6. wait和notify
public class Demo23 {public static void main(String[] args) throws InterruptedException {Object obj=new Object();System.out.println("1");obj.wait();//拋出異常//wait,會先執行解鎖操作,給其他線程獲取鎖的機會//前提是已經加上鎖System.out.println("2");}
}
加上鎖:
public class Demo23 {public static void main(String[] args) throws InterruptedException {Object obj=new Object();System.out.println("1");synchronized (obj){//加鎖obj.wait();//進入wait,釋放鎖,阻塞等待//如果其他線程做完了必要的工作,調用notify喚醒這個wait線程//wait就會解除阻塞,重新獲取到鎖,繼續執行并返回(又一次加鎖)//要求synchronized的鎖對象必須和wait的對象是同一個}System.out.println("2");}
}
package thread;import java.util.Scanner;public class Demo24 {public static void main(String[] args) {Object locker=new Object();Thread t1=new Thread(()->{try {System.out.println("wait前");synchronized (locker) {locker.wait();//wait先釋放鎖}System.out.println("wait后");} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("輸入任意內容,通知喚醒t1");sc.next();//next就是一個帶有阻塞的操作,等待用戶輸入synchronized (locker){locker.notify();//這里需要先拿到鎖,再notify}});t1.start();t2.start();}
}
wait操作必須搭配鎖進行,wait會先釋放鎖;
notify操作,原則上不涉及加鎖解鎖操作,在Java中,強制要求notify搭配synchronized.
要確保先wait后notify,如果先notify后wait,此時wait無法被喚醒。notify的這個線程也沒有副作用(notify一個沒有在wait的對象,不會報錯)。
搭配synchronized,鎖對象得和調用wait/notify的對象一致。
如果多個線程在同一對象上wait,進行notify的時候是隨機喚醒其中一個線程,再一次notify喚醒另一個線程:
package thread;import java.util.Scanner;public class Demo25 {public static void main(String[] args) {Object locker = new Object();Thread t1=new Thread(()->{try{System.out.println("t1 wait前");synchronized (locker){locker.wait();}System.out.println("t1 wait后");} catch (InterruptedException e){throw new RuntimeException(e);}});Thread t2=new Thread(()->{try{System.out.println("t2 wait前");synchronized (locker){locker.wait();}System.out.println("t2 wait后");} catch (InterruptedException e){throw new RuntimeException(e);}});Thread t3=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("輸入任意內容,喚醒其中一個線程:");sc.next();synchronized (locker){locker.notify();}System.out.println("輸入任意內容,喚醒另一個線程:");sc.next();synchronized (locker){locker.notify();}});t1.start();t2.start();t3.start();}
}
notifyAll一次喚醒所有線程:
雖然同時喚醒t1和t2,由于wait喚醒之后要重新加鎖。其中某個線程先加上鎖開始執行,另一個線程因為加鎖失敗再次阻塞等待。等先走的線程解鎖,后走的線程才能加上鎖,繼續執行。
package thread;import java.util.Scanner;public class Demo25 {public static void main(String[] args) {Object locker = new Object();Thread t1=new Thread(()->{try{System.out.println("t1 wait前");synchronized (locker){locker.wait();}System.out.println("t1 wait后");} catch (InterruptedException e){throw new RuntimeException(e);}});Thread t2=new Thread(()->{try{System.out.println("t2 wait前");synchronized (locker){locker.wait();}System.out.println("t2 wait后");} catch (InterruptedException e){throw new RuntimeException(e);}});Thread t3=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("輸入任意內容,喚醒所有線程:");sc.next();synchronized (locker){locker.notifyAll();}});t1.start();t2.start();t3.start();}
}
wait和join類似,提供了“死等”版本和“超時時間”版本。
wait和sleep都有等待時間。wait可以使用notify提前喚醒,sleep也可以使用Interrupt提前喚醒。
wait和sleep最主要的區別在于對鎖的操作:
- wait必須要搭配鎖,先加鎖,才能用wait,sleep不需要
- 如果都是在synchronized內部使用,wait會釋放鎖,sleep不釋放鎖
有三個線程,分別只能打印A,B,C,要求按順序打印ABC,打印10次:
package thread;public class Demo26 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Thread t1=new Thread(()->{try {for(int i=0;i<10;i++){synchronized (locker1){locker1.wait();}System.out.print("A");synchronized (locker2){locker2.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2=new Thread(()->{try {for(int i=0;i<10;i++){synchronized (locker2){locker2.wait();}System.out.print("B");synchronized (locker3){locker3.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t3=new Thread(()->{try {for(int i=0;i<10;i++){synchronized (locker3){locker3.wait();}System.out.println("C");synchronized (locker1){locker1.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();t3.start();//主線程中,通知一下locker1,讓上述邏輯從t1開始執行//需要確保上述三個線程都執行到wait,再進行notifyThread.sleep(1000);synchronized (locker1){locker1.notify();}}
}