Java多线程02(线程安全、线程同步、等待唤醒机制)
生活随笔
收集整理的這篇文章主要介紹了
Java多线程02(线程安全、线程同步、等待唤醒机制)
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
Java多線程2(線程安全、線程同步、等待喚醒機制、單例設計模式)
1、線程安全
- 如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
- 通過案例演示線程的安全問題:電影院要賣票。
- 我們模擬電影院的賣票過程。假設本場電影的座位共100個(本場電影只能賣100張票)。
- 我們來模擬電影院的售票窗口,實現多個窗口同時賣這場電影的票(多個窗口一起賣這100張票)
- 需要窗口,采用線程對象來模擬;
- 需要票,Runnable接口子類來模擬;
- 代碼:
- 分析:
- 三個窗口每個窗口都在買票,假設此時只剩一張票,可能會發生以下情況:
- 線程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
同步代碼塊中的鎖對象可以是任意的對象;但多個線程時,要使用同一個鎖對象才能夠保證線程安全。
使用同步代碼塊,對電影院賣票案例中Ticket類進行如下代碼修改:
- 當使用了同步代碼塊后,上述的線程的安全問題,解決了。
- 分析:
- 同步對象:可以是任意對象,可以稱之為同步鎖,對象監視器,注意不能用匿名內部類,因為這樣在會導致每次獲得鎖對象都是新的對象,無法實現加鎖的效果。
- 同步是如何保證安全性的:沒有鎖的線程不能執行,只能等待。
- 具體執行過程:
- 線程遇到同步代碼塊后,線程判斷同步鎖還有沒有
- 如果同步鎖有:獲取鎖,進入同步中,去執行,執行完畢后,離開同步代碼塊,線程將鎖對象還回去。
- 在同步中的線程休眠,此時另一個線程會執行;
- 遇到同步代碼塊,判斷對象鎖是否還有,如果沒有鎖,該線程不能進入同步代碼塊中執行,被阻擋在同步代碼塊的外面,處于阻塞狀態。
- 加了同步之后,執行步驟增加:線程首先進同步判斷鎖,獲取鎖,出同步釋放鎖,導致程序運行速度的下降。
- 沒有鎖的線程,不能進入同步,在同步中的線程,不出同步,不會釋放鎖。
2.2 同步方法(推薦使用)
- 同步方法:在方法聲明上加上synchronized
- 同步方法中的鎖對象是 this
- 使用同步方法,對電影院賣票案例中Ticket類進行如下代碼修改:
- 問題:同步方法中有鎖嗎?
- 有,同步方法中的對象鎖是本類方法的引用
- 靜態同步方法: 在方法聲明上加上static synchronized
- 靜態同步方法中的鎖對象是本類自己:類名.class
1.4 Lock接口
- 查閱API,查閱Lock接口描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。
- 實現類:ReentrantLock
- Lock接口中的常用方法
- void lock():獲得鎖。
- void unlock():釋放鎖。
- Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。
- 我們使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下代碼修改:
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(线程安全、线程同步、等待唤醒机制)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redis伪集群脚本
- 下一篇: 廖雪峰Java1-3流程控制-9brea