網(wǎng)站建設(shè)政策自己如何注冊一個網(wǎng)站
文章目錄
- 一.線程安全的概念
- 1.1 線程安全的概念
- 1.2 線程不安全的原因
- 1.3 解決線程不安全
- 二.synchronized-monitor lock(監(jiān)視器鎖)
- 2.1 synchronized的特性
- (1)互斥
- (2)刷新內(nèi)存
- (3)可重入
- 2.2 synchronied使用方法
- 1.直接修飾普通方法:
- 2.修飾靜態(tài)方法:
- 3.修飾代碼塊:
- 三.死鎖
- 3.1死鎖的情況
- 3.2 死鎖的四個必要條件
- 1.互斥使用
- 2.不可搶占
- 3.請求和保持
- 4.循環(huán)等待
- 3.3解決死鎖的辦法
- 四.volatile 關(guān)鍵字
- 五. wait和notify
- 5.1 wait()方法
- 5.2 notify()方法
一.線程安全的概念
先來看一段代碼
class Counter{public int count = 0;public void add(){count++;}}
public class Thread14 {public static void main(String[] args) throws InterruptedException {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("count = "+ counter.count);}
}
可以看到結(jié)果是不確定的
1.1 線程安全的概念
先來說一下非線程安全的概念:非線程安全主要是指多個線程對同一個對象中的同一個實例變量進(jìn)行操作時會出現(xiàn)值被更改、值不同步的情況,進(jìn)而影響程序的執(zhí)行流程。
則線程安全:如果多線程環(huán)境下代碼運(yùn)行的結(jié)果是符合我們預(yù)期的,即在單線程環(huán)境應(yīng)該的結(jié)果,則說這個程序是線程安全的。
1.2 線程不安全的原因
先解釋上述線代碼程不安全的原因:
如果兩個線程并發(fā)執(zhí)行count++,此時就相當(dāng)于兩組load add save進(jìn)行執(zhí)行,此時不同的線程調(diào)度順序就可能會產(chǎn)生一些結(jié)果上的差異
由于線程的搶占執(zhí)行,導(dǎo)致當(dāng)前執(zhí)行到任意一個指令,線程都可能bei調(diào)度走,CPU讓別的線程來執(zhí)行
如下圖:
導(dǎo)致下面的結(jié)果:
線程安全問題的原因:
1.搶占式執(zhí)行,隨機(jī)調(diào)度(根本原因)
2.代碼結(jié)構(gòu):多個線程同時修改同一個變量
3.原子性(操作是非原子性,容易出現(xiàn)問題)
4.內(nèi)存可見性問題(如一個線程讀,一個線程改)
5.指令重排序
1.3 解決線程不安全
從原子性入手,通過加鎖,把非原子的,轉(zhuǎn)成"原子"的
加了synchronized之后,進(jìn)入方法就會加鎖,出了方法就會解鎖,如果兩個線程同時嘗試加鎖,此時一個能獲取鎖成功,另一個只能阻塞等待(BLOCKED),一直阻塞到剛才的線程解鎖,當(dāng)前線程才能加鎖成功
二.synchronized-monitor lock(監(jiān)視器鎖)
2.1 synchronized的特性
(1)互斥
- 進(jìn)入sychronized修飾的代碼塊,相當(dāng)于加鎖
- 退出sychronizde修飾的代碼塊,相當(dāng)于解鎖
(2)刷新內(nèi)存
synchronized的工作過程:
1.獲得互斥鎖
2.從內(nèi)存拷貝變量的最新副本到工作的內(nèi)存
3.執(zhí)行代碼
4.將更改后的共享變量的值刷新到主內(nèi)存
5.釋放互斥鎖
(3)可重入
synchronized同步塊對同一條線程來說是可重入的,不會出現(xiàn)自己把自己鎖死的問題(自己可以再次獲取自己的內(nèi)部鎖)
理解"把自己鎖死"
一個線程沒有釋放鎖,然后又嘗試再次加鎖
按照之前對于鎖的設(shè)定,第二次加鎖的時候,就會阻塞等待,而獲取不到第一次的鎖,就把自己鎖死
2.2 synchronied使用方法
1.直接修飾普通方法:
鎖的SynchronizedDemo1對象
public class SynchronizedDemo1 {public synchronized void methond() {}
}
2.修飾靜態(tài)方法:
鎖SynchronizedDemo2對象
public class SynchronizedDemo2 {public synchronized void methond() {}
}
3.修飾代碼塊:
明確指定鎖哪個對象
public class SychronizedDemo{public void method(){sychronized(this){}}
}
鎖類對象
public class SynchronizedDemo {public void method() {synchronized (SynchronizedDemo.class) {}}
}
三.死鎖
3.1死鎖的情況
1.一個線程,連續(xù)加鎖兩次,如果鎖是不可重入鎖,就會死鎖
2.兩個線程,兩把鎖,t1和t2各自先針對鎖A和鎖B加鎖,在獲取對方的鎖
public class Thread15 {public static void main(String[] args) {Object lock1 = new Object();Object lock2= new Object();Thread t1 = new Thread(()->{synchronized (lock1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2){System.out.println("t1把鎖1和鎖2都獲得了");}}});Thread t2 = new Thread(()->{synchronized (lock2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1){System.out.println("t2把鎖1和鎖2都獲得了");}};});t1.start();t2.start();}
}
3.多個線程,多把鎖(相當(dāng)于2的一般情況)
3.2 死鎖的四個必要條件
1.互斥使用
線程1拿到了鎖,線程2就須等著
2.不可搶占
線程1拿到鎖A之后,必須是線程1主動釋放
3.請求和保持
線程1拿到鎖A之后,在嘗試獲取鎖B,A這把鎖還是保持的
4.循環(huán)等待
線程1嘗試獲取到鎖A和鎖B,線程2嘗試獲取鎖B和鎖A,線程1在獲取B的時候等待線程2釋放B,同時線程2 在獲取A的時候等待線程1釋放A
3.3解決死鎖的辦法
給鎖編號,然后指定一個固定的順序來加鎖,任意線程加把鎖,都讓線程遵守上述順序,此時循環(huán)等待自然破除
對于synchronied前三個條件都是鎖的基本特性,我們只能對四修改
public class Thread15 {public static void main(String[] args) {Object lock1 = new Object();Object lock2= new Object();Thread t1 = new Thread(()->{synchronized (lock1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2){System.out.println("t1把鎖1和鎖2都獲得了");}}});Thread t2 = new Thread(()->{synchronized (lock1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2){System.out.println("t2把鎖1和鎖2都獲得了");}};});t1.start();t2.start();}
}
四.volatile 關(guān)鍵字
volatile 和內(nèi)存可見性問題密切相關(guān)
一個線程針對一個變量進(jìn)行讀取操作,同時另一個線程針對這個變量進(jìn)行修改,此時讀取到值,不一定是修改之后的值(歸根結(jié)底是編譯器/jvm在多線程下優(yōu)化時產(chǎn)生了誤判)
使用匯編語言解釋
1.load,把內(nèi)存中flag的值,讀取到寄存器
2.cmp把寄存器的值和0進(jìn)行比較,根據(jù)比較結(jié)果,決定下一不執(zhí)行.
由于load執(zhí)行速度太慢(相比于cmp來說),再加上反復(fù)load的結(jié)果都一樣,JVM就不在重復(fù)load判定沒人改flag值,就只讀取一次就好
而給flag加上volatile關(guān)鍵字,告訴編譯器變量是"易變"的,不再進(jìn)行優(yōu)化
class MyCounter{volatile public int flag = 0;
}
public class Thread16 {public static void main(String[] args) {MyCounter myCounter = new MyCounter();Thread t1 = new Thread(() ->{while (myCounter.flag == 0){//循環(huán)體空著}System.out.println("t1循環(huán)結(jié)束");});Thread t2 = new Thread(() ->{Scanner scanner = new Scanner(System.in);System.out.println("請輸入一個整數(shù):");myCounter.flag = scanner.nextInt();});t1.start();t2.start();}
}
結(jié)果:
五. wait和notify
wait和notify可以協(xié)調(diào)線程之間的先后順序
完成這個協(xié)調(diào)工作, 主要涉及到三個方法
- wait() / wait(long timeout): 讓當(dāng)前線程進(jìn)入等待狀態(tài).
- notify() / notifyAll():喚醒在當(dāng)前對象上等待的線程.
注意: wait, notify, notifyAll 都是 Object 類的方法
5.1 wait()方法
wait的操作
1.先釋放鎖
2.在阻塞等待
3.收到通知之后,重新獲取鎖,并且在獲取鎖后,繼續(xù)往下執(zhí)行
wait操作需要搭配synchorized來使用
public class Thread17 {public static void main(String[] args) throws InterruptedException {Object object = new Object();System.out.println("wait之前");object.wait();System.out.println("wait之后");}
}
無synchorized的情況
wait無參數(shù)版本,就是死等
wait帶參數(shù)版本,指定了等待的最大時間
5.2 notify()方法
notify()方法是喚醒等待線程
如果有多個線程等待,則有線程調(diào)度器隨機(jī)挑選出一個呈 wait 狀態(tài)的線程。(并沒有 “先來后到”)
在notify()方法后,當(dāng)前線程不會馬上釋放該對象鎖,要等到執(zhí)行notify()方法的線程將程序執(zhí)行完,也就是退出同步代碼塊之后才會釋放對象鎖。
notfiyAll()方法可以一次喚醒所有的等待線程