同步鎖-線程安全問題解決方案
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
1 同步鎖
1.1 前言
經(jīng)過前面多線程編程的學(xué)習(xí),我們遇到了線程安全的相關(guān)問題,比如多線程售票情景下的超賣/重賣現(xiàn)象.
我們?nèi)绾闻袛喑绦蛴袥]有可能出現(xiàn)線程安全問題,主要有以下三個(gè)條件:
在多線程程序中 + 有共享數(shù)據(jù) + 多條語句操作共享數(shù)據(jù)
多線程的場景和共享數(shù)據(jù)的條件是改變不了的(就像4個(gè)窗口一起賣100張票,這個(gè)是業(yè)務(wù))
所以思路可以從第3點(diǎn)"多條語句操作共享數(shù)據(jù)"入手,既然是在這多條語句操作數(shù)據(jù)過程中出現(xiàn)了問題
那我們可以把有可能出現(xiàn)問題的代碼都包裹起來,一次只讓一個(gè)線程來執(zhí)行
1.2 同步與異步
那怎么"把有可能出現(xiàn)問題的代碼都包裹起來"呢?我們可以使用synchronized關(guān)鍵字來實(shí)現(xiàn)同步效果
也就是說,當(dāng)多個(gè)對(duì)象操作共享數(shù)據(jù)時(shí),可以使用同步鎖解決線程安全問題,被鎖住的代碼就是同步的
接下來介紹下同步與異步的概念:
同步:體現(xiàn)了排隊(duì)的效果,同一時(shí)刻只能有一個(gè)線程獨(dú)占資源,其他沒有權(quán)利的線程排隊(duì)。
壞處就是效率會(huì)降低,不過保證了安全。
異步:體現(xiàn)了多線程搶占資源的效果,線程間互相不等待,互相搶占資源。
壞處就是有安全隱患,效率要高一些。
1.3 synchronized同步關(guān)鍵字
1.3.1 寫法
synchronized (鎖對(duì)象){
需要同步的代碼(也就是可能出現(xiàn)問題的操作共享數(shù)據(jù)的多條語句);
}
1.3.2 前提
同步效果的使用有兩個(gè)前提:
前提1:同步需要兩個(gè)或者兩個(gè)以上的線程(單線程無需考慮多線程安全問題)
前提2:多個(gè)線程間必須使用同一個(gè)鎖(我上鎖后其他人也能看到這個(gè)鎖,不然我的鎖鎖不住其他人,就沒有了上鎖的效果)
1.3.3 特點(diǎn)
synchronized同步關(guān)鍵字可以用來修飾方法,稱為同步方法,使用的鎖對(duì)象是this
synchronized同步關(guān)鍵字可以用來修飾代碼塊,稱為同步代碼塊,使用的鎖對(duì)象可以任意
同步的缺點(diǎn)是會(huì)降低程序的執(zhí)行效率,但我們?yōu)榱吮WC線程的安全,有些性能是必須要犧牲的
但是為了性能,加鎖的范圍需要控制好,比如我們不需要給整個(gè)商場加鎖,試衣間加鎖就可以了
為什么同步代碼塊的鎖對(duì)象可以是任意的同一個(gè)對(duì)象,但是同步方法使用的是this呢?
因?yàn)橥酱a塊可以保證同一個(gè)時(shí)刻只有一個(gè)線程進(jìn)入
但同步方法不可以保證同一時(shí)刻只能有一個(gè)線程調(diào)用,所以使用本類代指對(duì)象this來確保同步

