cookie里面用到的关键字_晓龙吊打面试官系列:synchronized关键字入门(同步方法与同步代码块)...
文章目錄
一、 線程安全問題
二、synchronized簡介
1) 原子性
2) 可見性
3) 有序性
4)可重入
1. 什么是synchronized
2.什么是同步
3.synchronized的特性
4.synchronized的實現(xiàn)原理(了解即可)
三、synchronized的用法
1. 同步方法
2. 同步代碼塊
四、對象鎖和類鎖
1)對象鎖
2)類鎖
1.對象鎖的探索
2.對象鎖的簡介
1.對象鎖的驗證
2.類鎖的驗證
3.對象鎖和類鎖的區(qū)別
一、 線程安全問題
線程安全問題使我們平時面試中總避不開會談論的一個點,通常情況下線程安全問題都是由于非方法內(nèi)的實例變量引起的。就比如我們舉個很簡單的例子:
在描述線程的相關Demo中,我喜歡用銀行相關的業(yè)務場景做舉例,大多數(shù)和線程相關的知識點都能對應到銀行需要用到的業(yè)務。就比如今天我會通過一個簡單的銀行排號系統(tǒng)的實現(xiàn)來解釋今天的知識點。
代碼實現(xiàn):
public class BankLineUp {// 設置一個當天最大服務人次
public static final Integer MAX_NUM = 500;
// 設置一個排號計數(shù)器
public static Integer count = 0;
// 創(chuàng)建一個內(nèi)部類 實現(xiàn)runnable接口
class LineUp implements Runnable {
// 叫號邏輯實現(xiàn)
@Override
public void run() {
// 輸出當前叫到的號碼
call();
}
}
// 提供構造方法
public LineUp lineUp() {
return new LineUp();
}
public static void main(String[] args) {
BankLineUp bankLineUp = new BankLineUp();
new Thread(bankLineUp.lineUp(), "一號窗口").start();
new Thread(bankLineUp.lineUp(), "二號窗口").start();
new Thread(bankLineUp.lineUp(), "三號窗口").start();
}
// 通過synchronized修飾方法實現(xiàn)同步方法
public void call() {
while (true) {
if (count < MAX_NUM) {
try {
// 通過休眠一秒 模擬系統(tǒng)延時
Thread.sleep(1l);
System.out.println("有請" + ++count + "號顧客到" + Thread.currentThread().getName() + "辦理業(yè)務!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
運行結果:
上面的案例我們模擬了因為系統(tǒng)可能存在的延時而導致了叫號系統(tǒng)處理了超過自己限制的數(shù)據(jù)量,這明顯是線程不安全的,那么我們有沒有簡單的方式可以解決這個小問題呢?
二、synchronized簡介
1. 什么是synchronized
synchronized的字面翻譯就是同步,Java通過引入synchronized的關鍵字來保證同一時間只有一個線程可以運行被synchronized保護的代碼。
2.什么是同步
同步指的是為了完成某種任務而建立的兩個或多個進程,這些進程因為要在某些位置上協(xié)調(diào)他們的工作次序而瞪大,傳遞信息所產(chǎn)生的制約關系,因此同步又叫做直接制約關系。在Java線程中的同步主要就體現(xiàn)在如果該資源為同步資源程,為了防止多個線程訪問該資源時對數(shù)據(jù)對象產(chǎn)生破壞,CPU在調(diào)度該資源時一次僅允許一個線程訪問修改。
3.synchronized的特性
1) 原子性
所謂的原子性指的是同一個或者多個操作,要么全部執(zhí)行,要么全部不執(zhí)行
2) 可見性
可見性就是指多個線程訪問同一個資源時,該資源狀態(tài)及變化對其他線程可見。
3) 有序性
有序性指的是CPU對于線程的執(zhí)行順序與代碼順序相同。
JAVA允許編譯器和處理器對于指令進行重排序,指令重排序并不會影響單線程的執(zhí)行結果。但是在多線程中,由于存在半初始化線程,指令重排會造成很難排查的線程安全問題。
4)可重入
當一個線程師徒操作一個由其他線程持有鎖的臨界資源時,將會出于阻塞狀態(tài),當一個線程再次請求自己持有對象鎖的資源時,無需等待,這種現(xiàn)象叫做可重入鎖。
4.synchronized的實現(xiàn)原理(了解即可)
synchronized是在對象頭里面存儲一個ACC_SYNCHRONIZED標識進行實現(xiàn)的,當JVM進入和退出一個Monitor對象的時候,會判斷此時的monitor是否被持有,如果被持有,則它將會處于鎖定狀態(tài)。
在匯編指令中,monitorenter和monitorexit分別代表獲得和釋放持有的monitor對象鎖
三、synchronized的用法
1. 同步方法
在剛剛的例子里,我們發(fā)現(xiàn)了共享變量在多線程訪問的情況下會出現(xiàn)線程安全問題,之后又引出了synchronized關鍵字,那么這個關鍵字該如何用來解決線程安全問題?我們可以看下面的一段代碼
package xiao.thread.synchronize;/**
* @Title: BankLineUp.java
* @Package xiao.thread.synchronize
* @Description: TODO
* @author: 曉
* @date: 2020年11月30日 上午10:15:36
* @version V1.0
* @Copyright: com.cdzg.com
*
*/
public class BankLineUp {
// 設置一個當天最大服務人次
public static final Integer MAX_NUM = 500;
// 設置一個排號計數(shù)器
public static Integer count = 0;
// 創(chuàng)建一個內(nèi)部類 實現(xiàn)runnable接口
class LineUp implements Runnable {
// 叫號邏輯實現(xiàn)
@Override
public void run() {
// 輸出當前叫到的號碼
call();
}
}
// 提供構造方法
public LineUp lineUp() {
return new LineUp();
}
public static void main(String[] args) {
BankLineUp bankLineUp = new BankLineUp();
new Thread(bankLineUp.lineUp(), "一號窗口").start();
new Thread(bankLineUp.lineUp(), "二號窗口").start();
new Thread(bankLineUp.lineUp(), "三號窗口").start();
}
// 通過synchronized修飾方法實現(xiàn)同步方法
public synchronized void call() {
while (true) {
if (count < MAX_NUM) {
try {
// 通過休眠一秒 模擬系統(tǒng)延時
Thread.sleep(1l);
System.out.println("有請" + ++count + "號顧客到" + Thread.currentThread().getName() + "辦理業(yè)務!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
執(zhí)行結果:
代碼變化:
我們僅僅是在call的方法體上加了一個synchronized關鍵字就實現(xiàn)了線程變量的保護,從而達到線程安全的目的,但是新的問題新出現(xiàn)了:
由于call方法被設置為了同步方法,此時二號線程和三號線程一直在等待一號線程執(zhí)行完才能拿到cpu的執(zhí)行權,但是所有的變量此時都被一號線程處理完畢,二號和三號線程并沒有實際的參與到代碼的運行中,由此可見當我們在將方法添加synchronized后,同步方法的鎖力度太大了,已經(jīng)影響了我們的運行效率,那么此時有沒有辦法對這個問題進行簡單的優(yōu)化呢?
2. 同步代碼塊
public class BankLineUp {// 設置一個當天最大服務人次
public static final Integer MAX_NUM = 500;
// 設置一個排號計數(shù)器
public static Integer count = 0;
// 創(chuàng)建一個內(nèi)部類 實現(xiàn)runnable接口
class LineUp implements Runnable {
// 叫號邏輯實現(xiàn)
@Override
public void run() {
// 輸出當前叫到的號碼
call();
}
}
// 提供構造方法
public LineUp lineUp() {
return new LineUp();
}
public static void main(String[] args) {
BankLineUp bankLineUp = new BankLineUp();
new Thread(bankLineUp.lineUp(), "一號窗口").start();
new Thread(bankLineUp.lineUp(), "二號窗口").start();
new Thread(bankLineUp.lineUp(), "三號窗口").start();
}
// 通過synchronized修飾方法實現(xiàn)同步方法
public void call() {
while (true) {
synchronized (this) {
if (count < MAX_NUM) {
try {
// 通過休眠一秒 模擬系統(tǒng)延時
Thread.sleep(1l);
System.out.println("有請" + ++count + "號顧客到" + Thread.currentThread().getName() + "辦理業(yè)務!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
運行結果:
在我們將call方法的同步粒度從整個方法體變成方法中的一個代碼塊的時候,有效的解決了線程等待方法運行結束才能獲得方法鎖的問題。那么此時有個小的疑問困擾著我,同步方法和同步方法體使用的是否是統(tǒng)一把鎖來控制同步代碼的運行???
四、對象鎖和類鎖
1.對象鎖的驗證
1.對象鎖的探索
為了驗證我的疑問,首先需要寫一個小的demo
public class ObjectLock {public static void main(String[] args) {
ObjectLock objectLock = new ObjectLock();
new Thread(() -> {
objectLock.call();
}, "一號線程").start();
new Thread(() -> {
objectLock.speak();
}, "二號線程").start();
}
// 同步方法 通過死循環(huán)使同步方法不會退出
public synchronized void call() {
System.out.println(Thread.currentThread().getName() + ":開始運行");
while (true) {
}
}
// 同步代碼塊
public void speak() {
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + ":開始運行");
// 通過sleep延長方法執(zhí)行時間
Thread.sleep(100_000);
System.out.println(Thread.currentThread().getName() + ":運行結束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
然后打開cmd窗口,輸入jps命令,拿到此時執(zhí)行線程的線程號
然后輸入jstack pid 命令查看線程狀態(tài):
在這里我們可以看到同步方法call()獲取了<0x0000000780676b40>這把鎖,而此時使用了同步代碼塊的方法speak()也在等待獲取<0x0000000780676b40>這把鎖,因此可以看出在一個對象中,同步方法和同步代碼塊使用的是同樣的鎖。在同步代碼塊中,我們傳入了一個參數(shù)this,它代表了我們?yōu)楫斍暗膶ο筇砑恿随i,也就是給同步代碼塊上了對象鎖。
類聲明后,我們可以 new 出來很多的實例對象。這時候,每個實例在 JVM 中都有自己的引用地址和堆內(nèi)存空間,這時候,我們就認為這些實例都是獨立的個體,很顯然,在實例上加的鎖和其他的實例就沒有關系,互不影響了。
2.對象鎖的簡介
對象鎖也叫方法鎖,是針對一個對象實例的,它只在該對象的某個內(nèi)存位置聲明一個標識該對象是否擁有鎖,所有它只會鎖住當前的對象,而并不會對其他對象實例的鎖產(chǎn)生任何影響。為了驗證對象鎖是否會影響其他對象這個事情,我們將main方法的對象實例進行更換
public static void main(String[] args) {ObjectLock objectLock = new ObjectLock();
ObjectLock objectLock2 = new ObjectLock();
new Thread(() -> {
objectLock.call();
}, "一號線程").start();
new Thread(() -> {
objectLock2.speak();
}, "二號線程").start();
}
運行結果:
此時二號線程可以無視一號線程持有鎖進行的死循環(huán)而直接運行,也就證明了我們說的對象鎖的影響范圍僅為當前對象?;蛘呶覀冞€可以采用下面將參數(shù)this更換為其他對象的方式解決:
synchronized (new String()) {
try {
System.out.println(Thread.currentThread().getName() + ":開始運行");
// 通過sleep延長方法執(zhí)行時間
Thread.sleep(100_000);
System.out.println(Thread.currentThread().getName() + ":運行結束");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
這里需要注意的一點:我在這里使用的是new String()?的方式來創(chuàng)建新的對象當做鎖,如果此時直接傳入的是一個字面量為:abc的字符串,那么此時該同步代碼塊所持有的將不再是對象鎖,而是類鎖。
2.類鎖的驗證
那么什么是類鎖?按照慣例,我們還是寫個小demo來做驗證:
public class ObjectLock {public static void main(String[] args) {
//分別創(chuàng)建兩個對象來調(diào)用同步方法 以避免對象鎖的干擾
ObjectLock objectLock01 = new ObjectLock();
ObjectLock objectLock02 = new ObjectLock();
new Thread(()->{objectLock01.test();},"一號線程").start();
new Thread(()->{objectLock02.test();},"二號線程").start();
}
public void test() {
synchronized (ObjectLock.class) {
System.out.println(Thread.currentThread().getName()+": start!");
while(true) {
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
還是用jstack命令來觀察:
此時二號線程和一號線程持有的鎖相同,證明此時所有ObjectLock對象的實例都持有該鎖。
類鎖是加載類上的,而類信息是存在 JVM 方法區(qū)的,并且整個 JVM 只有一份,方法區(qū)又是所有線程共享的,所以類鎖是所有線程共享的。
3.對象鎖和類鎖的區(qū)別
參考文章:https://zhuanlan.zhihu.com/p/98145713
1)對象鎖
通常我們使用實例鎖的方式有下面三種:
1、 鎖住實體里的非靜態(tài)變量:
非靜態(tài)變量是實例自身變量,不會與其他實例共享,所以鎖住實體內(nèi)聲明的非靜態(tài)變量可以實現(xiàn)對象鎖。鎖住同一個變量的方法塊共享同一把鎖。
2、鎖住 this 對象:
this 指的是當前對象實例本身,所以,所有使用 synchronized(this)方式的方法都共享同一把鎖。3、直接鎖非靜態(tài)方法
2)類鎖
類鎖是所有線程共享的鎖,所以同一時刻,只能有一個線程使用加了鎖的方法或方法體,不管是不是同一個實例。類鎖主要應用在下面的情況中:1、鎖住類中的靜態(tài)變量
因為靜態(tài)變量和類信息一樣也是存在方法區(qū)的并且整個 JVM 只有一份,所以加在靜態(tài)變量上可以達到類鎖的目的。2、直接在靜態(tài)方法上加 synchronized
因為靜態(tài)方法同樣也是存在方法區(qū)的并且整個 JVM 只有一份,所以加在靜態(tài)方法上可以達到類鎖的目的。3、鎖住 xxx.class
對當前類的 .class 屬性加鎖,可以實現(xiàn)類鎖。
總結
以上是生活随笔為你收集整理的cookie里面用到的关键字_晓龙吊打面试官系列:synchronized关键字入门(同步方法与同步代码块)...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java语言矩形与立方体的继承,沈阳师范
- 下一篇: linux如何运行verilog,lin