文章目錄
- 多線程帶來的風險-線程安全
- 線程不安全的舉例
- 分析產出線程安全的原因:
- 1.線程是搶占式的
- 2. 多線程修改同一個變量(程序的要求)
- 3. 原子性
- 4. 內存可見性
- 5. 指令重排序
- 總結線程安全問題產生的原因
- 解決線程安全問題
- 1. synchronized關鍵字的介紹(監視器鎖 monitor lock)
- a.鎖的概念
- b.synchronized的特性
- c.synchronized的用法
- d.針對上述問題,加synchronied解決問題,分析底層邏輯
- e.關于synchronized的總結
- f.不同鎖對象的情況
- g.如何判斷多個線程競爭的是不是同一把鎖
- h.可重入鎖
- I.鎖對象
- Java 標準庫中的線程安全類
- 不安全類
- 安全類
- volatile 關鍵字
- 解決線程安全的問題
- 內存可見性
- 實例
- CPU層面保證可見性
- Java層面(保證指令順序,從而保證內存可見性)
- 總結
- wait() 和 notify()
- wait和notify的基礎知識
- wait()方法
- notify()?法
- notifyAll()?法
- wait 和 sleep和join的對?(?試題)
- wait和notify的總結
多線程帶來的風險-線程安全
線程不安全的舉例
場景: 用兩個線程對同一個變量分別自增5萬次,預期結果和自增結果是一個累加和,10萬次。
public static int count;public static void main(String[] args) {Counter counter = new Counter();Thread t1 =new Thread(()->{for(int i=0;i<5_0000;i++){counter.count();}});Thread t2 =new Thread(()->{for(int i=0;i<5_0000;i++){counter.count();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: "+counter.count);}public void count(){{count+=1;}} }
執行結果:
程序運行得到的結果與預期的結果值不一樣,而且是一個錯誤的結果,而且我們程序的邏輯是正確的,這個現象所表現的問題稱為線程安全問題
分析產出線程安全的原因:
1.線程是搶占式的
線程是搶占執行的(執行順序是隨機的)
由于線程的執行順序無法為人控制,搶占式執行是造成線程安全問題的主要罪魁禍首,而且我們解決不了,完全是CPU自己調度,而且和CPU的核數有關
2. 多線程修改同一個變量(程序的要求)
單個線程修改同一個變量不會產生線程安全問題
多個線程修改不同的變量不會產生線程安全問題
多個線程修改同一個變量,會產生線程安全問題
3. 原子性
- 什么是原?性
我們把?段代碼想象成?個房間,每個線程就是要進?這個房間的?。如果沒有任何機制保證,A進?房間之后,還沒有出來;B 是不是也可以進?房間,打斷 A 在房間?的隱私。這個就是不具備原?性那我們應該如何解決這個問題呢?是不是只要給房間加?把鎖,A
進去就把?鎖上,其他?是不是就進不來了。這樣就保證了這段代碼的原?性了。 有時也把這個現象叫做同步互斥,表?操作是互相排斥的。- ?條 java 語句不?定是原?的,也不?定只是?條指令 比如上面的:count++,對應的是多條CPU指令 1:從內存或者寄存器讀取count值 LOAD 2:執行自增 ADD 3:把計算結果寫回寄存器或者內存 STORE
- 不保證原?性會給多線程帶來什么問題 如果?個線程正在對?個變量操作,中途其他線程插?進來了,如果這個操作被打斷了,結果就可能是錯誤的。 這點也和線程的搶占式調度密切相關. 如果線程不是 “搶占” 的, 就算沒有原?性, 也問題不?.
4. 內存可見性
- 什么是內存可見性 一個線程對共享變量進行了修改,其他線程能感知到變量修改后的值。
- Java內存模型(JMM) java虛擬機規范定義了Java內存模型 ?的是屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到?致的并發效果.
- 分析Java內存模型
1.工作內存和線程之間是一一對應的
2.java的共享變量都在主內存里面,java線程線程首先從主內存讀取變量的值到自己的工作內存
3.每個線程都有自己的工作內存,且線程工作內存直接是相互隔離的
4.線程在工作內存修改完變量的值后,又從工作內存把變量的值刷回主內存里面。
5.在以上執行count++操作,由于兩個線程在執行,每個線程都有自己的工作內存,且相互不可見,最終導致了線程安全問題。線程對共享變量的修改線程之間相互感知不到- 注意: 為什么整這么多內存? 實際并沒有這么多 “內存”. 這只是 Java 規范中的?個術語, 是屬于 “抽象” 的叫法,所謂的 “主內存” 才是真正硬件?度的 “內存”. ?所謂的 “?作內存”, 則是指 CPU 的寄存器和?速緩存 為啥要這么?煩的拷來拷去? 因為
CPU 訪問??寄存器的速度以及?速緩存的速度, 遠遠超過訪問內存的速度(快了 3 - 4 個數量級,也就是?千倍,
上萬倍).那為什么不全部用寄存器,原因很簡單,太貴了。- 關于JMM內存模型的面試題:JMM規定
1.所以線程不直接修改主內存中的共享變量
2.如果修改共享變量,需要把這個變量從主內存復制到自己的工作內存中,修改完之和再刷回主內存
3.各個線程之間不能相互通信,做到了內存級別的線程隔離。
5. 指令重排序
1.什么是指令重排序 我們寫的代碼,在編譯之后可能與代碼對應的指令順序不同,這個過程就是指令重排序(JVM層面可能重排序,CPU執行指令也可能重排序)
1.一段代碼是這樣的 a.代陽去教室取英語書 b.代陽去食堂吃飯 c.代陽去教室去數學書 在單線程情況下,JVM,CPU指令集會對其優化,執行順序按a–c–b的方式執行,也是沒有問題,可以少跑一次教室,這就叫指令重排序
編譯器對于指令重排序的前提是 “保持邏輯不發?變化”. 這?點在單線程環境下?較容易判斷, 但是在多線程環境下就沒那么容易了,
多線程的代碼執?復雜程度更?, 編譯器很難在編譯階段對代碼的執?效果進?預測, 因此激進的重排序很容易導致優化后的邏輯和之前不等價
總結線程安全問題產生的原因
- 線程是搶占式執行的
- CPU的調度問題,硬件層面,我們解決不了
- 多個線程修改同一個變量
- 在真實業務場景中,使用多線程就是為了提升效率,在并發編成中這個需求是滿足的
- 原子性
- 指令是在CPU上執行,怎么才能讓CPU在執行時實現原子性,這個可能可以解決
- 內存可見性
- java層面應該可以解決,進程之間可以進行通信,那那么在線程中應該也有這樣的機制,讓線程在內存中也可以彼此感知
- 指令重排序
- 對于代碼來說誰的優先級高,我們可以通過某種方式告訴編譯器,不要對我的代碼進行重排序
- 總結以上1,2我們不能改變,但是3,4,5我們可以進行改變,只要滿足3,4,5中的一條或者多條,線程安全問題就可以解決
解決線程安全問題
1. synchronized關鍵字的介紹(監視器鎖 monitor lock)
a.鎖的概念
比如線程A拿到了鎖,別的線程如果要執行被鎖住的代碼,那就要等到線程A釋放鎖之后,如果A沒有釋放鎖,那么別的線程只能阻塞等待,這個狀態就是BLOCK
b.synchronized的特性
- 互斥:synchronized會引起互斥效果,某個線程執行到某個對象的synchronized時,其他線程如果也執行到同一個對象sychronized就會阻塞等待
- 保證了原子性(通過加鎖實現)
- 保證了內存可見性(通過串行執行實現)
- 不保證有序性
c.synchronized的用法
修飾方法:
- 修飾非靜態方法:默認鎖對象是this(當前對象)
- 修飾靜態方法:默認鎖對象是本身類
修飾代碼塊:
可以充當鎖對象的是實例對象(new出來的對象,類對象,this)
d.針對上述問題,加synchronied解決問題,分析底層邏輯
public synchronized void count(){ // 修飾代碼塊加鎖synchronized(this){ // count+=1; // }synchronized(this){count+=1;}} }
如果修飾方法:其實把方法進行了串行化處理
如果修飾的是代碼塊:其實把修飾代碼塊的內容,進行了串行話處理。對于部分類似這種要修改共享變量的情況進行串行話,其他代碼模塊繼續并行執行,這樣就可以提高效率
畫圖分析:
注意的點: t1釋放鎖之后,也可能第二次還是t1先于t2拿到鎖,因為線程是搶占式執行的,不一定是t2
由于線程在執行邏輯之前要拿到鎖,當拿到鎖時,上一個線程已經執行完所有的指令,并把修改的值刷新會主內存,所有當前線程永遠讀到的是上一個線程執行完后的值synchronized保證了原子性
因為當前線程永遠拿到的是前一個線程修改后的值,所有這樣也現象上實現了內存可見性,但是并沒有真正對內存可見性做出技術上的處理。
synchronized沒有保證有序性(不會禁止指令重排序)
e.關于synchronized的總結
- 被synchronized修身的代碼塊會編成串行執行
- synchronized可以修飾方法或者代碼塊
- 被修飾的代碼并不是一次性在CPU執行完,而是中途可能會被CPU調度走,當所有指令執行完后才會釋放鎖
- 只給一個線程加鎖,也會出現線程安全
f.不同鎖對象的情況
- 同·一個引用調用(靜態和非靜態)兩個方法(一個用synchronized修飾一個不用synchronized不修飾)只加一把鎖
public static int count;public static void main(String[] args) {Counter_Demo1 counter = new Counter_Demo1();Thread t1 =new Thread(()->{for(int i=0;i<5_0000;i++){counter.count();}});Thread t2 =new Thread(()->{for(int i=0;i<5_0000;i++){counter.count1();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: "+counter.count);}public void count(){//修飾非靜態代碼塊synchronized(this){count+=1;}}public void count1(){{count+=1;}}
//修飾非靜態方法
// public void synchronized count(){
// {
// count+=1;
// }
// }
// public void count1(){
// {
// count+=1;
// }
// }
修飾靜態的方法和代碼塊
// public static void count(){
// //修飾代碼塊
// //靜態方法里面不能用this
// synchronized(Counter_Demo1.class){
// count+=1;
// }
// }
// public static void count1(){
// {
// count+=1;
// }
// }
// public synchronized static void count(){
// //修飾代碼塊
// //靜態方法里面不能用this
// {
// count+=1;
// }
// }
// public static void count1(){
// {
// count+=1;
// }
// }
執行結果
都不符合預期
- 兩個引用調用同一個方法(鎖對象是實例對象new)
public static int count;public static void main(String[] args) {Counter_Demo1 counter1 = new Counter_Demo1();Counter_Demo1 counter2 = new Counter_Demo1();Thread t1 =new Thread(()->{for(int i=0;i<5_0000;i++){counter1.count();}});Thread t2 =new Thread(()->{for(int i=0;i<5_0000;i++){counter2.count();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: "+counter1.count);}Object object=new Object();public void count(){synchronized (object){count+=1;}}
執行結果不符合預期
用類對象來加鎖
static Object object= new Object();public void count(){synchronized (object){count+=1;}}
執行結果符合預期
結論:
- 只要一個線程A獲得鎖,沒有鎖競爭
- 線程A和線程B共同搶一把鎖,誰先拿到鎖就先執行誰,另一個線程就要阻塞等待,等到持有鎖的線程釋放鎖之后再競爭鎖
- 線程A與線程B搶的不是同一把鎖,它們之間沒有競爭關系,分別去拿到自己的鎖,不存在鎖關系
g.如何判斷多個線程競爭的是不是同一把鎖
- 實例對象:new出來的對象,每個都是單獨存在
- 類中的屬性:類沒有用static修飾的變量,每個實例對象都是不同的
- 類中的靜態成員變量:用static修飾,屬于類對象,全局唯一
- 類對象:.class文件加載jvm之后的對象,全局唯一
- 線程之間是否存在鎖競爭,關鍵是看訪問的是不是同一個鎖對象,如果是則存在鎖競爭,如果不是則不存在鎖競爭
h.可重入鎖
- 對同一個鎖對象和同一個線程,如果可以重復加鎖,稱之為不互斥,稱之為可重入。
- 對同一個鎖對象和同一線程,如果不可以重復加鎖,稱之為互斥,就會形成死鎖。
- 已經獲取鎖對象的線程,如果再多次進行加鎖操作,不會產生互斥現象
I.鎖對象
- 鎖對象記錄了獲取鎖的線程信息
- 任何對象都可以做鎖對象
- java中每個對象都是由以下幾個部分組成:
– 1.markword
– 2.類型指針
– 3.實例數據
– 4.對齊填充
– 5對象默認帶線啊哦是16byte
Java 標準庫中的線程安全類
不安全類
- Arraylist
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
安全類
-Vector(不推薦)
-
HashTable(不推薦)
-
CocurrentHashMap
-
StringBuffer
-
String (雖然沒有加鎖,但是不涉及修飾仍然是線程安全的)
volatile 關鍵字
解決線程安全的問題
- 真正意義上解決了內存可見性
- 解決了指令重排序(禁止指令重排序)問題
- 沒有解決原子性問題
內存可見性
- 我們都知道,實際工作時候,訪問的數據都是工作內存里面的數據,這樣是為了保證效率,但是這樣有時候會產生安全問題。但是加上 volatile , 強制讀寫內存. 速度是慢了, 但是數據變的更準確了
- 代碼在寫? volatile 修飾的變量的時候
– 改變線程?作內存中volatile變量副本的值
– 將改變后的副本的值從?作內存刷新到主內存 - 代碼在讀取volatile修改的變量時候
–從主內存中讀取volatile變量的最新值到線程的?作內存中
–從?作內存中讀取volatile變量的副本
實例
static class Counter {public volatile int flag = 0;}public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {// do nothing}System.out.println("循環結束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("輸??個整數:");counter.flag = scanner.nextInt();});t1.start();t2.start();}
// 執?效果
// 當??輸??0值時, t1 線程循環不會結束. (這顯然是?個 bug)
//static class Counter {
// public volatile int flag = 0;
//}
// 執?效果
// 當??輸??0值時, t1 線程循環能夠?即結束.
-
對于線程t1來說,只是比較flag這個變量的值,從來都沒有修改過,所有認為,這個值永遠也不會改變,從而也不會重新從主內存中讀取值(cpu為了提升高運行效率這個值一般存在寄存器或者cpu的緩存中)
-
在多線程環境下,就會出現出現這個問題,一個線程修改了另一個線程無法感知到的變量
CPU層面保證可見性
MESI緩存 一致協議(可以理解是一種通知機制)
Java層面(保證指令順序,從而保證內存可見性)
內存屏障:作用是保證指令執行的順序,從而保證內存可見性
volatile寫:
volatile讀:
有序性:用volatile 修改過的變量,由于前后有內存屏障,保證了指令的執行順序,也可以理解為告訴編譯器,不要進行指令重排序。
總結
volatile不保證原子性
public static volatile int count;public synchronized static void main(String[] args) {Counter counter = new Counter();Thread t1 =new Thread(()->{for(int i=0;i<5_0000;i++){counter.count();}});Thread t2 =new Thread(()->{for(int i=0;i<5_0000;i++){counter.count();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: "+counter.count);}public void count(){count+=1;}
volatile保證可見性:MESI緩存 一致協議(可以理解是一種通知機制)
volatile保證有序性:內存屏障:作用是保證指令執行的順序,從而保證內存可見性
wait() 和 notify()
wait和notify的基礎知識
- wait() 和notify(),notifyAll()是object方法
- wait()/wait(long timeout):讓線程進入等待的線程
- notify()/notifyAll():喚醒在當前對象上等待的線程
wait()方法
- wait做的事情
– 使當前執行代碼的線程進行等待(把線程放到等待隊列中)
– 釋放當前鎖
– 滿足一定條件被喚醒,嘗試重新獲得這個鎖
– wait要搭配sychronized來使用,脫離sychronized使用wait會之間拋出異常 - wait 結束等待的條件:
– 其他線程調用 調用該對象的notify方法
– wait等待時間超時(wait ?法提供?個帶有 timeout 參數的版本, 來指定等待時間)
– 其他線程調用該等待線程的interrupted方法,導致wait拋出InterruptedException 異常. - wait()方法代碼使用
public static void main(String[] args) throws InterruptedException {Object object = new Object();System.out.println("等待中");synchronized (object) {object.wait(1000);}System.out.println("等待結束");}這樣在執?到object.wait()之后就?直等待下去,那么程序肯定不能?直這么等待下去了。這個時候就
需要使?到了另外?個?法喚醒的?法notify()。
notify()?法
- notify ?法是喚醒等待的線程.
– 方法notify()也要在同步方法或者同步代碼塊中執行,該方法是用來通知哪些可能等待該對象的對象鎖的其他線程,對其發出通知,并使它們重新獲取該對象對象鎖
– 如果由多個線程等待,則有線程調度器隨機挑選出一個呈現wait狀態的線程。(并沒有 “先來后到”))
– 在notify()方法后,當前線程不會立馬釋放該對象鎖,需要等到notify方法線程將程序執行完,也就是退出同步代碼塊之后才會釋放鎖對象。 - 使用notify()方法喚醒線程
static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker){try {System.out.println("等待開始") ;locker.wait();System.out.println("等待結束");} catch (InterruptedException e) {e.printStackTrace();}}}}static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 開始");locker.notify();System.out.println("notify 結束");}}}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();Thread.sleep(1000);t2.start();}
notifyAll()?法
- notify?法只是喚醒某?個等待線程. 使?notifyAll?法可以?次喚醒所有的等待線程.
static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {try {System.out.println("等待開始");locker.wait();System.out.println("等待結束");} catch (InterruptedException e) {e.printStackTrace();}}}}static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 開始");locker.notifyAll();System.out.println("notify 結束");}}}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t3 = new Thread(new WaitTask(locker));Thread t4 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();t3.start();t4.start();sleep(1000);t2.start();}}
注意: 雖然是同時喚醒 3 個線程, 但是這 3 個線程需要競爭鎖. 所以并不是同時執?, ?仍然是有先有后的執?
wait 和 sleep和join的對?(?試題)
- wait需要搭配synchronized使用 sleep,join不需要
- wait是Object的方法,sleep是Thread的靜態方法,join是類中的方法(實例方法)
- 一個是用于線程之間的通信的,兩個是讓線程阻塞一段時間
- 相同點:可以讓線程放棄執行一段時間
wait和notify的總結
- join和wait是兩個不同的操作
–join是Thread類中的方法
– wait和notify是Object類中的方法
– join狀態,主線程要等待子線程的結果
– wait是等待另一個線程的資源 - wait和notify必須跟synchronized一起使用,并且使用同一個對象
– 否則會報錯
– wait的線程進入阻塞狀態,調用wait的線程會釋放自己持有的鎖(不再占有cpu資源) - notify()和notifyAll(),
– notify隨機喚醒一個線程,notifyAll喚醒所有線程,喚醒后的線程需要重新去競爭鎖,拿到鎖之后wait位置的代碼才會繼續執行。 - 使用小結
– wait和notify必須搭配synchronized一起使用
– wait和notify使用的鎖對象必須是同一個
– notify執行多少次都沒有關系(及時沒有wait)(類似老板把包子做好空喊了一聲) - 舉例:
– 現實舉例:
– 指令舉例