1.4 練習(xí)-改造售票案例
創(chuàng)建包: cn.tedu.thread
創(chuàng)建類:TestSaleTicketsV2.java
package cn.tedu.thread;
/**
* 本類用于改造售票案例,解決多線程編程安全問題
* 需求:4個(gè)線程共同賣100張票
* 問題1:出現(xiàn)了重賣現(xiàn)象:一張票賣給了多個(gè)人
* 問題2:出現(xiàn)了超賣現(xiàn)象:出現(xiàn)了0張/-1張/-2張這樣的現(xiàn)象
* */
public class TestSaleTicketsV2 {
public static void main(String[] args) {
//5.創(chuàng)建接口實(shí)現(xiàn)類對(duì)象作為目標(biāo)對(duì)象(目標(biāo)對(duì)象就是要做的業(yè)務(wù))
SaleTicketsV2 target = new SaleTicketsV2();
//6.將目標(biāo)對(duì)象與Thread線程對(duì)象綁定
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
Thread t4 = new Thread(target);
//7.以多線程的方式啟動(dòng)線程--會(huì)將線程由新建狀態(tài)變?yōu)榫途w狀態(tài)
/**1.如果只創(chuàng)建了一個(gè)線程對(duì)象,是單線程場景,不會(huì)出現(xiàn)數(shù)據(jù)問題*/
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/**2.多線程中出現(xiàn)數(shù)據(jù)不安全問題的前提:多線程程序 + 有共享數(shù)據(jù)(成員變量) +多條語句操作共享數(shù)據(jù)*/
/**3.同步鎖:相當(dāng)于給容易出現(xiàn)問題的代碼加了一把鎖,包裹了所有可能會(huì)出現(xiàn)問題的代碼
* 加鎖范圍:不能太大,也不能太小,太大,干啥都得排隊(duì),導(dǎo)致程序的效率太低,太小,沒鎖住,會(huì)有安全問題*/
//1.自定義售票類,通過實(shí)現(xiàn)Runnable接口的方式完成
class SaleTicketsV2 implements Runnable{
//2.定義靜態(tài)成員變量,用來保存票數(shù)
static int tickets = 100;
//9.2創(chuàng)建了一個(gè)唯一的"鎖"對(duì)象,不論之后那個(gè)線程進(jìn)同步代碼塊,使用的都是o對(duì)象,"唯一"很重要
Object o = new Object();
/**6.如果一個(gè)方法里的代碼都被同步了,可以直接把方法修飾成同步方法,同步方法用的鎖對(duì)象是this*/
//3.添加未實(shí)現(xiàn)方法run()--寫業(yè)務(wù)
@Override
//synchronized public void run() {//被synchronized修飾的方法是同步方法
public void run() {//被synchronized修飾的方法是同步方法
//4.1通過while(true)死循環(huán)的方式賣票,注意添加出口!!!
while(true) {
/**4.synchronized(鎖對(duì)象){}--同步代碼塊:是指同一時(shí)間這一資源會(huì)被一個(gè)線程獨(dú)享,大家使用的時(shí)候,都得排隊(duì),同步效果*/
/**5.鎖對(duì)象的要求:多線程之間必須使用同一把鎖,同步代碼塊的方式,關(guān)于鎖對(duì)象可以任意定義*/
//9.出現(xiàn)了線程安全問題,所以需要加同步代碼塊
//9.1這種寫法不對(duì),相當(dāng)于每個(gè)線程進(jìn)來一次new一個(gè)鎖對(duì)象,并不是使用的同一把鎖
//synchronized (new Object()) {
//9.3 使用同步代碼塊,鎖對(duì)象是o
synchronized (o) {
//4.2通過if結(jié)構(gòu)判斷賣票情況
if(tickets > 0) {//還有票,繼續(xù)售賣
//8.手動(dòng)添加休眠方法,創(chuàng)造延遲效果,注意可能會(huì)發(fā)生問題,需要由try-catch包裹
try {
Thread.sleep(10);//讓線程休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.3打印當(dāng)前賣票的線程名以及票數(shù)
System.out.println(Thread.currentThread().getName() + "=" + tickets--);
}
//4.4 沒票了,退出循環(huán),沒的賣了
if(tickets <= 0) break;
}
}
}
}
1.5 之前遇到過的同步例子
StringBuffer JDK1.0
加了synchronized ,性能相對(duì)較低(要排隊(duì),同步),安全性高
StringBuilder JDK1.5
去掉了synchronized,性能更高(不排隊(duì),異步),存在安全隱患

快速查找某個(gè)類的快捷鍵:Ctrl+Shift+T
2 線程創(chuàng)建的其他方式
2.1 ExecutorService/Executors
ExecutorService:用來存儲(chǔ)線程的池子,把新建線程/啟動(dòng)線程/關(guān)閉線程的任務(wù)都交給池來管理
execute(Runnable任務(wù)對(duì)象) 把任務(wù)丟到線程池
Executors 輔助創(chuàng)建線程池的工具類
newFixedThreadPool(int nThreads) 最多n個(gè)線程的線程池
newCachedThreadPool() 足夠多的線程,使任務(wù)不必等待
newSingleThreadExecutor() 只有一個(gè)線程的線程池
2.2 練習(xí):線程的其他創(chuàng)建方式
創(chuàng)建包: cn.tedu.thread
創(chuàng)建類: TestSaleTicketsV2.java
public class TestSaleTicketsV2 {
public static void main(String[] args) {
//5.創(chuàng)建接口實(shí)現(xiàn)類對(duì)象作為目標(biāo)對(duì)象(目標(biāo)對(duì)象就是要做的業(yè)務(wù))
SaleTicketsV2 target = new SaleTicketsV2();
//6.將目標(biāo)對(duì)象與Thread線程對(duì)象綁定
// Thread t1 = new Thread(target);
// Thread t2 = new Thread(target);
// Thread t3 = new Thread(target);
// Thread t4 = new Thread(target);
//7.以多線程的方式啟動(dòng)線程--會(huì)將線程由新建狀態(tài)變?yōu)榫途w狀態(tài)
/**1.如果只創(chuàng)建了一個(gè)線程對(duì)象,是單線程場景,不會(huì)出現(xiàn)數(shù)據(jù)問題*/
// t1.start();
// t2.start();
// t3.start();
// t4.start();
/**7.線程池ExecutorService:用來存儲(chǔ)線程的池子,把新建線程/啟動(dòng)線程/關(guān)閉線程的任務(wù)都交給池來管理*/
/**8.Executors用來輔助創(chuàng)建線程池對(duì)象,newFixedThreadPool()創(chuàng)建具有參數(shù)個(gè)數(shù)的線程數(shù)的線程池*/
ExecutorService pool = Executors.newFixedThreadPool(5);
for(int i = 0;i<5;i++) {
/**9.excute()讓線程池中的線程來執(zhí)行任務(wù),每次調(diào)用都會(huì)啟動(dòng)一個(gè)線程*/
pool.execute(target);//本方法的參數(shù)就是執(zhí)行的業(yè)務(wù),也就是實(shí)現(xiàn)類的目標(biāo)對(duì)象
}
}
3 拓展:線程鎖
3.1 悲觀鎖和樂觀鎖
悲觀鎖:像它的名字一樣,對(duì)于并發(fā)間操作產(chǎn)生的線程安全問題持悲觀狀態(tài).
悲觀鎖認(rèn)為競爭總是會(huì)發(fā)生,因此每次對(duì)某資源進(jìn)行操作時(shí),都會(huì)持有一個(gè)獨(dú)占的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。
樂觀鎖:還是像它的名字一樣,對(duì)于并發(fā)間操作產(chǎn)生的線程安全問題持樂觀狀態(tài).
樂觀鎖認(rèn)為競爭不總是會(huì)發(fā)生,因此它不需要持有鎖,將”比較-替換”這兩個(gè)動(dòng)作作為一個(gè)原子操作嘗試去修改內(nèi)存中的變量,如果失敗則表示發(fā)生沖突,那么就應(yīng)該有相應(yīng)的重試邏輯。
3.2 兩種常見的鎖
synchronized 互斥鎖(悲觀鎖,有罪假設(shè))
采用synchronized修飾符實(shí)現(xiàn)的同步機(jī)制叫做互斥鎖機(jī)制,它所獲得的鎖叫做互斥鎖。
每個(gè)對(duì)象都有一個(gè)monitor(鎖標(biāo)記),當(dāng)線程擁有這個(gè)鎖標(biāo)記時(shí)才能訪問這個(gè)資源,沒有鎖標(biāo)記便進(jìn)入鎖池。任何一個(gè)對(duì)象系統(tǒng)都會(huì)為其創(chuàng)建一個(gè)互斥鎖,這個(gè)鎖是為了分配給線程的,防止打斷原子操作。每個(gè)對(duì)象的鎖只能分配給一個(gè)線程,因此叫做互斥鎖。
ReentrantLock 排他鎖(悲觀鎖,有罪假設(shè))
ReentrantLock是排他鎖,排他鎖在同一時(shí)刻僅有一個(gè)線程可以進(jìn)行訪問,實(shí)際上獨(dú)占鎖是一種相對(duì)比較保守的鎖策略,在這種情況下任何“讀/讀”、“讀/寫”、“寫/寫”操作都不能同時(shí)發(fā)生,這在一定程度上降低了吞吐量。然而讀操作之間不存在數(shù)據(jù)競爭問題,如果”讀/讀”操作能夠以共享鎖的方式進(jìn)行,那會(huì)進(jìn)一步提升性能。
ReentrantReadWriteLock 讀寫鎖(樂觀鎖,無罪假設(shè))
因此引入了ReentrantReadWriteLock,顧名思義,ReentrantReadWriteLock是Reentrant(可重入)Read(讀)Write(寫)Lock(鎖),我們下面稱它為讀寫鎖。
讀寫鎖內(nèi)部又分為讀鎖和寫鎖,讀鎖可以在沒有寫鎖的時(shí)候被多個(gè)線程同時(shí)持有,寫鎖是獨(dú)占的。
讀鎖和寫鎖分離從而提升程序性能,讀寫鎖主要應(yīng)用于讀多寫少的場景。
3.3 嘗試用讀寫鎖改造售票案例
package cn.tedu.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 本類用于改造售票案例,使用可重入讀寫鎖
* ReentrantReadWriteLock
* */
public class TestSaleTicketsV3 {
public static void main(String[] args) {
SaleTicketsV3 target = new SaleTicketsV3();
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
Thread t4 = new Thread(target);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class SaleTicketsV3 implements Runnable{
static int tickets = 100;
//1.定義可重入讀寫鎖對(duì)象,靜態(tài)保證全局唯一
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
@Override
public void run() {
while(true) {
//2.在操作共享資源前上鎖
lock.writeLock().lock();
try {
if(tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + tickets--);
}
if(tickets <= 0) break;
} catch (Exception e) {
e.printStackTrace();
}finally {
//3.finally{}中釋放鎖,注意一定要手動(dòng)釋放,防止死鎖,否則就獨(dú)占報(bào)錯(cuò)了
lock.writeLock().unlock();
}
}
}
}
3.4 兩種方式的區(qū)別
需要注意的是,用sychronized修飾的方法或者語句塊在代碼執(zhí)行完之后鎖會(huì)自動(dòng)釋放,而是用Lock需要我們手動(dòng)釋放鎖,所以為了保證鎖最終被釋放(發(fā)生異常情況),要把互斥區(qū)放在try內(nèi),釋放鎖放在finally內(nèi)!
與互斥鎖相比,讀-寫鎖允許對(duì)共享數(shù)據(jù)進(jìn)行更高級(jí)別的并發(fā)訪問。雖然一次只有一個(gè)線程(writer 線程)可以修改共享數(shù)據(jù),但在許多情況下,任何數(shù)量的線程可以同時(shí)讀取共享數(shù)據(jù)(reader 線程)從理論上講,與互斥鎖定相比,使用讀-寫鎖允許的并發(fā)性增強(qiáng)將帶來更大的性能提高。
恭喜你,線程與線程鎖的學(xué)習(xí)可以暫時(shí)告一段落啦,接著我們可以繼續(xù)學(xué)習(xí)別的內(nèi)容
————————————————
版權(quán)聲明:本文為CSDN博主「程序媛 泡泡」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:
https://blog.csdn.net/weixin_43884234/article/details/115049704
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
??????

??長按上方微信二維碼 2 秒
感謝點(diǎn)贊支持下哈 
