程序猿學社的GitHub,歡迎Star
https://github.com/ITfqyd/cxyxs
本文已記錄到github,形成對應專題。
前言
在學習多線程的道路上,我們會經常看到線程安全這類詞彙,面試官也經常問,本文就來說一說什麼是線程安全。
1.什麼是線程安全?
多個線程同一時刻對同一個全局變量(同一份資源)做寫操作(讀操作不會涉及線程安全)時,如果跟我們預期的結果一樣,我們就稱之爲線程安全,反之,線程不安全。
- git應該大家都用過把,有github倉庫,還有本地庫,在項目開發過程中,我們經常會遇到衝突的問題,就是因爲,多個人同時對同一份資源進行了操作。
2.經典案例
代碼模擬業務
大家都搶過票,知道一到春運、過節的時候,票就很難搶,下面我們通過一段代碼,來模擬一下搶票的業務。
package com.cxyxs.thread.six;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/24 14:56
* Modified By:
*/
public class MyThread implements Runnable {
private int count=50;
@Override
public void run() {
while (count > 0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 搶到第"+count--+"張");
}
}
}
package com.cxyxs.thread.six;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/24 14:58
* Modified By:
*/
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread,"程序猿學社");
Thread thread1 = new Thread(myThread,"隔壁老王");
Thread thread2 = new Thread(myThread,"小張");
thread.start();
thread1.start();
thread2.start();
}
}
測試結果
小張: 搶到第50張
隔壁老王: 搶到第48張
程序猿學社: 搶到第49張
程序猿學社: 搶到第47張
小張: 搶到第46張
隔壁老王: 搶到第45張
程序猿學社: 搶到第44張
隔壁老王: 搶到第43張
小張: 搶到第42張
程序猿學社: 搶到第41張
隔壁老王: 搶到第40張
小張: 搶到第40張
程序猿學社: 搶到第39張
小張: 搶到第38張
隔壁老王: 搶到第38張
程序猿學社: 搶到第37張
隔壁老王: 搶到第36張
小張: 搶到第35張
程序猿學社: 搶到第34張
隔壁老王: 搶到第33張
小張: 搶到第33張
程序猿學社: 搶到第32張
小張: 搶到第31張
隔壁老王: 搶到第30張
程序猿學社: 搶到第29張
小張: 搶到第28張
隔壁老王: 搶到第27張
程序猿學社: 搶到第26張
隔壁老王: 搶到第25張
小張: 搶到第24張
程序猿學社: 搶到第23張
隔壁老王: 搶到第22張
小張: 搶到第21張
隔壁老王: 搶到第20張
程序猿學社: 搶到第19張
小張: 搶到第18張
程序猿學社: 搶到第17張
隔壁老王: 搶到第16張
小張: 搶到第15張
程序猿學社: 搶到第14張
隔壁老王: 搶到第13張
小張: 搶到第12張
隔壁老王: 搶到第11張
程序猿學社: 搶到第10張
小張: 搶到第9張
隔壁老王: 搶到第8張
小張: 搶到第7張
程序猿學社: 搶到第6張
隔壁老王: 搶到第5張
小張: 搶到第4張
程序猿學社: 搶到第3張
隔壁老王: 搶到第2張
程序猿學社: 搶到第1張
小張: 搶到第1張
隔壁老王: 搶到第0張
通過上面的測試結果,三個線程,同時搶票,有時候會搶到同一張票?爲什麼會有這種問題發現?
在回答這個問題之前,我們應該瞭解一下 Java 的內存模型(JMM),劃重點,也是面試官經常會問的一個問題。
什麼是JMM?
**JMM(Java Memory Model),**是一種基於計算機內存模型,保證了Java程序在各種平臺下對內存的訪問都能保證效果一致的機制及規範。保證共享內存的原子性、可見性、有序性(這三個也是多線程的三大特性,劃重點,面試經常問)。
本文就瞭解可見性就可。
可見性:
- 多線程操作共享內存時,執行結果能夠及時的同步到共享內存,確保其他線程對此結果及時可見。
看到這裏是不是還是有點懵,別急,我們通過圖,把之前搶票的業務畫出來。
同一進程下的多個線程,內存資源是共享的。主內存的count纔是共享資源。程序猿學社、隔壁老王、小張,實際上不是直接對主內存的count進行寫入操作。實際上,程序運行過程中,他們每個人,都有各自的工作內存。實際上就是把主內存的count,每個人,都copy一份,對各自的工作內存的變量進行操作。操作完後,再把對應的結果通知到主內存。 - 再回顧一下我之前git案例。有本地庫(工作內存),有github庫(主內存)。
- 在多個人同時過程中,組長會新建一個項目,其他的組員,是不是需要把代碼拉取下來,到本地。
- 我們開發完一個功能後,需要先提交本地庫,再提交到github(把工作內存的結果,提交給github。
- 提交代碼的時候,我們根本就無法知道,我這份代碼是不是最新的,所有有時候一提交,就報錯(可見性)。
說了這麼多,我們這時候應該知道之前寫的模擬搶票demo爲什麼會有線程安全問題了把。就是因爲各自都操作自己的工作內存,拿到主內存的值就開始操作。假設,這時候count爲40,同一時間,來了三個線程,那這三個線程的工作內存拿到的值都是40,這樣就會導致,這三個線程,都會搶到39這張票。
我們應該如何解決這個問題勒?
怎麼解決線程安全問題?
要想解決線程安全的問題,我們就需要解決一個問題,就是線程之間進行同步交互。瞭解可見性後,我們知道是沒有辦法相互操作對方的工作內存的。
一般有如下幾種方法
synchronized關鍵字(放在方法上)
同步代碼塊
jdk1.5的Lock
synchronized關鍵字(放在方法上)
package com.cxyxs.thread.six;
/**
* Description:通過同步代碼塊解決線程安全問題
* 轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/24 14:56
* Modified By:
*/
public class MySynchronizedThread implements Runnable {
private int count=50;
@Override
public void run() {
while(true){
buy();
}
}
public synchronized void buy(){
if(count>0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 搶到第"+count--+"張,"+System.currentTimeMillis());
}
}
}
package com.cxyxs.thread.six;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/24 14:58
* Modified By:
*/
public class Test {
public static void main(String[] args) {
//線程不安全
//MyThread myThread = new MyThread();
MySynchronizedThread myThread = new MySynchronizedThread();
Thread thread = new Thread(myThread,"程序猿學社");
Thread thread1 = new Thread(myThread,"隔壁老王");
Thread thread2 = new Thread(myThread,"小張");
thread.start();
thread1.start();
thread2.start();
}
}
測試結果
通過圖片我們可以發現,同一時間,搶票的間隔差不多都是50ms,爲什麼,不是說多線程嗎(前提不是單核)?
因爲在搶票的方法上,增加了synchronized,導致同一時候,只能有一個線程運行,需要等這個線程運行完後,下一個線程才能運行。
- 可以理解爲,有一個茅坑,裏面有四個坑,隔壁小王這個人,就怕別人偷窺他,直接把進茅坑的們直接鎖上,意思就是我在茅坑的時候,其他的都不能進茅坑,需要等隔壁小王,出來後,其他人才能進入。這樣的結果就會導致,大家都有意見,所以這種方式,一般很少使用。
同步代碼塊
這種方式就是利用synchronized+鎖對象
package com.cxyxs.thread.six;
/**
* Description:通過同步代碼塊解決線程安全問題
* 轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/24 14:56
* Modified By:
*/
public class SynchronizedBlockThread implements Runnable {
private int count=50;
private Object object = new Object();
@Override
public void run() {
while(true){
buy();
}
}
public void buy(){
synchronized (object){
if(count>0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 搶到第"+count--+"張,"+System.currentTimeMillis());
}
}
}
}
- 這種方式相對於前一種方式,性能有提升,只鎖了代碼塊,而不是把這個方法都鎖咯。
jdk1.5的Lock
package com.cxyxs.thread.six;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/24 16:52
* Modified By:
*/
public class LockThread implements Runnable {
private int count=50;
//定義鎖對象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
buy();
}
}
public void buy(){
lock.lock();
if(count>0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 搶到第"+count--+"張,"+System.currentTimeMillis());
}
lock.unlock();
}
}
jdk1.5lock重要的兩個方法
- lock(): 獲取鎖。
- unlock():釋放鎖。