日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java多线程02(线程安全、线程同步、等待唤醒机制)

發布時間:2025/3/16 java 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java多线程02(线程安全、线程同步、等待唤醒机制) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Java多線程2(線程安全、線程同步、等待喚醒機制、單例設計模式)

1、線程安全

  • 如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
    • 通過案例演示線程的安全問題:電影院要賣票。
    • 我們模擬電影院的賣票過程。假設本場電影的座位共100個(本場電影只能賣100張票)。
    • 我們來模擬電影院的售票窗口,實現多個窗口同時賣這場電影的票(多個窗口一起賣這100張票)
    • 需要窗口,采用線程對象來模擬;
    • 需要票,Runnable接口子類來模擬;
  • 代碼:
public class Tickets implements Runnable {private int num = 100; //(1)@Overridepublic void run() { // (2)// 死循環,一直處于可以售票狀態while(true) { // (3)if(num>0) { // (4)System.out.println(Thread.currentThread().getName()+" 第 "+ num-- + " 張票售出"); //(5)}}} }public class TicketsDemo {public static void main(String[] args) { //(6)Tickets t = new Tickets(); // (7)new Thread(t).start(); // (8)new Thread(t).start(); // (9)new Thread(t).start(); // (10)} }
  • 分析:
    • 三個窗口每個窗口都在買票,假設此時只剩一張票,可能會發生以下情況:
    • 線程t1執行run方法到(4)時,產生阻塞,線程t2執行run方法到(4)時葉阻塞,線程t3執行完了run方法,釋放CPU,此時num=0;t1再次得到CPU時,不會再次判斷,而是直接執行下一步(5),這時就會發生0--,出現出售第0張票,并且票數變成負數,這樣就出現了安全隱患。
  • 運行結果發現:上面程序出現了問題
    • 票出現了重復的票
    • 錯誤的票 0、-1
  • 線程安全問題都是由全局變量及靜態變量引起的。
  • 若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

  • 解決辦法:
    • 當一個線程進入數據操作的時候,無論是否休眠,其他線程智能等待。

2、線程同步(線程安全處理Synchronized)

  • java中提供了線程同步機制,它能夠解決上述的線程安全問題。
  • 線程同步的方式有兩種:
    • 方式1:同步代碼塊
    • 方式2:同步方法

2.1 同步代碼塊

  • 同步代碼塊: 在代碼塊聲明上 加上synchronized
synchronized (鎖對象) {可能會產生線程安全問題的代碼 }
  • 同步代碼塊中的鎖對象可以是任意的對象;但多個線程時,要使用同一個鎖對象才能夠保證線程安全。

