Java多線程編程(同步、死鎖、生產(chǎn)消費(fèi)者問(wèn)題)
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
作者 | xbhog
來(lái)源 | urlify.cn/Z3yMzu
關(guān)于線程同步以及死鎖問(wèn)題:
線程同步概念:是指若干個(gè)線程對(duì)象并行進(jìn)行資源的訪問(wèn)時(shí)實(shí)現(xiàn)的資源處理保護(hù)操作;
線程死鎖概念:是指兩個(gè)線程都在等待對(duì)方先完成,造成程序的停止的狀態(tài);
先了解相應(yīng)的概念,后面深入理解。
同步:
舉個(gè)例子:還是賣票問(wèn)題(經(jīng)典?)
不存在同步
開啟三個(gè)線程(售票員)測(cè)試
package com.xbhog;
class MyThread implements Runnable {// 定義線程執(zhí)行類
private int ticket = 3;// 總票數(shù)為6張
@Override
public void run() {
while (true) { // 持續(xù)賣票
if (this.ticket > 0) { // 還有剩余票
try {
Thread.sleep(100); // 模擬網(wǎng)絡(luò)延遲
} catch (InterruptedException e) {
e.printStackTrace();
}
//獲取當(dāng)前線程的名字
System.out.println(Thread.currentThread().getName() +
"賣票,ticket = " + this.ticket--);
} else {
System.out.println("***** 票已經(jīng)賣光了 *****");
break;// 跳出循環(huán)
}
}
}
}
public class Java多線程核心 {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "售票員A").start(); // 開啟賣票線程
new Thread(mt, "售票員B").start(); // 開啟賣票線程
new Thread(mt, "售票員C").start(); // 開啟賣票線程
}
}
結(jié)果:
| 售票員B賣票,ticket = 2 售票員C賣票,ticket = 3 售票員A賣票,ticket = 3 售票員A賣票,ticket = 1 售票員B賣票,ticket = -1 ***** 票已經(jīng)賣光了 售票員C賣票,ticket = 0 票已經(jīng)賣光了 票已經(jīng)賣光了 ***** | 售票員B賣票,ticket = 1 ***** 票已經(jīng)賣光了 售票員A賣票,ticket = 3 票已經(jīng)賣光了 售票員C賣票,ticket = 2 票已經(jīng)賣光了 ***** |
存在上述原因是因?yàn)樵诖a中兩個(gè)地方存在多線程訪問(wèn)時(shí)出現(xiàn)模糊的問(wèn)題:
this.ticket>0;
this,ticket--;
假設(shè)現(xiàn)在剩余的票數(shù)為1張;當(dāng)?shù)谝粋€(gè)線程滿足售票的條件的時(shí)候(此時(shí)還未減少票數(shù)),其他的線程也可能同時(shí)滿足售票的條件,這樣同時(shí)進(jìn)行自減減就可能造成負(fù)數(shù)!
解決上述問(wèn)題就需要采用線程同步技術(shù)實(shí)現(xiàn);
首先需要明確,在Java中實(shí)現(xiàn)線程同步(synchronized)的方法有兩個(gè):
同步代碼塊(同步策略加在方法內(nèi)部)
package com.xbhog.多線程1;
class MyThread implements Runnable { // 定義線程執(zhí)行類
private int ticket = 3; // 總票數(shù)為6張
@Override
public void run() {
while (true) { // 持續(xù)賣票
synchronized(this) { // 同步代碼塊
if (this.ticket > 0) { // 還有剩余票
try {
Thread.sleep(100); // 模擬網(wǎng)絡(luò)延遲
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"賣票,ticket = " + this.ticket--);
} else {
System.out.println("***** 票已經(jīng)賣光了 *****");
break; // 跳出循環(huán)
}
}
}
}
}
public class Java多線程同步代碼塊 {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt, "售票員A").start(); // 開啟賣票線程
new Thread(mt, "售票員B").start(); // 開啟賣票線程
new Thread(mt, "售票員C").start(); // 開啟賣票線程
}
}售票員A賣票,ticket = 3
售票員C賣票,ticket = 2
售票員B賣票,ticket = 1
***** 票已經(jīng)賣光了 *****
***** 票已經(jīng)賣光了 *****
***** 票已經(jīng)賣光了 *****同步方法(同步策略加在方法上)
class MyThread implements Runnable { // 定義線程執(zhí)行類
private int ticket = 3; // 總票數(shù)為6張
@Override
public void run() {
while (this.sale()) { // 調(diào)用同步方法
;
}
}
public synchronized boolean sale() { // 售票操作
if (this.ticket > 0) {
try {
Thread.sleep(100); // 模擬網(wǎng)絡(luò)延遲
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"賣票,ticket = " + this.ticket--);
return true;
} else {
System.out.println("***** 票已經(jīng)賣光了 *****");
return false;
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
MyThread mt = new MyThread();
new Thread(mt, "售票員A").start(); // 開啟賣票線程
new Thread(mt, "售票員B").start(); // 開啟賣票線程
new Thread(mt, "售票員C").start(); // 開啟賣票線程
}
}售票員A賣票,ticket = 3
售票員C賣票,ticket = 2
售票員B賣票,ticket = 1
***** 票已經(jīng)賣光了 *****
***** 票已經(jīng)賣光了 *****
***** 票已經(jīng)賣光了 *****
同步的本質(zhì):在同一個(gè)時(shí)間段只允許有一個(gè)線程執(zhí)行資源,所以在此線程對(duì)象未執(zhí)行完的過(guò)程中其他線程對(duì)象將處于等待的狀態(tài)。
同步的優(yōu)點(diǎn)與缺點(diǎn):
可以保證數(shù)據(jù)的準(zhǔn)確性
數(shù)據(jù)線程的訪問(wèn)安全
程序的處理性能下降
死鎖:
實(shí)例:
假如現(xiàn)在又張三想要李四的畫,李四想要張三的書,那么張三對(duì)李四說(shuō):把你的畫給我,我就給你書;
李四對(duì)張三說(shuō):把你的書給我,我就給你畫;
此時(shí):張三在等待李四,李四在等待張三,兩人一直等待下去形成死鎖;
觀察線程的死鎖:(實(shí)現(xiàn)張三李四)
package com.xbhog.死鎖;
class Book {
public synchronized void tell(Painting paint) { // 同步方法
System.out.println("張三對(duì)李四說(shuō):把你的畫給我,我就給你書,不給畫不給書!");
paint.get();
}
public synchronized void get() { // 同步方法
System.out.println("張三得到了李四的畫開始認(rèn)真欣賞。");
}
}
class Painting {
public synchronized void tell(Book book) { // 同步方法
System.out.println("李四對(duì)張三說(shuō):把你的書給我,我就給你畫,不給書不給畫!");
book.get();
}
public synchronized void get() { // 同步方法
System.out.println("李四得到了張三的書開始認(rèn)真閱讀。");
}
}
public class DeadLock implements Runnable{
private Book book = new Book();
private Painting paint = new Painting();
public DeadLock() {
new Thread(this).start();
book.tell(paint);
}
@Override
public void run() {
paint.tell(book);
}
public static void main(String[] args) {
new DeadLock() ;
}
}由于現(xiàn)在電腦的配置問(wèn)題,該代碼有可能在一次運(yùn)行中展示不出效果來(lái),需要多次運(yùn)行觀察效果;
效果圖:

由此引申出了生產(chǎn)者與消費(fèi)者模型。
生產(chǎn)者與消費(fèi)者問(wèn)題:
首先需要明確生產(chǎn)者與消費(fèi)者為兩個(gè)線程對(duì)象,是對(duì)同一資源進(jìn)行數(shù)據(jù)的保存與讀取;
基本操作是:生產(chǎn)者生產(chǎn)一個(gè)資源,消費(fèi)者則取走一個(gè)資源,一一對(duì)應(yīng)。
對(duì)應(yīng)類關(guān)系圖:

我們需要設(shè)想一個(gè)問(wèn)題,如果不加任何操作的話,會(huì)出現(xiàn)什么問(wèn)題?
數(shù)據(jù)錯(cuò)位:當(dāng)生產(chǎn)者線程只是開辟了一個(gè)棧空間保存信息名稱,在想存數(shù)據(jù)但是還沒(méi)存數(shù)據(jù)的時(shí)候切換到了消費(fèi)者線程上,那么消費(fèi)者線程將會(huì)把這個(gè)信息名稱與上個(gè)信息的內(nèi)容進(jìn)行結(jié)合聯(lián)系,這樣就造成了數(shù)據(jù)的錯(cuò)位。
重復(fù)數(shù)據(jù):當(dāng)生產(chǎn)者放了若干次的數(shù)據(jù),消費(fèi)者才開始取數(shù)據(jù),或者消費(fèi)者取完,但生產(chǎn)者還沒(méi)生產(chǎn)新數(shù)據(jù)時(shí)又取了直接已經(jīng)取過(guò)得數(shù)據(jù)。
解決以上兩個(gè)問(wèn)題需要涉及到以下兩個(gè)知識(shí)點(diǎn):
設(shè)置同步代碼塊或設(shè)置同步方法>>>解決數(shù)據(jù)錯(cuò)誤問(wèn)題
Object線程等待與喚醒>>>解決數(shù)據(jù)重復(fù)設(shè)置以及重復(fù)取出的問(wèn)題
增加數(shù)據(jù)同步方法或同步代碼塊:
在本程序中,生產(chǎn)者與消費(fèi)者代表的都是線程對(duì)象,所以同步操作只能在Message類中,可以將set與get方法設(shè)置為單獨(dú)的同步方法。
class Message {
private String title ; // 保存信息的標(biāo)題
private String content ; // 保存信息的內(nèi)容
public synchronized void set(String title, String content) {
this.title = title;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.content = content;
}
public synchronized String get() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.title + " --> " + this.content;
}
// setter、getter略
}
class Producer implements Runnable { // 定義生產(chǎn)者
private Message msg = null ;
public Producer(Message msg) {
this.msg = msg ;
}
@Override
public void run() {
for (int x = 0; x < 50; x++) { // 生產(chǎn)50次數(shù)據(jù)
if (x % 2 == 0) {
this.msg.set("xbhog","22") ; // 設(shè)置屬性
} else {
this.msg.set("xbhog","www.cnblog.cn/xbhog") ; // 設(shè)置屬性
}
}
}
}
class Consumer implements Runnable { // 定義消費(fèi)者
private Message msg = null ;
public Consumer (Message msg) {
this.msg = msg ;
}
@Override
public void run() {
for (int x = 0; x < 50; x++) { // 取走50次數(shù)據(jù)
System.out.println(this.msg.get()); // 取得屬性
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
Message msg = new Message() ; // 定義Message對(duì)象,用于保存和取出數(shù)據(jù)
new Thread(new Producer(msg)).start() ; // 啟動(dòng)生產(chǎn)者線程
new Thread(new Consumer(msg)).start() ; // 取得消費(fèi)者線程
}
}Object線程等待與喚醒機(jī)制:
線程的等待與喚醒只能依靠Object來(lái)完成,如果想要讓生產(chǎn)者與消費(fèi)者一個(gè)一個(gè)拿,一個(gè)一個(gè)取,那么需要加入標(biāo)志位來(lái)確定線程的當(dāng)前狀態(tài);
由圖所示:

當(dāng)生產(chǎn)者線程與消費(fèi)者線程進(jìn)入時(shí),判斷當(dāng)前的標(biāo)志位是否為true,
true:表示生產(chǎn)者可以生產(chǎn)資源,但是消費(fèi)者不能取走資源
false:表示生產(chǎn)者不能生產(chǎn)資源,但是消費(fèi)者需要取走資源
class Message {
private String title ;
private String content ;
private boolean flag = true; // 表示生產(chǎn)或消費(fèi)的形式
// flag = true:允許生產(chǎn),但是不允許消費(fèi)
// flag = false:允許消費(fèi),不允許生產(chǎn)
public synchronized void set(String title,String content) {
if (this.flag == false) { // 無(wú)法進(jìn)行生產(chǎn),等待被消費(fèi)
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.title = title ;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.content = content ;
this.flag = false ; // 已經(jīng)生產(chǎn)過(guò)了
super.notify(); // 喚醒等待的線程
}
public synchronized String get() {
if (this.flag == true) { // 還未生產(chǎn),需要等待
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
return this.title + " - " + this.content ;
} finally { // 不管如何都要執(zhí)行
this.flag = true ; // 繼續(xù)生產(chǎn)
super.notify(); // 喚醒等待線程
}
}
}在本程序中追加一個(gè)數(shù)據(jù)產(chǎn)生與消費(fèi)的控制邏輯成員屬性,通過(guò)此程序的值控制實(shí)現(xiàn)線程的等待與喚醒處理操作,從而解決線程重復(fù)操作的問(wèn)題。
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
??????

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