JUC併發編程之:簡單概述(二)
##內容重點
·分析多線程訪問共享資源時,哪些代碼片段屬於臨界區
·使用synchronized互斥解決臨界區的線程安全問題
>synchronized鎖對象語法
>synchronized加載成員方法和靜態方法語法
>wait/notify同步方法
·使用ReentrantLock互斥解決臨界區的線程安全問題
>lock使用細節:可打斷、鎖超時、公平鎖、條件變量
·分析變量的線程安全性、常見線程安全類的使用
·線程活躍性:死鎖、活鎖、飢餓
##應用
·互斥:synchronized或ReentrantLock達到共享資源互斥效果
·同步:wait/notify或ReetrantLock的條件變量來達到線程間通信效果
##原理
·monitor、synchronized、wait/notify原理
·synchronized進階原理
·park & unpark 原理
##模式
·同步模式-保護性暫停
·同步模式-順序控制
·異步模式-生產者/消費者
一、共享帶來的問題
@Slf4j
public class ShareData {
private static int counter = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
counter++;
}
},"t1");
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
counter--;
}
},"t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("counter : {}",counter);
}
}
//上面結果不可預測
##臨界區:Critical Section
·一個程序運行多個線程本身是沒有問題的
·問題出在多個線程訪問共享資源
>多個線程 讀共享資源 其實也沒有問題
>多個線程 對共享資源讀寫操作室發生指令交錯 就會出現問題
·一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊爲【臨界區】
##竟態條件:Race Condition
·多個線程再臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之爲發生了
【竟態條件】
二、synchronized解決方案
2.1、初識synchronized
爲了避免臨界區的竟態條件發生,有多種手段可以達到目的:
·阻塞式的解決方案:synchronized lock
·非阻塞時的解決方案:原子變量
synchronized即對象鎖,它採用互斥的方式讓同一時刻之多隻有一個線程能持有對象鎖,其它
線程再想獲取這個對象鎖時就會阻塞住。這樣就能保證擁有鎖的線程可以安全的執行臨界區內的
代碼,不用擔心線程上線文切換。
##語法:
synchronized(對象){
//臨界區
}
·synchronized實際是用對象鎖保證了臨界區內代碼的原子性,臨界區內的代碼對外不可分割,
不會被線程切換所打斷。
·【sleep是不會讓出鎖的】
解決共享問題:
@Slf4j
public class ShareDataSynchronized {
private static int counter = 0;
private static Object obj = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
synchronized (obj){
counter++;
}
}
},"t1");
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
synchronized (obj){
counter--;
}
}
},"t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("counter : {}",counter);
}
}
##思考:
·如果把synchronized(obj)放在for循環外面,如何理解?
·如果t1 synchronized(obj1)而t2 synchronized(obj2)會怎樣運作?
·如果t1 synchronized(obj)而t2沒有加會怎樣?
鎖對象面向對象改進:
/**
* 上面的代碼時面向過程的,如果面向對象如何改進?
**/
@Slf4j
public class ShareDataObjSynchronized {
public static void main(String[] args) {
Room room = new Room();
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
room.increment();
}
},"t1");
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
room.decrement();
}
},"t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("counter : {}",room.getCounter());
}
}
class Room{
private int counter = 0;
public void increment(){
synchronized (this){
counter++;
}
}
public void decrement(){
synchronized (this){
counter--;
}
}
public int getCounter(){
return counter;
}
}
2.2、方法上的synchronized
##語法:
//1、普通方法
public synchronized void test(){
}
//等價於
public void test(){
synchronized(this){
}
}
//靜態方法:因爲靜態方法不需要靠對象獲取,因此鎖是類對象
public synchronized static void test(){
}
//等價於
public static void test(){
synchronized(當前類.class){
}
}
三、線程安全分析
3.1、變量的線程安全分析
##成員變量 和 靜態變量 是否線程安全
·如果它們沒有共享,則線程安全
·如果它們被共享了,根據他們的狀態是否改變,又分兩種情況:
>如果只有讀操作,則線程安全
>如果有讀寫操作,則代碼是臨界區,需要考慮線程安全
##局部變量是否線程安全
·局部變量是線程安全的
·但局部變量引用的對象則未必
>如果該對象沒有逃離方法的作用範圍,它是線程安全的
>如果該對象逃離方法的作用範圍,需要考慮線程安全
3.2、常見的線程安全類
·String (不可變類)
·Integer (不可變類)
·StringBuffer
·Random
·Vector
·Hashtable
·java.util.concurrent包下的類
四、Monitor
類型 | byte | bit | 取值範圍 |
---|---|---|---|
byte | 1 | 8位 | -2^7 ~2^7 -1 |
short | 2 | 16位 | -2^15~2^15-1 |
int | 4 | 32位 | -2^31~2^31-1 |
long | 8 | 64位 | -2^63~2^63-1 |
float | 4 | 32位 | -2^31~2^31-1 |
double | 8 | 64位 | -2^63~2^63-1 |
char | 1 | 8位 | -2^7 ~2^7 -1 |
boolean | 1 | 8位 | -2^7 ~2^7 -1 |
Java對象頭:
Minitor被翻譯爲監視器或者管程
每個Java對象都可以關聯一個Monitor對象,如果使用synchronized給對象上鎖(重量級)之後
該對象的mark word中就被設置指向monitor對象的指針
##Monitor工作原理:
·剛開始Monitor中Owner爲NULL
·當Thread-2執行synchronized(obj)就會將Monitor的所有者Owner置爲Thread-2,
Monitor中只能有一個Owner
·在Thread-2上鎖的過程中,如果Thread-3和Thread-4也來執行synchronized(obj),
就會進入EntryList[Bolocked狀態]
·Thread-2執行完同步代碼塊的內容,然後喚醒EntryList中等待的線程來競爭鎖,競爭是
非公平的
·圖中WaitSet中的Thread-0和Thread-1是之前獲得過鎖,但條件不滿足進入waiting
狀態的線程,之後在waiting-notify時分析
##注意:
·synchronized必須是進入同一個對象的monitor纔有上訴的效果
·不加synchronized的對象不會關聯監視器,不遵從以上規則
[每個Obj關聯一個Monitor]
五、synchronized優化原理
5.1、輕量級鎖
##輕量級鎖使用場景:
如果一個對象雖然有很多線程訪問,但多線程訪問的時間是錯開的(也就是沒有競爭關係)
那麼可以使用輕量級鎖來優化
[如果輕量級鎖失敗會自動升級爲重量級鎖]
##輕量級鎖對使用者是透明的,語法仍然是synchronized
/**
*例:假設有兩個方法同步塊,利用同一個對象鎖
**/
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步塊A
method2();
}
}
public static method2(){
synchronized(obj){
//同步塊B
}
}
分析一:創建鎖記錄對象(Lock Record)
每個線程的棧幀都會包含一個鎖記錄的結構,內部可以存儲鎖定對象的Mark Word
分析二:讓鎖記錄中Object reference指向鎖對象,
並嘗試cas替換Object對象中的
Mark Word,將Mark Word的值存入鎖記錄
分析三:如果cas替換成功,對象頭中存儲了 鎖記錄地址和狀態00,
表示由該線程給對象加鎖
分析四:如果cas失敗,有兩種情況
>其他線程已經持有了該Object的輕量級鎖,這是表明有競爭,進入 鎖膨脹過程
>自己執行了 synchronized鎖重入(如上method1加鎖後 又調用method2加了同樣的鎖)
那麼需要在棧幀中再添加一條Lock Record作爲重入的計數
分析五:當退出synchronized代碼塊(解鎖時)
>如果有取值爲null的鎖記錄,表示有重入,這是重置鎖記錄,表示重入計數減一
>鎖記錄的值不爲null,這是使用cas將 Mark Word的值恢復給對象頭
·成功,則解鎖成功
·失敗,說明輕量級鎖進行了鎖膨脹或已經升級爲重量級鎖,進入重量級鎖解鎖流程
5.2、鎖膨脹
如果在嘗試加輕量級鎖的過程中,CAS操作無法成功,這是一種情況就是已經有其他線程爲此
對象加上了輕量級鎖(有競爭),這是需要進行 鎖膨脹,將輕量級鎖變爲 【重量級鎖】
(對應上面的 分析四)
/**
* 例:鎖膨脹
**/
static object obj = new Object();
public static void method1(){
synchronized(obj){
//同步塊
}
}
##分析一:當Thread-1進行輕量級加鎖時,Thread-0已經對該對象加了輕量級鎖
##分析二:這時Thread-1加輕量級鎖失敗,進入鎖膨脹流程
>即爲Object對象申請Monitor鎖,讓Object指向重量級鎖地址
>然後自己進入Monitor的EntryList BLOCKED
##分析三:當Thread-0退出同步塊解鎖時,使用cas將Mark Word的值恢復給對象頭失敗
。這時會進入重量級解鎖流程,即按照Monitor地址找到Monitor對象設置Owner爲NULL
,喚醒EntryList中BOLOCKED線程
5.3、自旋優化
·【重量級鎖】競爭的時候,還可以使用自旋來進行優化,如果當前線程自旋成功(即這時候
持鎖線程已經推出了同步塊,釋放了鎖),這時當前線程就可以避免阻塞
{即:線程在進入阻塞狀態之前會進行幾次自旋獲取Monitor的嘗試,如果獲取Monitor成
功,則表示獲取鎖成功,如果嘗試幾次後仍是失敗則進入阻塞狀態}
·JDK6之後自旋鎖是自適應的,比如對象剛剛的一次自旋操作成功過,那麼認爲這次自旋成功
的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋
·自旋會佔用CPU時間片,單核CPU自旋就是浪費,多核CPU自旋才能發揮優勢
·JDK7之後不能控制是否開啓自旋功能
5.4、偏向鎖
·輕量級鎖在沒有競爭時(就自己這個線程),每次鎖重入仍然需要執行CAS操作。
·JDK6之後引入偏向鎖來進一步優化:只有第一次使用CAS將線程ID設置到對象頭的Mark
Word,之後發現這個線程ID是自己的就表示沒有競爭,不用重新CAS,以後只要不發生競
爭,這個對象就歸該線程所有
[輕量級鎖是將Lock Record(鎖記錄地址)存入對象頭的Mark Word]
[偏向鎖是將線程ID存入對象頭的Mark Word]
static final Object obj = new Object();
public static void m1(){
synchronized(obj){
//同步塊A
m2();
}
}
public static m2(){
synchronized(obj){
//同步塊B
m3();
}
}
public static m3(){
synchronized(obj){
//同步塊C
}
}
偏向狀態:
對象頭格式
一、##一個對象創建時:
·如果開啓了偏向鎖(默認開啓),那麼對象創建後,MarkWord最後三位爲101,這時它
的thread、epoch、age都爲0
·偏向鎖是默認延遲的,不會在程序啓動時立即生效,如果想避免延遲,可以加VM參數:
-XX:BiasedLockingStartupDelay=0來禁用延遲
·如果沒有開啓偏向鎖,那麼對象創建後,MarkWord後三位爲001,這時它的hashcode,
age都爲0,第一次用到還是從的時纔會賦值
BiasedLocking (-禁用 +開啓,默認開啓)
二、##撤銷偏向鎖狀態:
1、調用對象hashCode
調用了對象的hashCode,但偏向鎖的對象MarkWord中存儲的是Thread Id,如果調用
hashCode會導致偏向鎖被撤銷(成爲Normal)
·輕量級所會在鎖記錄Lock Record中記錄hashCode
·重量級鎖會在Monitor中記錄hashCode
在調用hashCode後使用偏向鎖,記得去掉-XX:-UseBiasedLocking
2、其他線程使用該對象
當有其他線程使用偏向鎖對象時,會將偏向鎖升級爲輕量級鎖
3、調用wait/notify
wait/notify只有重量級鎖纔有,即使用wait/notify時會自動升級爲重量級鎖
三、批量重偏向
如果對象雖然被多個線程訪問,但沒有競爭,這時偏向了線程T1的對象仍有機會重新偏
向T2,重偏向會重置對象的Thread Id
當撤銷偏向鎖閾值超過20次後,JVM會這樣覺得:我是不是偏向錯了,於是會在給這些
對象加鎖時重新偏向至加鎖線程
四、批量撤銷
當撤銷偏向鎖閾值超過40次後,jvm就會覺得自己確實偏向錯了,根本就不該偏向。
於是整個類的所有對象都會變爲不可偏向的,新建的對象也是不可偏向的
六、wait和notify
##上圖解析:
·WaitSet中的等待線程被notify喚醒後,重新進入EntryList隊列等待
·線程在Owner狀態調用wait方法進入WaitSet狀態,WaitSet狀態的線程調用
notify後進入EntryList狀態
##一、原理之wait-notify:
·Owner線程發現條件不滿足,調用wait方法,即可進入WaitSet變爲WAITING狀態
·BLOCKED和WAITING的線程都處於阻塞狀態,不佔用CPU時間片
·BLOCKED線程會在Owner線程釋放鎖時喚醒
·WAITING線程會在Owner線程調用notify或notifyAll時喚醒,但喚醒後並不意
味着立刻獲得鎖,仍需要進入EntryList重新競爭
##二、API介紹:
·obj.wait()讓進入object監視器的線程到waitset等待
·obj.wait(long timeout)讓進入object監視器的線程到waitset等待,如果時
間結束沒有被喚醒,則自動結束等待
·obj.notify()在object上正在waitset等待的線程中挑一個喚醒
·obj.notifyAll()讓object上正在waitset等待的線程全部喚醒
它們都是線程之間進行協作的手段,都屬於Object對象的方法,必須獲得此對象的鎖
才能調用這幾個方法
@Slf4j
public class WaitNofifyThread {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (obj){
log.info("執行代碼...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("其他代碼...");
}
},"t1").start();
new Thread(()->{
synchronized (obj){
log.info("執行代碼...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("其他代碼...");
}
},"t2").start();
Thread.sleep(2000);
log.info("喚醒obj上其他線程");
synchronized (obj){
obj.notify();//喚醒waitset中的一個線程
// obj.notifyAll();//喚醒waitset中的所有線程
}
}
}
##三、wait(long n)和sleep(long n)的區別
·sleep是Thread靜態方法,wait是Object方法
·sleep不需要強制和synchronized配合使用,但wait需要和synchronized一起用
·sleep在睡眠的同時,不會釋放對象鎖;但wait在等待的時候會釋放對象鎖
·相同點:它們的狀態都是TIMED_WAITING
##四、wait和notify正確使用姿勢
eg:六個人要同一間屋子一起幹活(同一時間只能一個人),其中小南必須有煙才能幹,
沒煙他就罷工
/**
* 六個人要同一間屋子一起幹活(同一時間只能一個人),其中小南必須有煙才能幹,
* 沒煙他就罷工
*/
@Slf4j
public class WaitNotifyUseThread1 {
static final Object room = new Object();//屋子
static boolean hasCigarette = false;//是否有煙
public static void main(String[] args) throws InterruptedException {
//小南
new Thread(()->{
synchronized (room){
log.info("有煙沒:[{}]",hasCigarette);
if(!hasCigarette){
log.info("沒煙,歇會兒!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("現在有煙了沒[{}]",hasCigarette);
if(hasCigarette){
log.info("可以開始幹活了");
}
}
},"小南").start();
//其他五個人
for (int i=0;i<5;i++){
new Thread(()->{
synchronized (room){
log.info("可以開始幹活了!");
}
},"其他人").start();
}
//主線程休息1秒
Thread.sleep(1000);
//送煙的
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.info("煙到了");
}
},"送煙的").start();
}
}
##結果:
16:35:44.651 [小南] INFO xxxxxxxxx - 有煙沒:[false]
16:35:44.666 [小南] INFO xxxxxxxxx - 沒煙,歇會兒!
16:35:47.667 [小南] INFO xxxxxxxxx - 現在有煙了沒:[false]
16:35:47.667 [送煙的] INFO xxxxxxxxx - 煙到了
16:35:47.667 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:35:47.668 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:35:47.668 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:35:47.669 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:35:47.669 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
##問題:
·小南使用sleep(),導致其他幹活的線程一直阻塞,效率太低
·小南線程必須滿足3s後才能醒來,就算煙提前送到了,也無法立刻醒來
·因爲小南佔用線程,送煙的線程也進不去
(sleep可以用interrupt方法叫醒)
@Slf4j
public class WaitNotifyUseThread2 {
static final Object room = new Object();//屋子
static boolean hasCigarette = false;//是否有煙
public static void main(String[] args) throws InterruptedException {
//小南
new Thread(()->{
synchronized (room){
log.info("有眼沒:[{}]",hasCigarette);
if(!hasCigarette){
log.info("沒煙,歇會兒!");
try {
room.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("現在有煙了沒[{}]",hasCigarette);
if(hasCigarette){
log.info("可以開始幹活了");
}
}
},"小南").start();
//其他五個人
for (int i=0;i<5;i++){
new Thread(()->{
synchronized (room){
log.info("可以開始幹活了!");
}
},"其他人").start();
}
//主線程休息1秒
Thread.sleep(1000);
//送煙的
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.info("煙到了");
room.notify();
}
},"送煙的").start();
}
}
##結果:
16:45:43.598 [小南] INFO xxxxxxxxx - 有眼沒:[false]
16:45:43.602 [小南] INFO xxxxxxxxx - 沒煙,歇會兒!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以開始幹活了!
16:45:44.598 [送煙的] INFO xxxxxxxxx - 煙到了
16:45:44.598 [小南] INFO xxxxxxxxx - 現在有煙了沒[true]
16:45:44.598 [小南] INFO xxxxxxxxx - 可以開始幹活了
##問題:
·room.notify叫醒了小南,但如果有其他線程也在等待條件呢?
/**
* 小南和小女在一間屋子幹活,小南沒煙罷工,小女沒外賣罷工
*/
@Slf4j
public class WaitNotifyUseThread3 {
static final Object room = new Object();//屋子
static boolean hasCigarette = false;//是否有煙
static boolean hasTakeOut = false;//是否有外賣
public static void main(String[] args) {
//小南
new Thread(()->{
synchronized (room){
log.info("有煙沒:[{}]",hasCigarette);
if(!hasCigarette){
log.info("沒煙,歇會兒!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("現在有煙了沒[{}]",hasCigarette);
if(hasCigarette){
log.info("可以開始幹活了");
}else{
log.info("今天活是幹不成嘍");
}
}
},"小南").start();
//小女
new Thread(()->{
synchronized (room){
log.info("有外賣沒:[{}]",hasTakeOut);
if(!hasTakeOut){
log.info("沒外賣,歇會兒!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("現在有外賣了沒[{}]",hasTakeOut);
if(hasTakeOut){
log.info("可以開始幹活了");
}else{
log.info("今天活是幹不成嘍");
}
}
},"小女").start();
//主線程休息1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//送外賣的到了
new Thread(()->{
synchronized (room){
hasTakeOut = true;
log.info("外賣到了");
room.notify();
}
},"送外賣的").start();
//送煙的到了
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.info("煙到了");
room.notify();
}
},"送煙的").start();
}
}
##結果:
17:12:04.726 [小南] INFO xxxxxxxxx - 有煙沒:[false]
17:12:04.729 [小南] INFO xxxxxxxxx - 沒煙,歇會兒!
17:12:04.729 [小女] INFO xxxxxxxxx - 有外賣沒:[false]
17:12:04.729 [小女] INFO xxxxxxxxx - 沒外賣,歇會兒!
17:12:05.725 [送外賣的] INFO xxxxxxxxx - 外賣到了
17:12:05.726 [小南] INFO xxxxxxxxx - 現在有煙了沒[false]
17:12:05.726 [小南] INFO xxxxxxxxx - 今天活是幹不成嘍
17:12:05.726 [送煙的] INFO xxxxxxxxx - 煙到了
17:12:05.726 [小女] INFO xxxxxxxxx - 現在有外賣了沒[true]
17:12:05.726 [小女] INFO xxxxxxxxx - 可以開始幹活了
##問題
·room.notify是隨機喚醒,本來應該喚醒小女的,但喚醒了小南
/**
* 小南和小女在一間屋子幹活,小南沒煙罷工,小女沒外賣罷工
*/
@Slf4j
public class WaitNotifyUseThread4 {
static final Object room = new Object();//屋子
static boolean hasCigarette = false;//是否有煙
static boolean hasTakeOut = false;//是否有外賣
public static void main(String[] args) {
//小南
new Thread(()->{
synchronized (room){
log.info("有煙沒:[{}]",hasCigarette);
while(true){
if(!hasCigarette){
log.info("沒煙,歇會兒!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
log.info("可以開始幹活了");
break;
}
}
}
},"小南").start();
//小女
new Thread(()->{
synchronized (room){
log.info("有外賣沒:[{}]",hasTakeOut);
while(true){
if(!hasTakeOut){
log.info("沒外賣,歇會兒!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
log.info("可以開始幹活了");
break;
}
}
}
},"小女").start();
//主線程休息1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//送外賣的到了
new Thread(()->{
synchronized (room){
hasTakeOut = true;
log.info("外賣到了");
room.notifyAll();
}
},"送外賣的").start();
//送煙的到了
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.info("煙到了");
room.notifyAll();
}
},"送煙的").start();
}
}
##結果:
17:21:00.640 [小南] INFO xxxxxxxxx - 有煙沒:[false]
17:21:00.643 [小南] INFO xxxxxxxxx - 沒煙,歇會兒!
17:21:00.643 [小女] INFO xxxxxxxxx - 有外賣沒:[false]
17:21:00.643 [小女] INFO xxxxxxxxx - 沒外賣,歇會兒!
17:21:01.638 [送外賣的] INFO xxxxxxxxx - 外賣到了
17:21:01.638 [小女] INFO xxxxxxxxx - 可以開始幹活了
17:21:01.639 [小南] INFO xxxxxxxxx - 沒煙,歇會兒!
17:21:01.639 [送煙的] INFO xxxxxxxxx - 煙到了
17:21:01.639 [小南] INFO xxxxxxxxx - 可以開始幹活了
七、練習:保護性暫停
7.1、保護性暫停一
##一、定義:
·即Guarded Suspension,用在一個線程等待另一個線程的執行結果
要點:
·有一個結果需要從一個線程傳遞到另一個線程,讓他們關聯同一個GuardedObject
·如果有結果源源不斷不斷從一個線程到另一個線程那麼可以使用消息隊列(見生產者/消費者)
·JDK中,join的實現、Future的實現,採用的就是此模式
·因爲要等待另一方的結果,因此歸類到同步模式
/**
* 保護性暫停模式:
* 線程1等待線程2的結果
*/
@Slf4j
public class GuardedObjectThread1 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
//等待結果線程
new Thread(()->{
log.info("等待結果...");
String resp = (String) guardedObject.get();
log.info("結果爲:{}",resp);
},"t1").start();
//生產結果線程
new Thread(()->{
log.info("執行生產...");
guardedObject.produce("生產了一顆芒果");
try {
log.info("剛生忙了一會休息休息");
Thread.sleep(3000);
log.info("休息好了");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}
class GuardedObject{
//結果
private Object response;
//獲取結果
public Object get(){
synchronized (this){
while(response==null){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return response;
}
//生產結果
public void produce(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
}
##結果:
10:47:03.020 [t1] INFO xxxxxxx - 等待結果...
10:47:03.020 [t2] INFO xxxxxxx - 執行生產...
10:47:03.023 [t2] INFO xxxxxxx - 剛生忙了一會休息休息
10:47:03.023 [t1] INFO xxxxxxx - 結果爲:生產了一顆芒果
10:47:06.025 [t2] INFO xxxxxxx - 休息好了
##分析:
·join等待結果 需要t2線程全部運行結束纔可,保護性暫停模式不需要,結果產生即可
·join等待結果的變量需要設計成全局的,保護性暫停模式可以設計成局部的
##擴展-增加等待超時
/**
* 保護性暫停模式:
* 線程1等待線程2的結果
* 擴展:增加等待超時
*/
@Slf4j
public class GuardedObjectThread2 {
public static void main(String[] args) {
GuardedObject2 guardedObject = new GuardedObject2();
//等待結果線程
new Thread(()->{
log.info("等待結果...");
String resp = (String) guardedObject.get(2000);
log.info("結果爲:{}",resp);
},"t1").start();
//生產結果線程
new Thread(()->{
log.info("執行生產...");
try {
log.info("先休息一會再忙");
Thread.sleep(3000);
guardedObject.produce("生產了一顆芒果");
log.info("休息好了");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}
class GuardedObject2{
//結果
private Object response;
//獲取結果
public Object get(long timeout){
synchronized (this){
//開始時間
long beginTime = System.currentTimeMillis();
//等待的時間
long passedTime = 0;
while(response==null){
if(passedTime>=timeout){
break;
}
try {
this.wait(timeout-passedTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - beginTime;
}
}
return response;
}
//生產結果
public void produce(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
}
##結果:
10:14:36.587 [t1] INFO xxxxxx - 等待結果...
10:14:36.587 [t2] INFO xxxxxx - 執行生產...
10:14:36.590 [t2] INFO xxxxxx - 先休息一會再忙
10:14:38.590 [t1] INFO xxxxxx - 結果爲:null
10:14:39.591 [t2] INFO xxxxxx - 休息好了
##分析:
t1不再一直再等待t2線程的結果了
7.2、join的原理
·保護性暫停模式:一個線程等待另一個線程的結果
·join:一個線程等待另一個線程結束後再繼續運行
//join源碼:底層即保護性暫停模式
public final synchronized void join(long millis){
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
7.3、保護性暫停二
·圖中Futrues就好比郵箱,左側T0 T2 T4好比收件人,右側T1 T3 T5好比郵遞員
·如果需要在多個類之間使用GuardedObject對象,作爲參數傳遞不是很方便,因此設計一
各用來解耦的中間類,這樣不僅能夠解耦【生產者】和【消費者】,還能夠同時支持多個任務
的管理
@Slf4j
public class GuardedObjectThread3 {
public static void main(String[] args) throws InterruptedException {
for (int i=0;i<3;i++){
new Consignee().start();
}
Thread.sleep(1000);
for (int id : MailBox.getIds()){
new Postman(id,"內容"+id).start();
}
}
}
//收件人
@Slf4j
class Consignee extends Thread{
@Override
public void run() {
//收信--我收信的信件ID是xxx
GurdedObject3 go = MailBox.createGurdedObject();
log.info("開始收信 id : {}",go.getId());
Object mail = go.get(5000);
log.info("收到信 id: {},內容:{}",go.getId(),mail);
}
}
//郵遞員
@Slf4j
class Postman extends Thread{
private int id;
private String mail;
public Postman(int id,String mail){
this.id = id;
this.mail = mail;
}
@Override
public void run() {
//送信--我送信的信件ID是xxx
GurdedObject3 go = MailBox.getGurdedObject(id);
log.info("送信 id: {},內容:{}",go.getId(),mail);
go.produce(mail);
}
}
//郵箱
class MailBox{
//int爲 gurdedObject id,hashtable是線程安全的
private static Map<Integer,GurdedObject3> boxes = new Hashtable<>();
//生產唯一ID
private static int id = 1;
private static synchronized int generatorId(){
return id++;
}
public static GurdedObject3 createGurdedObject(){
//創建信件
GurdedObject3 go = new GurdedObject3(generatorId());
//放入信箱
boxes.put(go.getId(),go);
return go;
}
public static GurdedObject3 getGurdedObject(int id){
//收到信件後移除從信箱中移除信件
return boxes.remove(id);
}
public static Set<Integer> getIds(){
return boxes.keySet();
}
}
//郵件
@Data
class GurdedObject3{
//唯一標識
private int id;
//結果
private Object response;
//獲取結果
public Object get(long timeout){
synchronized (this){
//開始時間
long beginTime = System.currentTimeMillis();
//等待的時間
long passedTime = 0;
while(response==null){
if(passedTime>=timeout){
break;
}
try {
this.wait(timeout-passedTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - beginTime;
}
}
return response;
}
//生產結果
public void produce(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
//構造函數
public GurdedObject3(int id){
this.id = id;
}
}
11:30:54.052 [Thread-1] INFO xxxxxx - 開始收信 id : 1
11:30:54.052 [Thread-2] INFO xxxxxx - 開始收信 id : 3
11:30:54.052 [Thread-0] INFO xxxxxx - 開始收信 id : 2
11:30:55.051 [Thread-5] INFO xxxxxx - 送信 id: 1,內容:內容1
11:30:55.051 [Thread-3] INFO xxxxxx - 送信 id: 3,內容:內容3
11:30:55.051 [Thread-1] INFO xxxxxx - 收到信 id: 1,內容:內容1
11:30:55.051 [Thread-4] INFO xxxxxx - 送信 id: 2,內容:內容2
11:30:55.051 [Thread-2] INFO xxxxxx - 收到信 id: 3,內容:內容3
11:30:55.051 [Thread-0] INFO xxxxxx - 收到信 id: 2,內容:內容2
八、練習:生產者-消費者
·與之前的保護性暫停中GuardedObject不同,不需要生產結果和消費結果線程一一對應
·消費隊列可以用來平和生產和消費的線程資源
·生產者僅負責生產結果數據,不關心數據該如何處理,而消費者專心處理結果數據
·消息隊列是有容量限制的,滿時不再加入數據,空時不再消費數據
【JDK中各種阻塞隊列,採用的就是這種模式】
/**
* 生產者-消費者模式
*/
@Slf4j
public class ProducerAndConsumerThread {
public static void main(String[] args) {
MessageQueue mq = new MessageQueue(2);
//生產者
for (int i = 0; i <3 ; i++) {
int id = i;
new Thread(()->{
mq.put(new Message(id,"值"+id));
},"生產者"+i).start();
}
//消費者
new Thread(()->{
while(true){
try {
Thread.sleep(1000);
Message take = mq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消費者").start();
}
}
//消息隊列,java線程間通信
@Slf4j
class MessageQueue{
//消息的隊列集合
private LinkedList<Message> list = new LinkedList<>();
//隊列容量
private int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
//獲取消息
public Message take(){
synchronized (list){
while(list.isEmpty()){
try {
log.info("隊列爲空,消費者等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//將消息從隊頭移除
Message message = list.removeFirst();
log.info("已消費消息 {}",message);
list.notifyAll();
return message;
}
}
//添加消息
public void put(Message message){
synchronized (list){
while(list.size()==capcity){
try {
log.info("隊列已滿,生產者等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//將消息添加到隊尾
list.addLast(message);
log.info("已生產消息 {}",message);
list.notifyAll();
}
}
}
//消息
@Data
@ToString
@AllArgsConstructor
final class Message{
private int id;
private Object value;
}
##結果:
14:33:01.590 [生產者2] INFO xxx - 已生產消息 Message(id=2, value=值2)
14:33:01.594 [生產者0] INFO xxx - 已生產消息 Message(id=0, value=值0)
14:33:01.594 [生產者1] INFO xxx - 隊列已滿,生產者等待
14:33:02.589 [消費者] INFO xxx - 已消費消息 Message(id=2, value=值2)
14:33:02.589 [生產者1] INFO xxx - 已生產消息 Message(id=1, value=值1)
14:33:03.589 [消費者] INFO xxx - 已消費消息 Message(id=0, value=值0)
14:33:04.589 [消費者] INFO xxx - 已消費消息 Message(id=1, value=值1)
14:33:05.590 [消費者] INFO xxx - 隊列爲空,消費者等待
九、park & unpark
##一、基本使用
·它們是LockSupport類中的方法:
> LockSupport.park(); //暫停當前線程(狀態爲WAIT)
> LockSupport.unpark(暫停線程對象); //恢復某個線程的運行
##注意
·unpark既可以在park之後調用,也可以在park之前調用
·在park之前調用,將來可以恢復park的線程(提前預約恢復了)
·多次調用unpark,僅會解鎖一次park線程
##特點
>與Object的wait和notify相比:
·wait notify和notifyAll必須配合Object monitor一起使用,而unpark不必
·park和unpark是以線程爲單位來【阻塞】和【喚醒】線程,而notify只是隨機喚醒
一個等待線程,notifyAll是喚醒所有等待線程,就不那麼【精確】
·park和unpark可以先unpark,而wait和notify不能先notify
@Slf4j
public class ParkAndUnpark {
public static void main(String[] args){
Thread t1 = new Thread(()->{
log.info("start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("park...");
LockSupport.park();
log.info("resume...");
},"t1");
t1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("unpark");
LockSupport.unpark(t1);
}
}
##結果:
15:01:54.152 [t1] INFO xxx - start...
15:01:55.155 [t1] INFO xxx - park...
15:01:59.152 [main] INFO xxx - unpark
15:01:59.152 [t1] INFO xxx - resume...
@Slf4j
public class ParkAndUnpark {
public static void main(String[] args){
Thread t1 = new Thread(()->{
log.info("start...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("park...");
LockSupport.park();
log.info("resume...");
},"t1");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("unpark");
LockSupport.unpark(t1);
}
}
##結果:
15:03:09.448 [t1] INFO xxx - start...
15:03:10.447 [main] INFO xxx - unpark
15:03:12.451 [t1] INFO xxx - park...
15:03:12.451 [t1] INFO xxx - resume...
##結論:
unpark可以提前解鎖park線程
##二、原理之 park & unpark
每個線程都有自己的一個Parker對象,由三部分組成_counter _cond和_mutex
·線程就像一個旅人,Parker就像它隨身攜帶的揹包,條件變量就好比揹包中的帳篷.
_counter就好比揹包中的備用乾糧(0爲耗盡1爲充足)
·調用park就是要看需不需要停下來歇息
>如果備用乾糧耗盡,那麼鑽進帳篷歇息
>如果備用乾糧充足,那麼不需要停留,繼續前進
·調用unpark,就好比令乾糧充足
>如果這時線程還在帳篷,就喚醒讓他繼續前進
>如果這時線程還在運行,那麼下次它調用park時,不需要停留繼續前進
>>因爲揹包空間優先,多次調用unpark金輝補充一份備用乾糧
1> 當前線程調用unsafe.park()方法
2> 檢查_counter,本情況爲0,這是,獲得_mutex互斥鎖
3> 線程進入_cond條件變量阻塞
4> 設置_counter=0
1> 調用unsafe.unpark(Thread-0)方法,設置_counter爲1
2> 喚醒_cond條件變量中的Thread-0
3> Thread_0恢復運行
4> 設置_counter爲0
十、線程狀態轉換
##一、new - > runnable
·當調用t.start()方法時,由new->runnable
##二、runnable -> waiting -> runnable
·(1)、t線程調用synchronized(obj)獲取了對象鎖後
>調用obj.wait()方法時,t線程從runnable->waiting
>調用obj.notify()或notifyAll()或t.interrupt()時
>>競爭鎖成功:t線程從waiting->runnable
>>競爭鎖失敗:t線程從waiting->blocked
·(2)、當前線程調用t.join()方法時,當前線程從runnable到waiting
>注意是當前線程在t線程對象的監視器上等待
>t線程運行結束,或調用了當前線程的interrupt()時,當前線程從waiting到runnable
·(3)、當前線程調用LockSupport.park()方法會讓當前線程從runnable到waiting
>調用LockSupport.unpark()或調用了線程的interrupt(),會讓目標線程從
waiting到runnable
##三、runnable -> timed_waiting -> runnable
·(1)、t線程用synchronized(obj)獲取了對象鎖後
>調用obj.wait(long n)方法時,t線程從runnable到timed_waiting
>t線程等待時間超過了n毫秒,或調用obj.notify()或notifyAll()或interrupt()時
>>競爭鎖成功:t線程從timed_waiting->runnable
>>競爭鎖失敗:t線程從timed_waiting->blocked
·(2)、當前線程調用t.join(long n)方法時,當前線程從runnable到timed_waiting
>注意是當前線程在t線程對象的監視器上等待
>當前線程等待超過了n還秒,或t線程運行結束,或調用了當前線程的interrupt()時,當前
線程從timed_waiting到runnable
·(3)當前線程調用Thread.sleep(long n),當前線程從runnable到timed_waiting
>當前線程等待時間超過了n毫秒,當前線程從timed_waiting到runnable
·(4)當前線程調用LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)時,
當前線程從runnable到timed_waiting
>調用LockSupport.unpark(目標線程)或調用了線程的interrupt()或是等待超時
會讓目標線程從timed_waiting到runnable
##四、runnable -> blocked -> runnable
·t線程用synchronized(obj)獲取了對象鎖時如果競爭失敗,從runnable到blocked
·持obj鎖線程的同步代碼塊執行完畢,會喚醒該對象上所有blocked的線程重新競爭,如果
其中t線程競爭成功從blocked到runnable,其他失敗的線程仍然blocked
##五、runnable -> terminated
當前線程所有代碼運行完畢,進入terminated
十一、鎖活躍性
11.1、多把不相干的鎖
一間大屋子有兩個功能:睡覺、學習,兩者互不相干:
現小南學習,小女睡覺,但如果用一間屋子(一個對象鎖)的話,那麼併發度會很低
解決方法時準備多個房間(多個對象鎖)
@Slf4j
public class BigRoomThread {
public static void main(String[] args) {
BigRoom br = new BigRoom();
new Thread(()->{
try {
br.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小南").start();
new Thread(()->{
try {
br.study();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小女").start();
}
}
@Slf4j
class BigRoom{
public void sleep() throws InterruptedException {
synchronized (this){
log.info("sleep 2個小時");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (this){
log.info("study 1個小時");
Thread.sleep(1000);
}
}
}
##結果:
16:25:41.620 [小南] INFO xxx - sleep 2個小時
16:25:43.623 [小女] INFO xxx - study 1個小時
study是在sleep執行後2秒纔開始執行的,效率低下
##多把鎖的情況:
@Slf4j
public class MultiRoomThread {
public static void main(String[] args) {
BigRoom2 br = new BigRoom2();
new Thread(()->{
try {
br.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小南").start();
new Thread(()->{
try {
br.study();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小女").start();
}
}
@Slf4j
class BigRoom2{
private final Object sleepRoom = new Object();
private final Object studyRoom = new Object();
public void sleep() throws InterruptedException {
synchronized (sleepRoom){
log.info("sleep 2個小時");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (studyRoom){
log.info("study 1個小時");
Thread.sleep(1000);
}
}
}
##結果:
16:30:18.981 [小女] INFO xxx - study 1個小時
16:30:18.981 [小南] INFO xxx - sleep 2個小時
sleep和study同時執行
##分析:
兩個sleep和study業務毫無關聯的情況下,將鎖的粒度細分
·好處,是可以增強併發度
·壞處,如果一個線程需要同時獲得多把鎖,就容易發生死鎖
11.2、死鎖
t1線程獲得A對象鎖,接下來想獲取B對象的鎖
t2線程獲得B對象鎖,接下來想獲取A對象的鎖
@Slf4j
public class DeadLockThread {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
new Thread(()->{
synchronized (A){
log.info("lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B){
log.info("lock B");
log.info("操作.....");
}
}
},"A").start();
new Thread(()->{
synchronized (B){
log.info("lock B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A){
log.info("lock A");
log.info("操作.....");
}
}
},"B").start();
}
}
11.3、哲學家就餐問題
有五位哲學家,圍坐在圓桌旁:
·他們只做兩件事,思考和喫飯,思考一會喫一口,喫完飯後接着思考
·喫飯時要用兩根筷子喫飯,桌上共有5根筷子,每位哲學家左右手邊各有一根筷子
·如果筷子被身邊的人拿着,自己就得等待
@Slf4j
public class PhilosopherEatThread {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("蘇格拉底",c1,c2).start();
new Philosopher("柏拉圖",c2,c3).start();
new Philosopher("亞里士多德",c3,c4).start();
new Philosopher("赫拉克利特",c4,c5).start();
new Philosopher("阿基米德",c5,c1).start();
//將阿基米德的C5C1換成C1C5也可解決死鎖問題
//t1線程先獲得A對象鎖,接下來想獲取B對象的鎖
//t2線程先獲得不了A對象鎖,無法接下來再獲取B對象的鎖
//出現線程飢餓現象
//new Philosopher("阿基米德",c1,c5).start();
}
}
@Slf4j
class Philosopher extends Thread{
//左邊的筷子
private Chopstick left;
//右邊的筷子
private Chopstick right;
public Philosopher(String name,Chopstick left,Chopstick right){
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true){
//嘗試獲取左手邊的筷子
synchronized (left){
//嘗試獲取右手邊的筷子
synchronized (right){
//喫飯
eat();
}
}
}
}
public void eat(){
log.info("eat.....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Data
@AllArgsConstructor
class Chopstick{
private String name;
@Override
public String toString() {
return "筷子{ "+name+"}";
}
}
11.4、活鎖
活鎖出現在兩個線程互相改變對方的結束條件,最後誰也無法結束
##解決方案:增加隨機的睡眠時間
/**
* 活鎖
*/
@Slf4j
public class LiveLockThread {
static volatile int count = 10;
public static void main(String[] args) {
new Thread(()->{
//期望count減到0停止
while(count>0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
log.info("count : {}"+count);
}
},"t1").start();
new Thread(()->{
//期望count加到0停止
while(count<20){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.info("count : {}"+count);
}
},"t2").start();
}
}
11.5、飢餓
·一個線程由於優先級太低,始終得不到CPU調度執行,也不能夠結束(讀寫鎖是涉及界問題)
解決方案:大家按照相同的鎖的順序加鎖
十二、ReentrantLock
12.1、基本語法
##相對於synchronized它具有的特點:
·可中斷
·可設置超時時間
·可設置爲公平鎖(防止線程飢餓)
·支持多個條件變量
·與synchronized一樣可重入
(相當於synchronized(obj)中的obj)
##基本語法
//獲取鎖
reentrantLock.lock();
try{
//臨界區
}finally{
//釋放鎖
reentrantLock.unlock();
}
12.2、可重入
##可重入
·可重入是指同一個線程如果首次獲得了這把鎖,那麼因爲它是這把鎖的擁有者,因此有權利再次
獲取這把鎖
·如果是不可重入鎖,那麼第二次獲得鎖時,自己也會被鎖擋住
@Slf4j
public class ReentrantLockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try{
log.info("lock main");
m1();
}finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try{
log.info("lock m1");
m2();
}finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try{
log.info("lock m2");
}finally {
lock.unlock();
}
}
}
12.3、可打斷
##可打斷
·t線程在等待鎖的過程中,其他線程可以使用interrupt()方法終止t線程的等待
(synchronized和lock方法是不可打斷的)
·防止線程無限制的等待下去
##語法:
//如果沒有競爭那麼此方法就會獲取lock對象鎖
//如果有競爭就進入阻塞隊列,但可以被其他線程使用interrupt()方法打斷
reentrantLock.lockInterruptibly()
@Slf4j
public class ReentrantLockInterruptTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
log.info("t1嘗試獲取鎖");
lock.lockInterruptibly();
} catch (InterruptedException e) {
log.info("t1獲取鎖失敗");
e.printStackTrace();
return;
}
try {
log.info("t1獲取鎖成功");
}finally {
lock.unlock();
}
},"t1");
log.info("main線程獲取鎖");
lock.lock();
t1.start();
//main睡1s
Thread.sleep(1000);
//主線程打斷t1
log.info("mian線程打斷t1");
t1.interrupt();
}
}
12.4、鎖超時
##鎖超時
interrupt是被動的避免線程死等,鎖超時是主動的避免死等
##語法
//嘗試獲取鎖,獲取不了之後直接失敗
//返回的是true和false
reentrantLock.tryLock();
//嘗試等待timeout時長,如果等待時長到了還獲取不到所直接失敗
//返回的是true和false
reentrantLock.tryLock(long timeout,TimeUnit u);
@Slf4j
public class ReentrantLockTryLockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try{
log.info("t1嘗試獲取鎖");
if(!lock.tryLock(2, TimeUnit.SECONDS)){
log.info("t1獲取鎖失敗");
return;
}
} catch (InterruptedException e) {
log.info("t1獲取鎖失敗");
e.printStackTrace();
return;
}
try{
log.info("t1成功獲取鎖");
}finally {
lock.unlock();
}
}, "t1");
log.info("main線程獲取鎖");
lock.lock();
t1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("main線程釋放鎖");
lock.unlock();
}
}
12.5、哲學家就餐問題
·在11.3中哲學家就餐問題,因爲使用synchronized哲學家會出現在持有左手筷子的情況下一
直等待右手筷子。我們可以使用reentrantLock的tryLock()方法防止鎖無限制的等待下去,
如果拿不到右手筷子,我們主動釋放左手筷子
/**
* reentrantLock解決哲學家就餐問題
*/
public class PhilosopherEatReentrantLock {
public static void main(String[] args) {
Chopstick2 c1 = new Chopstick2("1");
Chopstick2 c2 = new Chopstick2("2");
Chopstick2 c3 = new Chopstick2("3");
Chopstick2 c4 = new Chopstick2("4");
Chopstick2 c5 = new Chopstick2("5");
new Philosopher2("蘇格拉底",c1,c2).start();
new Philosopher2("柏拉圖",c2,c3).start();
new Philosopher2("亞里士多德",c3,c4).start();
new Philosopher2("赫拉克利特",c4,c5).start();
new Philosopher2("阿基米德",c5,c1).start();
}
}
@Slf4j
class Philosopher2 extends Thread{
//左邊的筷子
private Chopstick2 left;
//右邊的筷子
private Chopstick2 right;
public Philosopher2(String name,Chopstick2 left,Chopstick2 right){
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true){
//嘗試獲取左手邊的筷子
if(left.tryLock()){
try{
//嘗試獲取右手邊的筷子
if(right.tryLock()){
try {
//喫飯
eat();
}finally {
right.unlock();
}
}
}finally {
left.unlock();
}
}
}
}
public void eat(){
log.info("eat.....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//筷子集成ReentrantLock
@Data
@AllArgsConstructor
class Chopstick2 extends ReentrantLock {
private String name;
@Override
public String toString() {
return "筷子{ "+name+"}";
}
}
12.6、公平鎖
·synchronized的monitor鎖是不公平的
(blocked狀態下的鎖在獲取到競爭權時會一擁而上,誰搶到算誰的,毫無公平性可言)
·ReentrantLock鎖默認是不公平的
·但我們可以通過ReentrantLock的構造方法設置其爲公平鎖
##語法:
//true爲公平鎖,false爲不公平鎖,默認是false
public ReentrantLock(boolean fair){
sync = fair ? new FairSync():new NonfairSync()
}
【公平鎖主要是爲了解決飢餓問題,一般沒有必要使用,它會降低併發度】
12.7、條件變量
·synchronized中也有條件變量,就是waitSet休息室,當條件不滿足時進入waitset等待
·ReentrantLock的條件變量比synchronized強大之處在於,它是支持多個條件變量的:
>synchronized是哪些不滿足條件的線程都在一間休息室等待消息
>ReentrantLock支持多間休息室,喚醒時也是按休息室來喚醒
##使用流程:
·await前需要獲得鎖
·await執行後,會釋放鎖,進入conditionObject等待
·await的線程被喚醒(或打斷、或超時)取重新競爭lock鎖
·競爭lock鎖成功後,從await後繼續執行
##創建條件變量(休息室)
Condition c1 = rLock.newCondition();
##進入休息室等待
c1.await();//執行其前必須先獲得鎖:rLock.lock();
##從休息室中喚醒
c1.signal();//隨機喚醒c1中的某一個等待線程
c1.signalAll();//喚醒c1中的全部等待線程
/**
* 小南和小女在一間屋子幹活,小南沒煙罷工,小女沒外賣罷工
*/
@Slf4j
public class AWaitConditionReentrantLock {
static ReentrantLock ROOM = new ReentrantLock();//房間
static Condition cigaretteCondition = ROOM.newCondition();//等煙的休息室
static Condition takeOutCondition = ROOM.newCondition();//等外賣的休息室
static boolean hasCigarette = false;//是否有煙
static boolean hasTakeOut = false;//是否有外賣
public static void main(String[] args) {
//小南
new Thread(()->{
ROOM.lock();
try{
log.info("有煙沒:[{}]",hasCigarette);
while(true){
if(!hasCigarette){
log.info("沒煙,歇會兒!");
try {
cigaretteCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
log.info("可以開始幹活了");
break;
}
}
}finally {
ROOM.unlock();
}
},"小南").start();
//小女
new Thread(()->{
ROOM.lock();
try{
log.info("有外賣沒:[{}]",hasTakeOut);
while(true){
if(!hasTakeOut){
log.info("沒外賣,歇會兒!");
try {
takeOutCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
log.info("可以開始幹活了");
break;
}
}
}finally {
ROOM.unlock();
}
},"小女").start();
//主線程休息1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//送外賣的到了
new Thread(()->{
ROOM.lock();
try{
hasTakeOut = true;
log.info("外賣到了");
takeOutCondition.signal();
}finally {
ROOM.unlock();
}
},"送外賣的").start();
//送煙的到了
new Thread(()->{
ROOM.lock();
try {
hasCigarette = true;
log.info("煙到了");
cigaretteCondition.signal();
}finally {
ROOM.unlock();
}
},"送煙的").start();
}
}
##結果:
10:41:20.511 [小南] INFO xxx - 有煙沒:[false]
10:41:20.514 [小南] INFO xxx - 沒煙,歇會兒!
10:41:20.514 [小女] INFO xxx - 有外賣沒:[false]
10:41:20.514 [小女] INFO xxx - 沒外賣,歇會兒!
10:41:21.510 [送外賣的] INFO xxx - 外賣到了
10:41:21.510 [小女] INFO xxx - 可以開始幹活了
10:41:21.510 [送煙的] INFO xxx - 煙到了
10:41:21.511 [小南] INFO xxx - 可以開始幹活了
十三、練習:固定運行順序
兩個線程,一個打印1一個打印2,必須21
synchronized wait notify實現
@Slf4j
public class ThreadOrder_1 {
static final Object lock = new Object();//鎖對象
static boolean if2runned = false;//線程2是否執行
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!if2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.info("2");
if2runned = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
synchronized park unpark實現
@Slf4j
public class ThreadOrder_2 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();//暫停t1線程,等待t2線程喚醒
log.info("1");
}, "t1");
t1.start();
new Thread(()->{
log.info("2");
LockSupport.unpark(t1);
},"t2").start();
}
}
十四、練習:交替輸出
三個線程:t1輸出a 5次 t2輸出b 5次 t3輸出c 5次
現要求輸出abcabcabcabcabc
synchronized wait notify實現
@Slf4j
public class ThreadAlternately_1 {
public static void main(String[] args) {
WaitNotify waitNotify = new WaitNotify(1, 5);
new Thread(()->{
waitNotify.print("a",1,2);
},"t1").start();
new Thread(()->{
waitNotify.print("b",2,3);
},"t2").start();
new Thread(()->{
waitNotify.print("c",3,1);
},"t3").start();
}
}
/**
* 輸出內容 等待標記flag 下一個標記flag
* a 1 2
* b 2 3
* c 3 1
*/
@Slf4j
@Data
@AllArgsConstructor
class WaitNotify{
//等待標記
private int flag;
//循環次數
private int loopNum;
//打印
public void print(String str,int waitFlag,int nextFlag){
for (int i = 0; i <5 ; i++) {
synchronized (this){
while(flag!=waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
ReentrantLock await signal實現
@Slf4j
public class ThreadAlternately_2 {
public static void main(String[] args) {
AwaitNotify an = new AwaitNotify(5);
Condition aCond = an.newCondition();
Condition bCond = an.newCondition();
Condition cCond = an.newCondition();
new Thread(()->{
an.print("a",aCond,bCond);
},"t1").start();
new Thread(()->{
an.print("b",bCond,cCond);
},"t2").start();
new Thread(()->{
an.print("c",cCond,aCond);
},"t3").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//主線程喚醒t1
an.lock();
try {
aCond.signal();
}finally {
an.unlock();
}
}
}
@Slf4j
@Data
@AllArgsConstructor
class AwaitNotify extends ReentrantLock {
private int loopNum;//循環次數
//str打印內容 currentCond當前休息室 nextCond下一個休息室
public void print(String str, Condition currentCond,Condition nextCond){
for (int i = 0; i <loopNum ; i++) {
this.lock();
try{
currentCond.await();
//被喚醒後打印 內容
log.info(str);
//喚醒另一個線程
nextCond.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
}
LockSupport park unpark
@Slf4j
public class ThreadAlternately_3 {
static Thread t1,t2,t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(()->{
pu.print("a",t2);
},"t1");
t2 = new Thread(()->{
pu.print("b",t3);
},"t2");
t3 = new Thread(()->{
pu.print("c",t1);
},"t3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
@Slf4j
@Data
@AllArgsConstructor
class ParkUnpark{
private int loopNum;//循環次數
//str打印的內容 t被喚醒的線程
public void print(String str,Thread t){
for (int i = 0; i <loopNum ; i++) {
LockSupport.park();
log.info(str);
LockSupport.unpark(t);
}
}
}