?JUC并發編程,深入學習Java并發編程,與視頻每一P對應,全系列6w+字。
P1-5 為什么學+特色+預備知識 進程線程概念
進程:
一個程序被運行,從磁盤加載這個程序的代碼到內存,就開起了一個進程。
進程可以視為程序的一個實例,大部分程序可以同時運行多個實例進程(筆記本,記事本,圖畫,瀏覽器等),也有的程序只能啟動一個實例進程(網易云音樂,360安全衛士等)。
線程:
一個進程內可以分為一到多個線程。
一個線程就是一個指令流,將指令流中的一條條指令以一定的順序交給CPU執行。
Java中線程是最小調度單元,進程作為資源分配的最小單位。在windows中進程是不活動的,知識作為線程的容器。
對比:
進程擁有共享的資源,如內存空間等,供其內部線程共享。
進程間通信較為復雜:同一臺計算機的進程通信稱為IPC。不同計算機之間的進程通信,需要通過網絡,遵守共同的協議。
線程通信簡單,因為共享進程內的內存,多個線程可以訪問同一個共享變量。
線程更輕量,上下文切換成本要比進程上下文切換低。
給項目引入如下pom依賴:
<properties><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target>
</properties><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.10</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.3</version></dependency>
</dependencies>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configurationxmlns="http://ch.qos.logback/xml/ns/logback"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd"><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern></encoder></appender><logger name="c" level="debug" additivity="false"><appender-ref ref="STDOUT"/></logger><root level="ERROR"><appender-ref ref="STDOUT"/></root>
</configuration>
P6 并發并行概念
操作系統任務調度器,可以把CPU時間交給不同線程使用,線程可以輪流使用CPU資源。
假如CPU為單核,同一時間段應對多件事情叫并發。同一時間段處理多件事情的能力。一個人做多件事。
假如CPU為多核,多個核心同時執行任務,叫作并行。同一時間同時做多件事情的能力。多個人做多件事。
P7 線程應用異步調用
同步:需要等待結果返回,才能繼續運行。
異步:不需要等待結果返回,就能繼續運行。
多線程可以讓方法執行變為異步的,不會干巴巴等著,比如讀取磁盤要花費5秒,如果沒有線程調度機制,這5秒什么事情都做不了。
視頻文件要轉換格式操作比較費時,可以開一個新線程處理視頻轉換,避免阻塞主線程。
P8 線程應用提升效率
P9 P10 線程應用提升效率驗證和小結
單核多線程比單核單線程的速度慢。
多核多線程比多核單線程快。
P11 創建線程方法1
源代碼是在如下位置:
一開始默認有一個主線程在運行。
@Slf4j(topic = "c.Test1")
public class Test1 {public static void main(String[] args){Thread t = new Thread(){@Overridepublic void run(){log.debug("running");}};t.setName("t1");t.start();log.debug("running");}
}
P12 創建線程方法2
使用Runnable配合Thread創建線程:
@Slf4j(topic="c.Test2")
public class test2 {public static void main(String[] args) {Runnable r = new Runnable() {@Overridepublic void run() {log.debug("running");}};Thread t = new Thread(r,"t2");t.start();}
}
將任務和線程分離:?
P13 創建線程lambda簡化
@Slf4j(topic="c.Test2")
public class test2 {public static void main(String[] args) {Runnable r =()->{log.debug("running");};Thread t = new Thread(r,"t2");t.start();}
}
超級簡化版:
@Slf4j(topic="c.Test2")
public class test2 {public static void main(String[] args) {Thread t = new Thread(()->{log.debug("running");},"t2");t.start();}
}
P14 創建線程方法1,2-原理
P15 創建線程方法3
FutureTask配合Thread,FutureTask能夠接收Callable類型的參數,用來處理有返回結果的情況。
@Slf4j(topic="c.Test2")
public class Test3 {public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {log.debug("running...");Thread.sleep(2000);return 100;}});Thread t1 = new Thread(task,"t1");t1.start();log.debug("{}",task.get());//阻塞住,等待線程,直到線程返回結果}
}
P16 線程運行現象
交替運行。
P17 線程運行windows查看和殺死
查看方式:1.通過任務管理器。2.在控制臺輸入tasklist
找到java進程:
tasklist | findstr java
?查看所有java進程:
jps
殺死某個進程:
taskkill /F /PID PID號
P18 線程運行linux查看和殺死
列出所有正在執行的進程信息:
ps -fe
?用grep關鍵字進行篩選:
ps -fe | grep 關鍵字
查看java進程頁可以用Jps。
殺死某個進程:
kill PID號
查看進程內的線程信息:
top -H -p PID號
P19 線程運行jconsole
輸入win+r,鍵入jconsole,可以打開圖形化界面。
可以遠程連接到服務器監控信息。
P20 線程運行原理棧幀debug
JVM由堆、棧、方法區組成。棧內存是給線程用的,每個線程啟動后,虛擬機會為其分配一塊棧內存。
棧由棧幀組成,對應每次方法調用時所占用的內存。
每個線程只能有一個活動棧幀,對應著當前正在執行的那個方法。
P21 線程運行原理棧幀圖解
?
返回地址對應的是方法區中的方法,局部變量對應的是堆中的對象。
P22?線程運行原理多線程
P23?線程運行原理上下文切換
CPU不再執行當前的線程,轉而執行另一個線程的代碼:
1.線程的CPU時間片用完。
2.垃圾回收。暫停當前所有的工作線程,讓垃圾回收的線程去回收垃圾。
3.有更高優先級的線程需要運行。
4.線程自己調用了sleep,yield,wait,join,park,synchronized,lock等方法。
當Context Switch發生時,需要由操作系統保存當前線程的狀態,并恢復另一個線程的狀態,Java中對應概念是程序計數器,作用是記住下一條jvm指令的執行地址,是線程私有的。
狀態包括程序計數器、虛擬機棧中每個棧幀的信息,如局部變量、操作數棧、返回地址。
P24 常見方法概述
start() 啟動一個新線程,在新的線程運行run方法中的代碼。start方法只能讓線程進入就緒,代碼不一定立即執行(只有等CPU的時間片分配給它才能運行)。每個線程對象的start方法只能調用一次。
join()等待線程運行結束。假如當前的主線程正在等待某個線程執行結束后返回的結果,就可以調用這個join方法。join(long n)表示最多等待n毫秒。
getId()獲得線程id,getName()獲得線程名稱,setName()設置線程名稱,getPriority()獲得優先級,setPriority(int)設置線程優先級,getStatus()獲取線程狀態,isInterupted()判斷是否被打斷,isAlive()判斷線程是否存活,interrupt()打斷線程,interrupted()判斷當前線程是否被打斷。
currentThread()獲取當前正在執行的線程,sleep(long n)讓當前執行的線程休眠n毫秒,休眠時讓出其cpu的時間片給其它線程。
yield()提示線程調度器讓出當前線程對CPU的使用。
P25 常見方法start vs run
用run時是主線程來執行run方法。無法做到異步。
@Slf4j(topic="c.Test4")
public class Test4 {public static void main(String[] args) {Thread t1 = new Thread("t1") {@Overridepublic void run() {log.debug("running...");}};t1.run();}
}
下面是使用start方法啟動,可以異步執行任務。
@Slf4j(topic="c.Test4")
public class Test4 {public static void main(String[] args) {Thread t1 = new Thread("t1") {@Overridepublic void run() {log.debug("running...");}};System.out.println(t1.getState());t1.start();System.out.println(t1.getState());}
}
在new之后start之前是NEW狀態,在start之后是RUNNABLE狀態。?
P26?常見方法sleep狀態
sleep讓線程從running狀態變成time waiting狀態,從運行狀態變到有時限(因為會傳遞一個參數)的等待狀態。
P27?常見方法sleep打斷
正在睡眠的線程可以由其它線程用interrupt方法打斷喚醒。此時睡眠的方法會拋出InterruptException。
程序思路,t1.start執行完,輸出begin,然后休眠,執行t1的run方法輸出enter slee...,然后休眠,1秒到后輸出interrupt,最終t1.interrupt方法被調用,休眠線程立刻被打斷,開始執行wake up....
@Slf4j(topic="c.Test7")
public class Test6 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread("t1") {public void run() {log.debug("enter sleep....");try {Thread.sleep(2000);} catch (InterruptedException e) {log.debug("wake up...");throw new RuntimeException(e);}}};t1.start();log.debug("begin");Thread.sleep(1000);log.debug("interrupt");t1.interrupt();}
}
P28?常見方法sleep可讀性
建議用TimeUnit的sleep代替Thread的sleep來獲得更好的可讀性。
@Slf4j(topic = "c.Test8")
public class Test7 {public static void main(String[] args) throws InterruptedException {log.debug("enter");TimeUnit.SECONDS.sleep(1);log.debug("end");}
}
?
P29?常見方法yield_vs_sleep
1.yield
某個線程調用yield,可以讓出CPU的使用權。
調用yield會讓當前線程從Running進入Runnable就緒狀態,然后調度執行其它線程。
2.sleep
調用sleep會讓當前線程從Running進入Timed Waitring狀態(阻塞)
P30?常見方法線程優先級
線程優先級會提示(hint)調度器優先調度該線程,但它僅僅只是一個提示,調度器可以忽略它。
如果cpu較忙,優先級高的線程會獲得更多的時間片,但cpu如果閑時,優先級幾乎沒作用。
@Slf4j(topic="c.Test4")
public class test4 {public static void main(String[] args) {Runnable task1 =()->{int count=0;for(;;){System.out.println("------>1"+count++);}};Runnable task2 =()->{int count=0;for(;;){//Thread.yield();System.out.println(" ------>2"+count++);}};Thread t1 = new Thread(task1,"t1");Thread t2 = new Thread(task2,"t2");//t1.setPriority(Thread.MIN_PRIORITY);//t2.setPriority(Thread.MAX_PRIORITY);t1.start();t2.start();}
}
P31?常見方法sleep應用
在沒有利用cpu來計算時,不要讓while(true)空轉浪費cpu,這時可以使用yield或sleep來讓出cpu的使用權給其它程序。
可以用wait或者條件變量達到類似的效果。但需要加鎖,并且需要設置相應的喚醒操作,一般適用于要進行同步的場景。sleep適合無鎖同步的場景。
P32?常見方法join
join等待某個線程執行結束。
下面這個例子因為t1線程睡了1秒,對r的更改不會發生,主線程會直接輸出r的結果r=0。此時若想讓r=10,則需要在t1.start()的下面加上t1.join()表示等待t1執行結束返回結果,主線程再執行。
@Slf4j(topic="c.Test5")
public class test5 {static int r=0;public static void main(String[] args) throws InterruptedException{test1();}public static void test1() throws InterruptedException{log.debug("開始");Thread t1 = new Thread(()->{log.debug("開始");sleep(1);log.debug("結束");r=10;},"t1");t1.start();t1.join();log.debug("結果為:{}",r);log.debug("結果");}
}
P33?常見方法join同步應用
需要等待結果返回,才能繼續運行是同步。
不需要等待結果返回,就能繼續運行是異步。
@Slf4j(topic = "c.TestJoin")
public class TestJoin {static int r = 0;static int r1 = 0;static int r2 = 0;public static void main(String[] args) throws InterruptedException {test2();}private static void test2() throws InterruptedException {Thread t1 = new Thread(() -> {sleep(1);r1 = 10;});Thread t2 = new Thread(() -> {sleep(2);r2 = 20;});t1.start();t2.start();long start = System.currentTimeMillis();log.debug("join begin");t1.join();log.debug("t1 join end");t2.join();log.debug("t2 join end");long end = System.currentTimeMillis();log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);}
}
P34?常見方法join限時同步
下面給t1.join()設置了1500毫秒等待時間,因為小于線程睡眠時間,所以沒法能線程蘇醒改變r,輸出結果為r1=0。
@Slf4j(topic = "c.TestJoin")
public class TestJoin {static int r = 0;static int r1 = 0;static int r2 = 0;public static void main(String[] args) throws InterruptedException {test3();}public static void test3() throws InterruptedException {Thread t1 = new Thread(() -> {sleep(2);r1 = 10;});long start = System.currentTimeMillis();t1.start();// 線程執行結束會導致 join 結束log.debug("join begin");t1.join(1500);long end = System.currentTimeMillis();log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);}
}
P35?常見方法interrupt打斷阻塞
如果線程是在睡眠中被打斷會以報錯的形式出現,打斷標記為false。
@Slf4j(topic="c.Test6")
public class test6 {public static void main(String[] args) throws InterruptedException{Thread t1 = new Thread(() -> {log.debug("sleep...");try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}}, "t1");t1.start();Thread.sleep(1000);log.debug("interrupt");t1.interrupt();log.debug("打斷標記:{}",t1.isInterrupted());}
}
P36?常見方法interrupt打斷正常
如果在main方法中調用t1的interrupt方法,t1線程只是會被告知有線程想打斷,不會強制被退出。此時isinterrupted狀態會被設為true,此時可以利用該狀態來讓線程決定是否退出。
@Slf4j(topic="c.Test7")
public class Test7 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{while(true){boolean interrupted = Thread.currentThread().isInterrupted();if(interrupted){log.debug("被打斷了,退出循環");break;}}},"t1");t1.start();Thread.sleep(1000);log.debug("interrupt");t1.interrupt();}
}
P37?設計模式兩階段終止interrupt
在一個線程T1中如何優雅的終止線程T2,這里的優雅指的是給T2一個料理后事的機會。
錯誤思路:
1.使用線程對象的stop方法停止線程。stop方法會真正殺死線程,如果線程鎖住了共享資源,那么當它被殺死后就再也沒有機會釋放鎖,其它線程將永遠無法獲取鎖。
2.使用System.exit(int)方法會直接把方法停止,直接把進程停止。
P38?設計模式兩階段終止interrupt分析
在工作中被打斷,打斷標記是false,會進入到料理后事。
在睡眠是被打斷,會拋出異常,此時打斷標記是true,此時可以重新設置打斷標記為false。
P39?設計模式兩階段終止interrupt實現
@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination{private Thread monitor;public void start(){monitor = new Thread(()->{while(true) {Thread current = Thread.currentThread();if (current.isInterrupted()) {log.debug("料理后事");break;}try {Thread.sleep(1000);log.debug("執行監控記錄");} catch (InterruptedException e) {e.printStackTrace();//重新設置打斷標記current.interrupt();}}});monitor.start();}public void stop(){monitor.interrupt();}
}
P40?設計模式兩階段終止interrupt細節
P41 常見方法interrupt打斷park
@Slf4j(topic="c.Test9")
public class java9 {public static void main(String[] args) throws InterruptedException{test1();}public static void test1() throws InterruptedException{Thread t1 = new Thread(()->{log.debug("park...");LockSupport.park();log.debug("unpark...");log.debug("打斷狀態:{}",Thread.interrupted());LockSupport.park();log.debug("unpark...");},"t1");t1.start();sleep(1);t1.interrupt();}
}
P42?常見方法過時方法
?切忌用stop,suspend方法。
P43?常見方法守護線程
默認情況下,Java進程需要等待所有的線程都運行結束,才會結束。
有一種特殊的線程叫守護線程,只要其它非守護線程執行結束了,即時守護線程的代碼沒有執行完,也會強制結束。
在t1啟動前調用setDaemon方法開啟守護線程,如果主線程運行結束,守護線程也會結束。
@Slf4j(topic="c.Test15")
public class Test10 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{while(true){if(Thread.currentThread().isInterrupted()){break;}}});t1.setDaemon(true);t1.start();Thread.sleep(1000);log.debug("結束");}
}
垃圾回收器線程是一種守護線程。如果程序停止,垃圾回收線程也會被強制停止。
P44?線程狀態五種
初始狀態:在語言層面上創建線程對象,還沒與操作系統中的線程關聯,僅停留在對象層面。比如new了一個Thread對象,但沒調用start方法。
可運行狀態:就緒狀態,線程已經被創建,與操作系統線程關聯,可以由CPU調度器調度執行,可以獲得CPU時間片,但暫時沒獲得時間片。
運行狀態:指獲取了CPU時間片,運行中的狀態。
阻塞狀態:調用了阻塞API,比如BIO讀寫文件,線程不會用到CPU,會導致上下文切換,進入阻塞狀態。等BIO操作完畢,會由操作系統喚醒阻塞的線程,轉換至可運行狀態。
終止狀態:線程已經執行完畢,生命周期結束,不會再轉換為其它狀態。
P45 線程狀態六種
從Java的層面進行描述:
NEW:指被創建,還沒調用Start方法。
RUNNABLE:涵蓋了操作系統層面的可運行、運行、阻塞狀態。
TERMINATED:指被終止狀態,不會再轉化為其它狀態。
3種阻塞的狀態:
BLOCKED(想獲得鎖,但獲得不了,拿不到鎖會陷入block狀態)
WAITING(這個是join等待時的狀態)
TIMED_WAITING(這個是sleep時的狀態,有時限的等待)
P46 線程狀態六種演示
P47 習題應用之統籌分析
P48?習題應用之統籌實現
@Slf4j(topic = "c.Test16")
public class Test11 {public static void main(String[] args) {Thread t1 = new Thread(()->{log.debug("洗水壺");Sleeper.sleep(1);log.debug("燒開水");Sleeper.sleep(5);},"老王");Thread t2 = new Thread(()->{log.debug("洗茶壺");Sleeper.sleep(1);log.debug("洗茶杯");Sleeper.sleep(2);log.debug("拿茶葉");Sleeper.sleep(1);try {t1.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("泡茶");},"小王");t1.start();t2.start();}
}
缺點:上面模擬的是小王等老王的水燒開了,小王泡茶,如果反過來要實現老王等小王的茶葉拿過來,老王泡茶呢?代碼最好能適應2種情況。
上面的兩個線程各執行各的,如果要模擬老王把水壺交給小王泡茶,或模擬小王把茶葉交給老王泡茶呢?
P49 第三章小節
P50 本章內容
P51 小故事線程安全問題
多線程下訪問共享資源,因為分時系統導致的數據不一致等安全問題。
P52 上下文切換分析
@Slf4j(topic="c.Test12")
public class Test12 {static int counter = 0;public static void main(String[] args) throws InterruptedException {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();t1.join();t2.join();log.debug("{}", counter);}
}
結果并不唯一如下:?
?
i++和i--編譯成字節碼不是一條代碼:
?
造成數據不一致的原因是:
某個線程的事情還沒干完,數據還沒來得及寫入,上下文就切換了。根本原因:上下文切換導致指令交錯。
P53 臨界區與競態條件
問題出現在多個線程訪問共享資源。
在多個線程對共享資源讀寫操作時發生指令交錯,出現問題。
一段代碼內如果存在對共享資源的多線程讀寫操作,稱這段代碼為臨界區。
競態條件:多個
P54 上下文切換synchronized解決
為了避免臨界區的競態條件發生,有多種手段可以達到目的。
1.阻塞式的解決方案:synchronized,Lock。
2.非阻塞式的解決方案:原子變量。
本次課使用的是synchronized來解決問題,即對象鎖,它采用互斥的方式來讓同一時刻至多只能有1個線程持有對象鎖,其它線程想獲取對象鎖會被阻塞。這樣保證擁有鎖的線程可以安全的執行臨界區內的代碼,不用擔心上下文切換。
synchronized(對象){臨界區
}
@Slf4j(topic="c.Test12")
public class Test12 {static int counter = 0;static Object lock = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (lock){counter++;}}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (lock){counter--;}}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}", counter);}
}
P55?上下文切換synchronized理解
假如t1通過synchronized拿到鎖以后,但是時間片不幸用完了,但這個鎖仍舊是t1的,只有時間片下次重新輪到t1時才能繼續執行。
只有當t1執行完synchronized()塊內的代碼,會釋放鎖。其它線程才能競爭。
P56 上下文切換synchronized理解
當鎖被占用時,就算指令沒執行完上下文切換,其它線程也獲取不到鎖,只有當擁有鎖的線程的所有代碼執行完才能釋放鎖。
?P57?上下文切換synchronized思考
1.把加鎖提到for循環外,相當于5000次for循環都視為一個原子操作。
2.如果線程1加鎖,線程2沒加鎖會導致的情況:線程2去訪問臨界資源時,不會嘗試獲取對象鎖,因此不會被阻塞住,仍然能繼續訪問。
P58 鎖對象面向對象改進
@Slf4j(topic="c.Test12")
public class Test12 {public static void main(String[] args) throws InterruptedException {Room room = new Room();Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room){room.increment();}}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room){room.decrement();}}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}", room.getCounter());}
}class Room{private int counter = 0;public void increment(){synchronized (this){counter++;}}public void decrement(){synchronized (this){counter--;}}public int getCounter(){synchronized (this){return counter;}}
}
P59 synchronized 加在方法上
synchronized可以加在方法上,相當于鎖住方法。
synchronized加在靜態方法上,相當于所住類。
P60 P61 P62上下文切換synchronized習題1~8
線程1鎖的是類,線程2鎖的是方法。
下面一個鎖類一個鎖方法,鎖的仍然是不同對象,所以會并行執行。
鎖的是同一個Number對象,鎖的是靜態方法,所以鎖的是類。
P63 線程安全分析
P64 線程安全分析局部變量
局部變量的i++只有一行字節碼,不同于靜態變量的i++。
P65 線程安全分析局部變量引用
創建2個線程,然后每個線程去調用method1:
如果method1還沒把數據放入,method2就要取出數據,此時集合為空,會報錯。
將list改為局部變量后,放到方法內:
list是局部變量,每個線程會創建不同實例,沒有共享。
method2和method3的參數從method1中傳遞過啦,與method1中引用同一個對象。
P66 線程安全分析?局部變量暴露引用
因為下面ThreadSafeSubClass繼承了ThreadSafe類,然后重寫了method3方法,導致出現了問題。
必須要改為private和final防止子類去重寫和修改,滿足開閉原則,不讓子類改變父類的行為。
P67? 線程安全分析 局部變量組合調用
常見線程安全的類:
String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包下的類
注意:每個方法是原子的,單多個方法的組合不是原子的。
下面Hashtable的get和put單個方法是線程安全的,但二者組合在一起仍然會受到線程上下文的切換的影響。
P68? 線程安全分析 常見類 不可變
String、Integer等都是不可變類,即時被線程共享,因為其內部的狀態不可以改變,因此它們的方法是線程安全的。
String的replace和substring等方法看似可以改變值,實則是創建了一個新的字符串對象,里面包含了截取后的結果。
P69?線程安全分析 實例分析1~3
線程不安全:Map<String,Object> map = new HashMap<>();
線程不安全:Date
下面這段非線程安全:
下面這段非線程安全:
Spring里某一個對象沒有加Scope都是單例的,只有1份,成員變量需要被共享。
P70?線程安全分析 實例分析4~7
下面這個方法是線程安全,因為沒有成員變量,也就是類下沒有定義變量。變量在方法內部,各自都在線程的棧內存中,因此是線程安全的。
下面是線程安全的,因為UserDaoImpl里面沒有可以更改的成員變量(無狀態)。
下面是線程安全的,因為是通過new來創建對象,相當于每個線程拿到的是不一樣的副本。
P71 習題 賣票 讀題
證明方法:余票數和賣出去的票數相等,代表前后一致,沒有線程安全問題。
@Slf4j(topic="c.ExerciseSell")
public class ExerciseTransfer {public static void main(String[] args) throws InterruptedException {//模擬多人買票TicketWindow window = new TicketWindow(100000);//所有線程的集合List<Thread> threadList = new ArrayList<>();//賣出的票數統計List<Integer> amountList = new Vector<>();for(int i=0;i<20000;i++){Thread thread = new Thread(()->{int amount = window.sell(randomAmount());//買票try {Thread.sleep(randomAmount());} catch (InterruptedException e) {throw new RuntimeException(e);}amountList.add(amount);});threadList.add(thread);thread.start();}for (Thread thread :threadList) {thread.join();}log.debug("余票:{}",window.getCount());log.debug("賣出的票數:{}",amountList.stream().mapToInt(i->i).sum());}static Random random = new Random();public static int randomAmount(){return random.nextInt(5)+1;}
}
class TicketWindow{private int count;public TicketWindow(int count){this.count = count;}public int getCount(){return count;}public int sell(int amount){if(this.count >= amount){this.count -= amount;return amount;}else{return 0;}}
}
P72 習題 賣票 測試方法
老師用的是一個測試腳本進行測試。
P73 習題 賣票 解題
臨界區:多個線程對共享變量有讀寫操作。
在sell方法中存在對共享變量的讀寫操作,因此只需要在方法上加synchronized:
P74 習題 轉賬
這道題的難點在于有2個共享變量,一個是a的賬戶中的money,一個是b的賬戶中的money。
@Slf4j(topic="c.ExerciseTransfer")
public class ExerciseTransfer1{public static void main(String[] args) throws InterruptedException {Account a = new Account(1000);Account b = new Account(1000);Thread t1 = new Thread(()->{for (int i = 0; i < 1000; i++) {a.transfer(b,randomAmount());}},"t1");Thread t2 = new Thread(()->{for (int i = 0; i < 1000; i++) {b.transfer(a,randomAmount());}},"t2");t1.start();t2.start();t1.join();t2.join();log.debug("total:{}",(a.getMoney()+b.getMoney()));}static Random random = new Random();public static int randomAmount(){return random.nextInt(100)+1;}
}
class Account {private int money;public Account(int money){this.money = money;}public int getMoney(){return money;}public void setMoney(int money){this.money = money;}public void transfer(Account target,int amount){synchronized(Account.class) {if (this.money >= amount) {this.setMoney(this.getMoney() - amount);//自身余額,減去轉賬金額target.setMoney(target.getMoney() + amount);//對方余額加上轉賬金額}}}}
偷懶的方法加入下面:synchronized(Account.class),相當于鎖住兩個賬戶的臨界資源,缺點是n個賬戶只能有2個賬戶進行交互。
P75 Monitor 對象頭
Klass word是一個指針,指向某個對象從屬的Class,找到類對象,每個對象通過Klass來辨明自己的類型。
在32位虛擬機中:Integer:8+4,int:4。
P76 Monitor 工作原理
Monitor是鎖,Monitor被翻譯為監視器或管程。每個Java對象都可以關聯一個Monitor對象,如果使用synchronized給對象上鎖之后,該對象頭的Mark Word中就被設置指向Monitor對象的指針。
obj是java的對象,Monitor是操作系統提供的監視器,調用synchronized是將obj和Monitor進行關聯,相當于在MarkWord里面記錄Monitor里面的指針地址。
Monitor里面的Owner記錄的是當前哪個線程享有這個資源,EntryList是一個線程隊列,來一個線程就進入到阻塞隊列。