【JAVA EE初階】多線程(上)

目錄

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. 內存空間小,硬盤空間大
  3. 內存成本高,硬盤成本低
  4. 內存數據掉電后丟失,硬盤數據掉電后持續存儲

有的設備既是輸入設備又是輸出設備,比如觸摸屏,網卡(上網時和網線連接部分的硬件設備,集成在主板上)...

1.2 現代CPU主要關心指標(和日常開發密切相關的)

  1. CPU的頻率
    1. 基頻/默頻
    2. 睿頻/加速頻率
  2. CPU的核心數
    1. 大小核

CPU的基本工作流程:讀取指令、解析指令、執行指令

1.3 計算機中,一個漢字占幾個字節?

取決于字符集(漢字怎樣編碼)

  1. gbk(中國大陸上曾經廣泛使用的)。Windows10/11簡體中文版默認gbk.使用VS寫代碼打印漢字strlen,結果是2;
  2. utf8(當下全世界最流行的編碼方式)
    1. 一個漢字占3字節。utf8本身是變長編碼(1-4);
  3. unicode(Java的char就是使用unicode)一個漢字2字節
    1. 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的一些關鍵要點

  1. pid(進程id)進程的身份標識符
  2. 內存指針(一組指針)
    1. 進程需要知道要執行的指令的地址
    2. 指令依賴的數據在哪里
  3. 文件描述符表
  4. 進程狀態
  5. 進程優先級
  6. 進程上下文
  7. 進程的記賬信息(統計每個進程在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);}}
}
對?上?兩種?法:
  1. 繼承 Thread 類, 直接使? this 就表?當前線程對象的引?.
  2. 實現 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());}
}
thread 收到通知的?式有兩種:
  1. 如果線程因為調? wait/join/sleep 等?法?阻塞掛起,則以 InterruptedException 異常的形式通 知,清除中斷標志 當出現 InterruptedException 的時候, 要不要結束線程取決于 catch 中代碼的寫法. 可以選擇忽略這個異常, 也可以跳出循環結束線程.
  2. 否則,只是內部的?個中斷標志被設置,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指令:

  1. load,把內存中的值(count變量)讀取到CPU寄存器
  2. add,把指定寄存器中的值進行+1操作(結果還是在寄存器中)
  3. 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的可重入特性可以解決:

當某個線程針對一個鎖加鎖成功后,后續該線程再次針對這個鎖進行加鎖,不會觸發阻塞,而是直接往下走。但是如果是其他線程嘗試加鎖就會正常阻塞。

可重入鎖的實現原理,關鍵在于讓鎖對象內部保存當前是哪個線程持有這把鎖。

后續有線程針對這個鎖加鎖的時候,對比一下鎖持有者的線程是否和當前加鎖的線程是同一個。

如何自己實現一個可重入鎖?

  1. 在鎖內部記錄當前是哪個線程持有的鎖,后續每次加鎖,都進行判定
  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();}
}

如果不加sleep,有可能t1把locker1和locker2都拿到了,t2還沒開始,自然無法構成死鎖。

  • 死鎖的第三種情況:N個線程M把鎖

一個經典的模型:哲學家就餐問題

構成死鎖的四個必要條件:

  1. 鎖是互斥的。一個線程拿到鎖之后,另一個線程再次嘗試獲取鎖,必須要阻塞等待。
  2. 鎖是不可搶占的。
  3. 請求和保持。一個線程拿到鎖1之后,不釋放鎖1的前提下獲取鎖2
  4. 循環等待。多個線程多把鎖之間的等待過程構成了“循環”

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最主要的區別在于對鎖的操作:

  1. wait必須要搭配鎖,先加鎖,才能用wait,sleep不需要
  2. 如果都是在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();}}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/93382.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/93382.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/93382.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

用互聯網思維擴展電商后臺的 CRUD 功能

一、自定義實現MyBatis-Plus逆向工程 多數據源的問題解決了&#xff0c;接下來開始進行實際開發時&#xff0c;你會發現&#xff0c;最麻煩的一件事情就是要創建與數據庫表對應的POJO了。這些沒什么難度&#xff0c;但是繁瑣的內容會占據大量的開發時間。比如一個PmsProducr對…

