Java線程同步機制_動力節(jié)點Java學(xué)院整理
在之前,已經(jīng)學(xué)習(xí)到了線程的創(chuàng)建和狀態(tài)控制,但是每個線程之間幾乎都沒有什么太大的聯(lián)系??墒怯械臅r候,可能存在多個線程多同一個數(shù)據(jù)進行操作,這樣,可能就會引用各種奇怪的問題?,F(xiàn)在就來學(xué)習(xí)多線程對數(shù)據(jù)訪問的控制吧。
由于同一進程的多個線程共享同一片存儲空間,在帶來方便的同時,也帶來了訪問沖突這個嚴重的問題。Java語言提供了專門機制以解決這種沖突,有效避免了同一個數(shù)據(jù)對象被多個線程同時訪問。
一、多線程引起的數(shù)據(jù)訪問安全問題
下面看一個經(jīng)典的問題,銀行取錢的問題:
1)、你有一張銀行卡,里面有5000塊錢,然后你到取款機取款,取出3000,當正在取的時候,取款機已經(jīng)查詢到你有5000塊錢,然后正準備減去300塊錢的時候
2)、你的老婆拿著那張銀行卡對應(yīng)的存折到銀行取錢,也要取3000.然后銀行的系統(tǒng)查詢,存折賬戶里還有6000(因為上面錢還沒扣),所以它也準備減去3000,
3)、你的卡里面減去3000,5000-3000=2000,并且你老婆的存折也是5000-3000=2000。
4)、結(jié)果,你們一共取了6000,但是卡里還剩下2000。
下面看程序的模擬過程:
package com.bjpowernode.test;
public class GetMoneyTest {
public static void main(String[] args) {
Account account = new Account(5000);
GetMoneyRun runnable = new GetMoneyRun(account);
new Thread(runnable, "你").start();
new Thread(runnable, "你老婆").start();
}
}
// 賬戶Mode
class Account {
private int money;
public Account(int money) {
super();
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
}
//runnable類
class GetMoneyRun implements Runnable {
private Account account;
public GetMoneyRun(Account account) {
this.account = account;
}
@Override
public void run() {
if (account.getMoney() > 3000) {
System.out.println(Thread.currentThread().getName() + "的賬戶有"
+ account.getMoney() + "元");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
int lasetMoney=account.getMoney() - 3000;
account.setMoney(lasetMoney);
System.out.println(Thread.currentThread().getName() + "取出來了3000元"
+ Thread.currentThread().getName() + "的賬戶還有"
+ account.getMoney() + "元");
} else {
System.out.println("余額不足3000" + Thread.currentThread().getName()
+ "的賬戶只有" + account.getMoney() + "元");
}
}
}
多次運行程序,可以看到有多種不同的結(jié)果,下面是其中的三種:
1. 你的賬戶有5000元
2. 你老婆的賬戶有5000元
3. 你老婆取出來了3000元你老婆的賬戶還有2000元
4. 你取出來了3000元你的賬戶還有-1000元
1. 你的賬戶有5000元
2. 你老婆的賬戶有5000元
3. 你老婆取出來了3000元你老婆的賬戶還有-1000元
4. 你取出來了3000元你的賬戶還有-1000元
1. 你的賬戶有5000元
2. 你老婆的賬戶有5000元
3. 你老婆取出來了3000元你老婆的賬戶還有2000元
4. 你取出來了3000元你的賬戶還有2000元
可以看到,由于有兩個線程同時訪問這個account對象,導(dǎo)致取錢發(fā)生的賬戶發(fā)生問題。當多個線程訪問同一個數(shù)據(jù)的時候,非常容易引發(fā)問題。為了避免這樣的事情發(fā)生,我們要保證線程同步互斥,所謂同步互斥就是:并發(fā)執(zhí)行的多個線程在某一時間內(nèi)只允許一個線程在執(zhí)行以訪問共享數(shù)據(jù)。
二、同步互斥鎖
同步鎖的原理:Java中每個對象都有一個內(nèi)置同步鎖。Java中可以使用synchronized關(guān)鍵字來取得一個對象的同步鎖。synchronized的使用方式,是在一段代碼塊中,加上synchronized(object){ ... }
例如,有一個show方法,里面有synchronized的代碼段:
public void show() {
synchronized(object){
......
}
}
這其中的object可以使任何對象,表示當前線程取得該對象的鎖。一個對象只有一個鎖,所以其他任何線程都不能訪問該對象的所有由synchronized包括的代碼段,直到該線程釋放掉這個對象的同步鎖(釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊)。
注意:synchronized使用方式有幾個要注意的地方(還是以上面的show方法舉例):
①、取得同步鎖的對象為this,即當前類對象,這是使用的最多的一種方式
public void show() {
synchronized(this){
......
}
}
②、將synchronized加到方法上,這叫做同步方法,相當于第一種方式的縮寫
public synchronized void show() {
}
③、靜態(tài)方法的同步
public static synchronized void show() {
}
相當于
public static void show() {
synchronized(當前類名.class)
}
相當于取得類對象的同步鎖,注意它和取得一個對象的同步鎖不一樣
明白了同步鎖的原理和synchronized關(guān)鍵字的使用,那么解決上面的取錢問題就很簡單了,我們只要對run方法里面加上synchronized關(guān)鍵字就沒有問題了,如下:
@Override
public void run() {
synchronized (account) {
if (account.getMoney() > 3000) {
System.out.println(Thread.currentThread().getName() + "的賬戶有"
+ account.getMoney() + "元");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
int lasetMoney = account.getMoney() - 3000;
account.setMoney(lasetMoney);
System.out.println(Thread.currentThread().getName()
+ "取出來了3000元" + Thread.currentThread().getName()
+ "的賬戶還有" + account.getMoney() + "元");
} else {
System.out.println("余額不足3000"
+ Thread.currentThread().getName() + "的賬戶只有"
+ account.getMoney() + "元");
}
}
}
當甲線程執(zhí)行run方法的時候,它使用synchronized (account)取得了account對象的同步鎖,那么只要它沒釋放掉這個鎖,那么當乙線程執(zhí)行到run方法的時候,它就不能獲得繼續(xù)執(zhí)行的鎖,所以只能等甲線程執(zhí)行完,然后釋放掉鎖,乙線程才能繼續(xù)執(zhí)行。
synchronized關(guān)鍵字使用要注意以下幾點:
1)、只能同步方法和代碼塊,而不能同步變量和類。只要保護好類中數(shù)據(jù)的安全訪問和設(shè)置就可以了,不需要對類使用synchronized關(guān)鍵字,所以Java不允許這么做。并且想要同步數(shù)據(jù),只需要對成員變量私有化,然后同步方法即可,不需要對成員變量使用synchronized,java也禁止這么做。
2)、每個對象只有一個同步鎖;當提到同步時,應(yīng)該清楚在什么上同步?也就是說,在哪個對象上同步?上面的代碼中run方法使用synchronized (account)代碼塊,因為兩個線程訪問的都是同一個Account對象,所以能夠鎖定。但是如果是其他的一個無關(guān)的對象,就沒用了。比如說synchronized (new Date())代碼塊,一樣沒有效果。
3)、不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
4)、如果兩個線程要執(zhí)行一個類中的synchronized方法,并且兩個線程使用相同的實例來調(diào)用方法,那么一次只能有一個線程能夠執(zhí)行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個線程在對象上獲得一個鎖,就沒有任何其他線程可以進入(該對象的)類中的任何一個同步方法。
5)、如果線程擁有同步和非同步方法,則非同步方法可以被多個線程自由訪問而不受鎖的限制。
6)、線程睡眠時,它所持的任何同步鎖都不會釋放。
7)、線程可以獲得多個同步鎖。比如,在一個對象的同步方法里面調(diào)用另外一個對象的同步方法,則獲取了兩個對象的同步同步鎖。
8)、同步損害并發(fā)性,應(yīng)該盡可能縮小同步范圍。同步不但可以同步整個方法,還可以同步方法中一部分代碼塊。
9)、編寫線程安全的代碼會使系統(tǒng)的總體效率會降低,要適量使用
一個線程取得了同步鎖,那么在什么時候才會釋放掉呢?
1、同步方法或代碼塊正常結(jié)束
2、使用return或 break終止了執(zhí)行,或者跑出了未處理的異常。
3、當線程執(zhí)行同步方法或代碼塊時,程序執(zhí)行了同步鎖對象的wait()方法。
三、死鎖
死鎖:多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,因此程序不能正常運行。簡單的說就是:線程死鎖時,第一個線程等待第二個線程釋放資源,而同時第二個線程又在等待第一個線程釋放資源。這里舉一個通俗的例子:如在人行道上兩個人迎面相遇,為了給對方讓道,兩人同時向一側(cè)邁出一步,雙方無法通過,又同時向另一側(cè)邁出一步,這樣還是無法通過。假設(shè)這種情況一直持續(xù)下去,這樣就會發(fā)生死鎖現(xiàn)象。
導(dǎo)致死鎖的根源在于不適當?shù)剡\用“synchronized”關(guān)鍵詞來管理線程對特定對象的訪問。“synchronized”關(guān)鍵詞的作用是,確保在某個時刻只有一個線程被允許執(zhí)行特定的代碼塊,因此,被允許執(zhí)行的線程首先必須擁有對變量或?qū)ο蟮呐潘栽L問權(quán)。當線程訪問對象時,線程會給對象加鎖,而這個鎖導(dǎo)致其它也想訪問同一對象的線程被阻塞,直至第一個線程釋放它加在對象上的鎖。
一個死鎖的造成很簡單,比如有兩個對象A 和 B 。第一個線程鎖住了A,然后休眠1秒,輪到第二個線程執(zhí)行,第二個線程鎖住了B,然后也休眠1秒,然后有輪到第一個線程執(zhí)行。第一個線程又企圖鎖住B,可是B已經(jīng)被第二個線程鎖定了,所以第一個線程進入阻塞狀態(tài),又切換到第二個線程執(zhí)行。第二個線程又企圖鎖住A,可是A已經(jīng)被第一個線程鎖定了,所以第二個線程也進入阻塞狀態(tài)。就這樣,死鎖造成了。
舉個例子:
package com.bjpowernode.test;
public class DeadLock2 {
public static void main(String[] args) {
Object object1=new Object();
Object object2=new Object();
new Thread(new T(object1,object2)).start();
new Thread(new T(object2,object1)).start();
}
}
class T implements Runnable{
private Object object1;
private Object object2;
public T(Object object1,Object object2) {
this.object1=object1;
this.object2=object2;
}
public void run() {
synchronized (object1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
System.out.println("無法執(zhí)行到這一步");
}
}
};
}
上面的就是個死鎖。
第一個線程首先鎖住了object1,然后休眠。接著第二個線程鎖住了object2,然后休眠。在第一個線程企圖在鎖住object2,進入阻塞。然后第二個線程企圖在鎖住object1,進入阻塞。死鎖了。
四、線程的協(xié)調(diào)運行
關(guān)于線程的協(xié)調(diào)運行,經(jīng)典的例子就是生產(chǎn)者和消費者的問題。比如有生產(chǎn)者不斷的生產(chǎn)饅頭,放入一個籃子里,而消費者不斷的從籃子里拿饅頭吃。并且,當籃子滿的時候,生產(chǎn)者通知消費者來吃饅頭,并且自己等待不在生產(chǎn)饅頭。當籃子沒滿的的時候,由消費者通知生產(chǎn)者生產(chǎn)饅頭。這樣不斷的循環(huán)。
要完成上面的功能,光靠我們前面的同步等知識,是不能完成的。而是要用到線程間的協(xié)調(diào)運行。頂級父類Object中有3種方法來控制線程的協(xié)調(diào)運行。
notify、notifyAll、wait。其中wait有3個重載的方法。
這三個方法必須由同步監(jiān)視器對象(即線程獲得的鎖對象)來調(diào)用,這可分為兩種情況:
1、對于使用synchronized修飾的同步代碼塊,因為當前的類對象(this)就是同步監(jiān)視器,所以可以再同步方法中直接調(diào)用這三個方法。
2、對于使用synchronized修飾的同步代碼塊,同步監(jiān)視器是synchronized后括號的對象,所以必須使用該對象調(diào)用這三個方法。
wait(): 導(dǎo)致當前線程等待,直到其他線程調(diào)用該同步監(jiān)視器的notify()方法或notifyAll()方法來喚醒該線程。wait()方法有三種形式:無時間參數(shù)的wait(一直等待,直到其他線程通知),帶毫秒?yún)?shù)的wait和帶毫秒、微秒?yún)?shù)的wait(這兩種方法都是等待指定時間后自動蘇醒)。調(diào)用wait()方法的當前線程會釋放對該同步監(jiān)視器的鎖定。
notify(): 喚醒在此同步監(jiān)視器上等待的單個線程。如果所有線程都在此同步監(jiān)視器上等待,則會選擇幻想其中一個線程。選擇是任意性。只有當前線程放棄對該同步監(jiān)視器的鎖定后(使用wait()方法),才可以執(zhí)行被喚醒的其他線程。
notifyAll():喚醒在此同步監(jiān)視器上等待的所有線程。只有當前線程放棄對該同步監(jiān)視器的鎖定后,才可以執(zhí)行被喚醒的線程。
因為使用wait、notify和notifyAll三個方法一定是在同步代碼塊中使用的,所以一定要明白下面幾點:
1、如果兩個線程是因為都要得到同一個對象的鎖,而導(dǎo)致其中一個線程進入阻塞狀態(tài)。那么只有等獲得鎖的線程執(zhí)行完畢,或者它執(zhí)行了該鎖對象的wait方法,阻塞的線程才會有機會得到鎖,繼續(xù)執(zhí)行同步代碼塊。
2、使用wait方法進入等待狀態(tài)的線程,會釋放掉鎖。并且只有其他線程調(diào)用notify或者notifyAll方法,才會被喚醒。要明白,線程因為鎖阻塞和等待是不同的,因為鎖進入阻塞狀態(tài),會在其他線程釋放鎖的時候,得到鎖在執(zhí)行。而等待狀態(tài)必須要靠別人喚醒,并且喚醒了也不一定會立刻執(zhí)行,有可能因為notifyAll方法使得很多線程被喚醒,多個線程等待同一個鎖,而進入阻塞狀態(tài)。還可能是調(diào)用notify的線程依然沒有釋放掉鎖,只有等他執(zhí)行完了,其他線程才能去爭奪這個鎖。
看下面的例子:
package com.bjpowernode.test;
public class ThreadA {
public static void main(String[] args) {
RunnableTest myRunnanle=new RunnableTest();
new Thread(myRunnanle).start();
synchronized (myRunnanle) {
try {
System.out.println("第一步");
myRunnanle.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第四步");
}
}
}
class RunnableTest implements Runnable {
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
System.out.println("第二步");
notify();
System.out.println("第三步");
}
}
}
有兩個線程,主線程和我們自己新建的子線程。一步步的分析程序的執(zhí)行:
1、因為子線程啟動后,調(diào)用了sleep,所以主線程先進入同步代碼塊,而子線程之后因為沒有鎖,會進入阻塞狀態(tài)。
2、主線程的同步代碼塊執(zhí)行,打印第一句話,然后調(diào)用wait方法,進入等待狀態(tài)。因為進入了等待狀態(tài),所以釋放掉了鎖,所以子線程可以獲得鎖,開始執(zhí)行。
3、子線程執(zhí)行,打印第二句話,然后調(diào)用notify方法,將主線程喚醒。可是子線程并沒有結(jié)束,依然持有鎖,所以主線程不得不進入阻塞狀態(tài),等待這個鎖。
4、子線程打印第三句話,然后線程正常運行結(jié)束,釋放掉鎖。然后主線程得到了鎖,從阻塞進入運行狀態(tài),打印第四句話。
5、完畢
在看一個關(guān)于上面提到的生產(chǎn)者和消費者的例子:
首先,是生產(chǎn)物品的Mode,這里以饅頭舉例:
// 饅頭的實例
class ManTou {
private int id;// 饅頭的id
public ManTou(int id) {
this.id = id;
}
public String toString(){
return "ManTou"+id;
}
}
共享對象,生產(chǎn)者生產(chǎn)的饅頭放入其中,消費者從里面拿出饅頭,這里以籃子舉例:
// 籃子的實例,用來放饅頭
class Basket{
private int index = 0;// 表示裝到第幾個了饅頭
private ManTou[] manTous = new ManTou[6];// 可以放6個饅頭
// 放進去一個饅頭
public synchronized void push(ManTou manTou) {
while(index==manTous.length){
try {
System.out.println("籃子滿了!");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"生產(chǎn)"+manTou.toString());
this.notify();
manTous[index] = manTou;
index++;
}
// 拿一個饅頭
public synchronized ManTou pop() {
while (index==0) {
try {
System.out.println("籃子空了!");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ManTou manTou=manTous[--index];
System.out.println(Thread.currentThread().getName()+"吃了"+manTou.toString());
this.notify();
return manTou;
}
}
生產(chǎn)者:
// 生產(chǎn)者,生產(chǎn)饅頭
class Producer implements Runnable {
private BasketBall basketBall;
public Producer(BasketBall basketBall) {
this.basketBall = basketBall;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ManTou manTou = new ManTou(i);// 生產(chǎn)饅頭
basketBall.push(manTou);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消費者,拿饅頭吃
class Consumer implements Runnable {
private BasketBall basketBall;
public Consumer(BasketBall basketBall) {
this.basketBall = basketBall;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ManTou manTou=basketBall.pop();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
測試:
public class ProducerConsumer {
public static void main(String[] args) {
BasketBall basketBall=new BasketBall();
new Thread(new Producer(basketBall)).start();
new Thread(new Consumer(basketBall)).start();
}
}
以上所述是小編給大家介紹的Java線程同步機制,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
Java實現(xiàn)獲取客戶端真實IP方法小結(jié)
本文給大家匯總介紹了2種使用java實現(xiàn)獲取客戶端真實IP的方法,主要用于獲取使用了代理訪問的來訪者的IP,有需要的小伙伴可以參考下。2016-03-03
基于servlet實現(xiàn)統(tǒng)計網(wǎng)頁訪問次數(shù)
這篇文章主要為大家詳細介紹了基于servlet實現(xiàn)統(tǒng)計網(wǎng)頁訪問次數(shù),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02
java使用list實現(xiàn)數(shù)據(jù)庫的like功能
這篇文章主要介紹了java使用list實現(xiàn)數(shù)據(jù)庫的like功能,需要的朋友可以參考下2014-04-04
關(guān)于TreeMap自定義排序規(guī)則的兩種方式
這篇文章主要介紹了關(guān)于TreeMap自定義排序規(guī)則的兩種方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08
詳解Spring-boot中讀取config配置文件的兩種方式
這篇文章主要介紹了詳解Spring-boot中讀取config配置文件的兩種方式,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10