  • 使用同步代碼塊,對電影院賣票案例中Ticket類進行如下代碼修改:

/*通過線程休眠,出現安全問題解決安全問題,Java程序,提供同步技術公式:syncronized (任意對象){線程要操作的共享數據} */ public class Tickets implements Runnable {// 定義出售的票數private int num = 100;Object obj = new Object(); // 創建對象,用于同步@Overridepublic void run() {// 死循環,一直處于可以售票狀態while(true) {// 線程共享數據,保證安全,加入同步代碼塊synchronized (obj) {if(num>0) {try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+" 第 "+ num-- + " 張票售出");}}}} }
  • 當使用了同步代碼塊后,上述的線程的安全問題,解決了。
  • 分析:
    • 同步對象:可以是任意對象,可以稱之為同步鎖,對象監視器,注意不能用匿名內部類,因為這樣在會導致每次獲得鎖對象都是新的對象,無法實現加鎖的效果。
    • 同步是如何保證安全性的:沒有鎖的線程不能執行,只能等待。
    • 具體執行過程:
      • 線程遇到同步代碼塊后,線程判斷同步鎖還有沒有
      • 如果同步鎖有:獲取鎖,進入同步中,去執行,執行完畢后,離開同步代碼塊,線程將鎖對象還回去。
      • 在同步中的線程休眠,此時另一個線程會執行;
      • 遇到同步代碼塊,判斷對象鎖是否還有,如果沒有鎖,該線程不能進入同步代碼塊中執行,被阻擋在同步代碼塊的外面,處于阻塞狀態。
    • 加了同步之后,執行步驟增加:線程首先進同步判斷鎖,獲取鎖,出同步釋放鎖,導致程序運行速度的下降。
    • 沒有鎖的線程,不能進入同步,在同步中的線程,不出同步,不會釋放鎖。

2.2 同步方法(推薦使用)

  • 同步方法:在方法聲明上加上synchronized
public synchronized void method(){可能會產生線程安全問題的代碼 }
  • 同步方法中的鎖對象是 this
  • 使用同步方法,對電影院賣票案例中Ticket類進行如下代碼修改:
/*采用同步方法的形式解決線程安全問題好處:代碼量少,簡潔做法:將線程共享數據和同步抽取到方法中*/ public class Tickets implements Runnable {private int num = 100;@Overridepublic void run() {// 死循環,一直處于可以售票狀態while(true) {payTicket();}}public synchronized void payTicket() {if(num>0) { try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+" 第 "+ num-- + " 張票售出");}} }
  • 問題:同步方法中有鎖嗎?
    • 有,同步方法中的對象鎖是本類方法的引用
  • 靜態同步方法: 在方法聲明上加上static synchronized
public static synchronized void method(){// 可能會產生線程安全問題的代碼 }
  • 靜態同步方法中的鎖對象是本類自己:類名.class

1.4 Lock接口

  • 查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。
    • 實現類:ReentrantLock
    • Lock接口中的常用方法
      • void lock():獲得鎖。
      • void unlock():釋放鎖。
    • Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。
  • 我們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下代碼修改:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;/*使用JDK1.5+的接口Locl,替換同步代碼塊,實現線程安全具體使用:Lock接口中的方法:lock(); // 獲取鎖unlock(); // 釋放鎖實現類:ReentrantLock*/ public class Tickets implements Runnable {// 存儲票數private static int num = 100; //在類的成員位置,創建Lock接口的實現類對象private Lock lock = new ReentrantLock(); @Overridepublic void run() {// 死循環,一直處于可以售票狀態while(true) {// 調用Lock接口中的方法,獲取鎖lock.lock();try {if(num>0) { Thread.sleep(200);System.out.println(Thread.currentThread().getName()+" 第 "+ num-- + " 張票售出");} }catch (InterruptedException e) {e.printStackTrace();}finally {// 釋放鎖,調用unlock方法lock.unlock(); }}} }

1.4 死鎖

  • 同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:
    • 程序出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。
  • 死鎖程序:
    • 前提:必須是多線程
    • 出現同步嵌套
    • 線程進入同步,獲取鎖,不出去同步,不會釋放鎖
  • 鎖的嵌套情況如下:

    synchronzied(A鎖){synchronized(B鎖){} } synchronzied(B鎖){synchronized(A鎖){} }
  • 注意A鎖和B鎖都是唯一的
  • 兩個線程每個獲得一個鎖,且都需要對方的鎖才能繼續執行,因此都會一直除以阻塞狀態,無法恢復,出現死鎖。

  • 我們進行下死鎖情況的代碼演示:

    // 定義鎖對象類 /*不允許任何類創建該對象只能通過類名調用靜態成員調用,不允許new保證了鎖的唯一性 */ public class LockA {private LockA() {}public final static LockA locka = new LockA(); }public class LockB { private LockB() {}public final static LockB lockb = new LockB(); }// 線程任務類 public class DeadLock implements Runnable{private int i = 0;@Overridepublic void run() {while(true) {if(i%2==0) {// 先進入A同步,再進入B同步synchronized (LockA.locka) {System.out.println(i+" --> if...locka");synchronized (LockB.lockb) {System.out.println(i+" --> if...lockb");}}}else {// 先進入B同步,再進入B同步synchronized (LockB.lockb) {System.out.println(i+" --> else...lockb");synchronized (LockA.locka) {System.out.println(i+" --> else...locka");}}}i++;}} }// 測試類 public class DeadLockDemo {public static void main(String[] args) {DeadLock deadLock = new DeadLock();new Thread(deadLock).start();new Thread(deadLock).start();} }// 運行結果: 0 --> if...locka 0 --> if...lockb 1 --> else...lockb 1 --> if...locka

1.5 等待喚醒機制

  • 在開始講解等待喚醒機制之前,有必要搞清一個概念—— 線程之間的通信。
  • 線程之間的通信:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。
  • 等待喚醒機制所涉及到的方法:
    • wait() :等待,無限等待。將正在執行的線程釋放其執行資格 和 執行權,并存儲到線程池中。
    • notify() :喚醒。喚醒線程池中被wait()的線程,一次喚醒一個,而且是任意的。
    • notifyAll() :喚醒全部:可以將線程池中的所有wait()線程都喚醒。
  • 所謂喚醒:就是讓線程池中的線程具備執行資格。
    • 必須注意的是:這些方法都是在同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的線程。
  • 仔細查看Java API之后,發現這些方法 并不定義在 Thread中,也沒定義在Runnable接口中,卻被定義在了Object類中,為什么這些操作線程的方法定義在Object類中?
    • 因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意對象。能被任意對象調用的方法一定定義在Object類中。
  • 線程通訊案例:輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務是:
  • 當input發現Resource中沒有數據時,開始輸入,輸入完成后,叫output來輸出。如果發現有數據,就wait();
  • 當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,然后,叫醒input來輸入數據。
  • 下面代碼,模擬等待喚醒機制的實現:
    • Resource.java

      /*定義資源類,有2個成員變量:name,sex同時有兩個線程,對資源中的變量操作1個對name,sex賦值1個對name,sex做變量的輸出打印*/ public class Resource {public String name;public String sex; }
    • Input.java

      /*輸入線程:對資源對象Resource中的成員變量賦值要求:一次賦值:張三,男另一次:李四,女*/ public class Input implements Runnable { private Resource r;public Input(Resource r) {this.r = r;}@Overridepublic void run() {int i = 0;while(true) {if(i%2==0) {r.name = "張三";r.sex = "男"; }else {r.name = "lisi";r.sex = "nv";}i++;}} }
    • Output.java

      /*輸出線程:對資源對象Resource中的成員變量輸出值*/ public class Output implements Runnable {private Resource r;public Output(Resource r) {this.r = r;}@Overridepublic void run() {while(true) {System.out.println("姓名:"+r.name + ", 性別:"+r.sex);}}}
    • ThreadDemo.java

      /*開啟輸入線程和輸出線程,實現賦值和打印*/ public class ThreadDemo {public static void main(String[] args) {Resource r = new Resource(); //共享數據Input in = new Input(r);Output out = new Output(r);new Thread(in).start();new Thread(out).start();} }
  • 此時會出現問題:打印出的結果并不是想要的結果

    姓名:lisi, 性別:nv 姓名:張三, 性別:nv 姓名:lisi, 性別:男 姓名:lisi, 性別:nv 姓名:lisi, 性別:nv 姓名:張三, 性別:男
  • 分析原因,兩個線程沒有實現同步。
  • 實現同步的方法:給線程加同步鎖。
    • 注意:給輸入和輸出加的同步鎖應為同一個對象鎖,而輸入和輸出線程是兩個不同的線程,因此不能使用this作為對象鎖,這里使用他們公用的資源類Resource對象。
  • 代碼修改如下:
    • Input.java修改

      /*輸入線程:對資源對象Resource中的成員變量賦值要求:一次賦值:張三,男另一次:李四,女*/ public class Input implements Runnable { private Resource r;public Input(Resource r) {this.r = r;}@Overridepublic void run() {int i = 0;while(true) {synchronized (r) {if(i%2==0) {r.name = "張三";r.sex = "男"; }else {r.name = "lisi";r.sex = "nv";}}i++;}} }
    • Input.java修改

      /*輸入線程:對資源對象Resource中的成員變量賦值要求:一次賦值:張三,男另一次:李四,女*/ public class Input implements Runnable { private Resource r;public Input(Resource r) {this.r = r;}@Overridepublic void run() {int i = 0;while(true) {synchronized (r) {if(i%2==0) {r.name = "張三";r.sex = "男"; }else {r.name = "lisi";r.sex = "nv";}}i++;}} }
    • Output.java修改:

      /*輸出線程:對資源對象Resource中的成員變量輸出值*/ public class Output implements Runnable {private Resource r;public Output(Resource r) {this.r = r;}@Overridepublic void run() {while(true) {synchronized (r) {System.out.println("姓名:"+r.name + ", 性別:"+r.sex); }}} }
  • 此時還有問題:輸出沒有交替進行

    姓名:張三, 性別:男 姓名:張三, 性別:男 姓名:張三, 性別:男 姓名:lisi, 性別:nv 姓名:lisi, 性別:nv 姓名:lisi, 性別:nv 姓名:lisi, 性別:nv
  • 分析原因:
    • 輸入:輸入完成以后,必須等待,等待輸出打印結束后,才能進行下一次賦值。
    • 輸出:輸出完變量值后,必須等待,等待輸入的重新賦值后,才能進行下一次打印。
  • 解決方法:
    • 輸入:賦值后,執行方法wait(),永遠等待,
    • 輸出:變量打印輸出,在輸出等待之前,喚醒輸入的nitify(),自己再wait等待。
    • 輸入:被喚醒后,重新對變量賦值,然后喚醒輸出的線程notify,自己再wait()等待。
    • 如何判斷輸入輸出結束:設置一個標記flag,以標記為準;
      • flag = false; 說明賦值完成
      • flag = true; 獲取值完成
    • 輸入操作:
      • 需要不需要賦值,看標記
      • 如果標記為true,等待
      • 如果標記為false,不需要等待,賦值
      • 賦值后,將標記改為true
    • 輸出操作:
      • 需要不需要獲取,看標記
      • 如過標記為false,等待
      • 如果標記為true,打印
      • 打印后,將標記改為false
  • 代碼修改如下:
    • Resource.java修改

      /*定義資源類,有2個成員變量:name,sex同時有兩個線程,對資源中的變量操作1個對name,sex賦值1個對name,sex做變量的輸出打印*/ public class Resource {public String name;public String sex;public boolean flag = false; }
    • Input.java修改

      /*輸入線程:對資源對象Resource中的成員變量賦值要求:一次賦值:張三,男另一次:李四,女*/ public class Input implements Runnable { private Resource r;public Input(Resource r) {this.r = r;}@Overridepublic void run() {int i = 0;while(true) {synchronized (r) { if(r.flag) { // 標記是true,等待try {r.wait();} catch (InterruptedException e) {e.printStackTrace();}}if(i%2==0) {r.name = "張三";r.sex = "男"; }else {r.name = "lisi";r.sex = "nv";}// 標記改為true,將對方線程喚醒r.flag = true;r.notify();}i++;}} }
    • Output.java修改

      /*輸出線程:對資源對象Resource中的成員變量輸出值*/ public class Output implements Runnable {private Resource r;public Output(Resource r) {this.r = r;}@Overridepublic void run() {while(true) {synchronized (r) { if(!r.flag) { // 判斷標記,false,等待try {r.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("姓名:"+r.name + ", 性別:"+r.sex);r.flag = false;r.notify();}}} }
  • 注意:
    • 等待和喚醒必須是由同一個對象調用,這里用Resource的對象

2 總結

同步鎖

  • 多個線程想保證線程安全,必須要使用同一個鎖對象
    • 同步代碼塊

      synchronized (鎖對象){可能產生線程安全問題的代碼 }
  • 同步代碼塊的鎖對象可以是任意的對象
    • 同步方法

      public synchronized void method()可能產生線程安全問題的代碼 } // 同步方法中的鎖對象是 this
    • 靜態同步方法

      public synchronized void method()可能產生線程安全問題的代碼 } // 靜態同步方法中的鎖對象是 類名.class

多線程有幾種實現方案,分別是哪幾種?

  • 繼承Thread類
  • 實現Runnable接口
  • 通過線程池,實現Callable接口

同步有幾種方式,分別是什么?

  • 同步代碼塊
  • 同步方法
  • 靜態同步方法

啟動一個線程是run()還是start()?它們的區別?

  • 啟動一個線程是start()
  • 區別:
    • start: 啟動線程,并調用線程中的run()方法
    • run : 執行該線程對象要執行的任務

sleep()和wait()方法的區別

  • sleep: 不釋放鎖對象, 釋放CPU使用權;在休眠的時間內,不能喚醒
  • wait(): 釋放鎖對象, 釋放CPU使用權;在等待的時間內,能喚醒

為什么wait(),notify(),notifyAll()等方法都定義在Object類中

  • 鎖對象可以是任意類型的對象

轉載于:https://www.cnblogs.com/luoyu113/p/10239666.html

總結

以上是生活随笔為你收集整理的Java多线程02(线程安全、线程同步、等待唤醒机制)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。