無代碼測試平臺ATECLOUD全場景測試方案

ATECLOUD 智能云測試平臺是有納米軟件開發的一款以無代碼架構與彈性擴展體系為核心的自動化測試平臺&#xff0c;通過數據模型驅動的創新設計&#xff0c;為研發、產線等多場景提供高效可控的測試解決方案。?無代碼架構 ATECLOUD 打破傳統技術壁壘&#xff0c;構建完全可視化的…

當 AI 重構審計流程,CISA 認證為何成為破局關鍵

在南京審計大學最新發布的《面向審計行業 DeepSeek 大模型操作指南》中&#xff0c;一組數據引發行業深思&#xff1a;通過自動化數據處理、智能風險識別和定制化報告生成&#xff0c;AI 大模型能幫助審計人員降低 40% 以上的人工成本&#xff0c;同時將風險識別準確率提升至 9…

NAT技術、代理服務器

NAT/NAPT技術NAT的全稱是network address translation&#xff0c;網絡地址轉換。NAT 能在對外通信時夠將源 IP 轉為新源 IP&#xff0c;對內通信時將目的ip轉換成新目的ip&#xff0c;實現這個操作&#xff0c;靠的是地址轉換表但NAT的說法其實是不準確的&#xff0c;因為多個…

【硬件-筆試面試題】硬件/電子工程師,筆試面試題-45,(知識點:負反饋的作用,基礎理解,干擾和噪聲的抑制)

目錄 1、題目 2、解答 步驟一&#xff1a;明確負反饋的作用原理 步驟二&#xff1a;逐一分析選項 3、相關知識點 一、負反饋的基本原理 二、負反饋對干擾和噪聲的抑制機制 三、選項分析與答案 四、擴展思考&#xff1a;如何抑制不同位置的干擾&#xff1f; 總結 題目…

Flutter藍牙BLE開發完全指南(內含高級功能擴展)

Flutter藍牙BLE開發完全指南 我將為您提供一個完整的Flutter藍牙BLE實現方案,包含UI設計、權限處理、設備掃描、連接通信等完整功能。 完整實現方案 1. 添加依賴與權限配置 pubspec.yaml dependencies:flutter:sdk: flutterflutter_blue_plus: ^1.10.0permission_handler…

使用 Canvas 替代 <video> 標簽加載并渲染視頻

在部分瀏覽器環境或業務場景下&#xff0c;直接使用 <video> 標簽加載視頻會出現首幀延遲的情況。以下方法通過 WebGPU Canvas 2D 將視頻幀繪制到自定義 Canvas 上&#xff0c;讓 <video> 只做解碼&#xff0c;WebGPU 接管渲染&#xff0c;通過最小化對象創建 精…

基于Flask的智能停車場管理系統開發實踐

在現代城市中&#xff0c;停車難已成為一個普遍問題。為了解決這一問題&#xff0c;我開發了一個基于Python Flask框架的智能停車場管理系統。該系統集成了車牌識別、車位狀態監控、收費管理等多項功能&#xff0c;為停車場的智能化管理提供了完整的解決方案。系統功能概述該停…

【C#獲取高精度時間】

在C#中&#xff0c;有幾種方法可以獲取高精度時間&#xff08;高分辨率時間戳&#xff09;&#xff0c;適用于性能測量、計時等需要高精度的場景。以下是幾種常用方法&#xff1a; 1. 使用 Stopwatch 類&#xff08;推薦&#xff09; Stopwatch 類提供了最高精度的時間測量&…

Spring Boot + React 打造現代化高校成績管理系統實戰記錄

作者: 笙囧同學 發布時間: 2025年7月 技術棧: Spring Boot 3.2.3 React 18 TypeScript 華為云GaussDB 項目類型: 全棧Web應用 開發周期: 30天 代碼量: 15000 行 &#x1f4d6; 前言 大家好&#xff0c;我是笙囧同學&#xff01;&#x1f64b;?♂? 作為一名計算機科學與技…

形參表不匹配(BUG)

在您的代碼中&#xff0c;存在兩個主要問題導致"形參表中不匹配"的錯誤&#xff1a;erase() 函數中的成員變量名錯誤iterator erase(iterator pos) {// ...size--; // ? 錯誤&#xff1a;成員變量名為 _size 而非 sizereturn iterator(next); }修正&#xff1a;ite…

Spring循環依賴以及三個級別緩存

Spring循環依賴以及三個級別緩存 什么是循環依賴&#xff1f; 循環依賴&#xff0c;顧名思義&#xff0c;就是指兩個或多個 Spring Bean 之間相互依賴&#xff0c;形成一個閉環。 最常見也是 Spring 能夠“解決”的循環依賴是構造器注入 和 setter 注入 混合或單獨使用時&…

《零基礎入門AI:OpenCV圖像預處理進一步學習》

本文全面講解OpenCV圖像預處理的七大核心技術&#xff08;插值方法、邊緣填充、圖像矯正&#xff08;透視變換&#xff09;、圖像掩膜、ROI切割、圖像添加水印、圖像噪點消除&#xff09;&#xff0c;每個知識點都配有詳細解釋和實用代碼示例&#xff0c;幫助初學者建立系統的圖…

MongoDB的內存和核心數對于運行效率的影響

在 MongoDB 線上生產環境中&#xff0c;CPU&#xff08;核心&#xff09; 和 內存 是兩大關鍵硬件資源&#xff0c;它們在不同的操作場景下發揮著核心作用&#xff0c;共同影響著數據庫的性能、穩定性和擴展性。理解它們的作用場景至關重要&#xff0c;是容量規劃、性能優化和故…

自己的SAPGUI嘗試

為滿足用戶需求&#xff0c;博主做了一個臺賬管理程序&#xff0c;嘗試用自己的程序做GUI&#xff0c;用SAP 系統做數據庫。 運行了半年&#xff0c;程序很nice,用戶每天都在高效的使用&#xff0c;已經有十幾萬的數據。 總結一下這次自己的GUI嘗試&#xff0c;好處是C# WINFOR…

高效處理 JSON 數據:JsonUtil 工具類全方位解析與實戰

在現代軟件開發中,JSON(JavaScript Object Notation)已成為數據交換的“通用語言”——從前后端接口通信到微服務數據交互,從配置文件解析到日志格式化,幾乎所有場景都離不開JSON的處理。然而,原生JSON框架(如FastJSON、Jackson)的API往往需要大量重復代碼,且空指針、…

Python 庫手冊:xmlrpc.client 與 xmlrpc.server 模塊

xmlrpc.client 和 xmlrpc.server 是 Python 標準庫中用于構建基于 XML-RPC 協議的遠程過程調用&#xff08;RPC&#xff09;通信模塊。xmlrpc.client 用于編寫客戶端程序&#xff0c;向遠程服務器發起方法調用。xmlrpc.server 用于編寫服務器端&#xff0c;暴露本地方法供遠程客…

渲染篇(一):從零實現一個“微型React”:Virtual DOM的真面目

渲染篇(一)&#xff1a;從零實現一個“微型React”&#xff1a;Virtual DOM的真面目 引子&#xff1a;前端性能的“永恒之問” 在前面兩章中&#xff0c;我們已經奠定了堅實的架構基礎。我們用“任務調度器”建立了聲明式和模塊化的編程范式&#xff0c;并通過對比MVC等模式論…

SWC 深入全面講解

一、核心功能與原理 1. 高性能編譯 Rust 架構優勢&#xff1a;SWC 基于 Rust 編寫&#xff0c;利用 Rust 的性能和并發性優勢&#xff0c;編譯速度比 Babel 快約 20 倍&#xff0c;比 TypeScript 編譯器更快。并行編譯&#xff1a;支持多線程并行處理&#xff0c;在四核基準測試…

XML Expat Parser:深入解析與高效應用

XML Expat Parser:深入解析與高效應用 引言 XML(可擴展標記語言)作為一種廣泛使用的標記語言,在數據交換、存儲和表示中扮演著重要角色。XML Expat Parser 是一個高性能、可擴展的XML解析庫,廣泛應用于各種編程語言中。本文將深入探討XML Expat Parser 的原理、特性以及…