Java多執行緒超詳解

Manito2020發表於2020-11-25

引言

隨著計算機的配置越來越高,我們需要將程式進一步優化,細分為執行緒,充分提高圖形化介面的多執行緒的開發。這就要求對執行緒的掌握很徹底。
那麼話不多說,今天本帥將記錄自己執行緒的學習。

程式,程式,執行緒的基本概念+並行與併發:

程式:是為完成特定任務,用某種語言編寫的一組指令的集合,即指一段靜態的程式碼,靜態物件。
程式:是程式的一次執行過程,或是正在執行的一個程式,是一個動態的過程,有它自身的產生,存在和消亡的過程。-------生命週期
執行緒:程式可進一步細化為執行緒,是一個程式內部的一條執行路徑

即:執行緒《執行緒(一個程式可以有多個執行緒)
程式:靜態的程式碼 程式:動態執行的程式
執行緒:程式中要同時幹幾件事時,每一件事的執行路徑成為執行緒。

並行:多個CPU同時執行多個任務,比如:多個人同時做不同的事
併發:一個CPU(採用時間片)同時執行多個任務,比如秒殺平臺,多個人做同件事

執行緒的相關API

//獲取當前執行緒的名字
Thread.currentThread().getName()

1.start():1.啟動當前執行緒2.呼叫執行緒中的run方法
2.run():通常需要重寫Thread類中的此方法,將建立的執行緒要執行的操作宣告在此方法中
3.currentThread():靜態方法,返回執行當前程式碼的執行緒
4.getName():獲取當前執行緒的名字
5.setName():設定當前執行緒的名字
6.yield():主動釋放當前執行緒的執行權
7.join():線上程中插入執行另一個執行緒,該執行緒被阻塞,直到插入執行的執行緒完全執行完畢以後,該執行緒才繼續執行下去
8.stop():過時方法。當執行此方法時,強制結束當前執行緒。
9.sleep(long millitime):執行緒休眠一段時間
10.isAlive():判斷當前執行緒是否存活

判斷是否是多執行緒

一條執行緒即為一條執行路徑,即當能用一條路徑畫出來時即為一個執行緒
例:如下看似既執行了方法一,又執行了方法2,但是其實質就是主執行緒在執行方法2和方法1這一條路徑,所以就是一個執行緒

public class Sample{
		public void method1(String str){
			System.out.println(str);
		}
	
	public void method2(String str){
		method1(str);
	}
	
	public static void main(String[] args){
		Sample s = new Sample();
		s.method2("hello");
	}
}

 

在這裡插入圖片描述

執行緒的排程

排程策略:
時間片:執行緒的排程採用時間片輪轉的方式
搶佔式:高優先順序的執行緒搶佔CPU
Java的排程方法:
1.對於同優先順序的執行緒組成先進先出佇列(先到先服務),使用時間片策略
2.對高優先順序,使用優先排程的搶佔式策略

執行緒的優先順序

等級:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5

方法:
getPriority():返回執行緒優先順序
setPriority(int newPriority):改變執行緒的優先順序

注意!:高優先順序的執行緒要搶佔低優先順序的執行緒的cpu的執行權。但是僅是從概率上來說的,高優先順序的執行緒更有可能被執行。並不意味著只有高優先順序的執行緒執行完以後,低優先順序的執行緒才執行。

多執行緒的建立方式

1. 方式1:繼承於Thread類

  1. 建立一個整合於Thread類的子類 (通過ctrl+o(override)輸入run查詢run方法)
    2.重寫Thread類的run()方法
    3.建立Thread子類的物件
    4.通過此物件呼叫start()方法


  2. 一次性搞明白Java多執行緒

  3. 學習資料,    http://www.cx1314.cn/thread-3531-1-1.html

start與run方法的區別:

start方法的作用:1.啟動當前執行緒 2.呼叫當前執行緒的重寫的run方法(在主執行緒中生成子執行緒,有兩條執行緒)
呼叫start方法以後,一條路徑代表一個執行緒,同時執行兩執行緒時,因為時間片的輪換,所以執行過程隨機分配,且一個執行緒物件只能呼叫一次start方法。
run方法的作用:在主執行緒中呼叫以後,直接在主執行緒一條執行緒中執行了該執行緒中run的方法。(呼叫執行緒中的run方法,只呼叫run方法,並不新開執行緒)

總結:我們不能通過run方法來新開一個執行緒,只能呼叫執行緒中重寫的run方法(可以線上程中不斷的呼叫run方法,但是不能開啟子執行緒,即不能同時幹幾件事),start是開啟執行緒,再呼叫方法(即預設開啟一次執行緒,呼叫一次run方法,可以同時執行幾件事)
在這裡插入圖片描述

多執行緒例子(火車站多視窗賣票問題)

	package com.example.paoduantui.Thread;
	
	import android.view.Window;
	
	/**
	 *
	 * 建立三個視窗賣票,總票數為100張,使用繼承自Thread方式
	 * 用靜態變數保證三個執行緒的資料獨一份
	 * 
	 * 存線上程的安全問題,有待解決
	 *
	 * */
	
	public class ThreadDemo extends Thread{
	
	    public static void main(String[] args){
	        window t1 = new window();
	        window t2 = new window();
	        window t3 = new window();
	
	        t1.setName("售票口1");
	        t2.setName("售票口2");
	        t3.setName("售票口3");
	
	        t1.start();
	        t2.start();
	        t3.start();
	    }
	
	}
	
	class window extends Thread{
	    private static int ticket = 100; //將其載入在類的靜態區,所有執行緒共享該靜態變數
	
	    @Override
	    public void run() {
	        while(true){
	            if(ticket>0){
	//                try {
	//                    sleep(100);
	//                } catch (InterruptedException e) {
	//                    e.printStackTrace();
	//                }
	                System.out.println(getName()+"當前售出第"+ticket+"張票");
	                ticket--;
	            }else{
	                break;
	            }
	        }
	    }
	}

 

2. 方式2:實現Runable介面方式

1.建立一個實現了Runable介面的類
2.實現類去實現Runnable中的抽象方法:run()
3.建立實現類的物件
4.將此物件作為引數傳遞到Thread類中的構造器中,建立Thread類的物件
5.通過Thread類的物件呼叫start()

具體操作,將一個類實現Runable介面,(插上介面一端)。
另外一端,通過實現類的物件與執行緒物件通過此Runable介面插上介面實現

	package com.example.paoduantui.Thread;
	
	public class ThreadDemo01 {
	    
	    public static  void main(String[] args){
	        window1 w = new window1();
	        
	        //雖然有三個執行緒,但是隻有一個視窗類實現的Runnable方法,由於三個執行緒共用一個window物件,所以自動共用100張票
	        
	        Thread t1=new Thread(w);
	        Thread t2=new Thread(w);
	        Thread t3=new Thread(w);
	
	        t1.setName("視窗1");
	        t2.setName("視窗2");
	        t3.setName("視窗3");
	        
	        t1.start();
	        t2.start();
	        t3.start();
	    }
	}
	
	class window1 implements Runnable{
	    
	    private int ticket = 100;
	
	    @Override
	    public void run() {
	        while(true){
	            if(ticket>0){
	//                try {
	//                    sleep(100);
	//                } catch (InterruptedException e) {
	//                    e.printStackTrace();
	//                }
	                System.out.println(Thread.currentThread().getName()+"當前售出第"+ticket+"張票");
	                ticket--;
	            }else{
	                break;
	            }
	        }
	    }
	}

 

比較建立執行緒的兩種方式:
開發中,優先選擇實現Runable介面的方式
原因1:實現的方式沒有類的單繼承性的侷限性
2:實現的方式更適合用來處理多個執行緒有共享資料的情況
聯絡:Thread也是實現自Runable,兩種方式都需要重寫run()方法,將執行緒要執行的邏輯宣告在run中

3.新增的兩種建立多執行緒方式

1.實現callable介面方式:

與使用runnable方式相比,callable功能更強大些:
runnable重寫的run方法不如callaalbe的call方法強大,call方法可以有返回值
方法可以丟擲異常
支援泛型的返回值
需要藉助FutureTask類,比如獲取返回結果

package com.example.paoduantui.Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
 * 建立執行緒的方式三:實現callable介面。---JDK 5.0新增
 *是否多執行緒?否,就一個執行緒
 *
 * 比runable多一個FutureTask類,用來接收call方法的返回值。
 * 適用於需要從執行緒中接收返回值的形式
 * 
 * //callable實現新建執行緒的步驟:
 * 1.建立一個實現callable的實現類
 * 2.實現call方法,將此執行緒需要執行的操作宣告在call()中
 * 3.建立callable實現類的物件
 * 4.將callable介面實現類的物件作為傳遞到FutureTask的構造器中,建立FutureTask的物件
 * 5.將FutureTask的物件作為引數傳遞到Thread類的構造器中,建立Thread物件,並呼叫start方法啟動(通過FutureTask的物件呼叫方法get獲取執行緒中的call的返回值)
 * 
 * */
//實現callable介面的call方法
class NumThread implements Callable{
    private int sum=0;//
    //可以丟擲異常
    @Override
    public Object call() throws Exception {
        for(int i = 0;i<=100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }
        return sum;
    }
}
public class ThreadNew {
    public static void main(String[] args){
        //new一個實現callable介面的物件
        NumThread numThread = new NumThread();
        //通過futureTask物件的get方法來接收futureTask的值
        FutureTask futureTask = new FutureTask(numThread);
        Thread t1 = new Thread(futureTask);
        t1.setName("執行緒1");
        t1.start();
        try {
            //get返回值即為FutureTask構造器引數callable實現類重寫的call的返回值
           Object sum = futureTask.get();
           System.out.println(Thread.currentThread().getName()+":"+sum);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

使用執行緒池的方式:

背景:經常建立和銷燬,使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。
思路:提前建立好多個執行緒,放入執行緒池之,使用時直接獲取,使用完放回池中。可以避免頻繁建立銷燬,實現重複利用。類似生活中的公共交通工具。(資料庫連線池)
好處:提高響應速度(減少了建立新執行緒的時間)
降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
便於執行緒管理
corePoolSize:核心池的大小
maximumPoolSize:最大執行緒數
keepAliveTime:執行緒沒有任務時最多保持多長時間後會終止
。。。。。。

JDK 5.0 起提供了執行緒池相關API:ExecutorService 和 Executors
ExecutorService:真正的執行緒池介面。常見子類ThreadPoolExecutor.
void execute(Runnable coommand):執行任務/命令,沒有返回值,一般用來執行Runnable
Futuresubmit(Callable task):執行任務,有返回值,一般又來執行Callable
void shutdown():關閉連線池。

Executors 工具類,執行緒池的工廠類,用於建立並返回不同型別的執行緒池
Executors.newCachedThreadPool() 建立一個可根據需要建立新執行緒的執行緒池
Executors.newFixedThreadPool(n) 建立一個可重用固定執行緒數的執行緒池
Executors.newSingleThreadExecutor() :建立一個只有一個執行緒的執行緒池
Executors.newScheduledThreadPool(n) 建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。

執行緒池構造批量執行緒程式碼如下:

package com.example.paoduantui.Thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 建立執行緒的方式四:使用執行緒池(批量使用執行緒)
 *1.需要建立實現runnable或者callable介面方式的物件
 * 2.建立executorservice執行緒池
 * 3.將建立好的實現了runnable介面類的物件放入executorService物件的execute方法中執行。
 * 4.關閉執行緒池
 *
 * */
class NumberThread implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i<=100;i++){
            if (i % 2 ==0 )
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i<100; i++){
            if(i%2==1){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
public class ThreadPool {
    public static void main(String[] args){
        //建立固定執行緒個數為十個的執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //new一個Runnable介面的物件
        NumberThread number = new NumberThread();
        NumberThread1 number1 = new NumberThread1();
        //執行執行緒,最多十個
        executorService.execute(number1);
        executorService.execute(number);//適合適用於Runnable
        //executorService.submit();//適合使用於Callable
        //關閉執行緒池
        executorService.shutdown();
    }
}

 

目前兩種方式要想呼叫新執行緒,都需要用到Thread中的start方法。

java virtual machine(JVM):java虛擬機器記憶體結構

程式(一段靜態的程式碼)——————》載入到記憶體中——————》程式(載入到記憶體中的程式碼,動態的程式)
程式可細分為多個執行緒,一個執行緒代表一個程式內部的一條執行路徑
每個執行緒有其獨立的程式計數器(PC,指導著程式向下執行)與執行棧(本地變數等,本地方法等)
在這裡插入圖片描述

 

 

執行緒通訊方法:

wait()/ notify()/ notifayAll():此三個方法定義在Object類中的,因為這三個方法需要用到鎖,而鎖是任意物件都能充當的,所以這三個方法定義在Object類中。

由於wait,notify,以及notifyAll都涉及到與鎖相關的操作
wait(在進入鎖住的區域以後阻塞等待,釋放鎖讓別的執行緒先進來操作)---- Obj.wait 進入Obj這個鎖住的區域的執行緒把鎖交出來原地等待通知
notify(由於有很多鎖住的區域,所以需要將區域用鎖來標識,也涉及到鎖) ----- Obj.notify 新執行緒進入Obj這個區域進行操作並喚醒wait的執行緒

有點類似於我要拉粑粑,我先進了廁所關了門,但是發現廁所有牌子寫著不能用,於是我把廁所鎖給了別人,別人進來拉粑粑還是修廁所不得而知,直到有人通知我廁所好了我再接著用。

所以wait,notify需要使用在有鎖的地方,也就是需要用synchronize關鍵字來標識的區域,即使用在同步程式碼塊或者同步方法中,且為了保證wait和notify的區域是同一個鎖住的區域,需要用鎖來標識,也就是鎖要相同的物件來充當

執行緒的分類:

java中的執行緒分為兩類:1.守護執行緒(如垃圾回收執行緒,異常處理執行緒),2.使用者執行緒(如主執行緒)

若JVM中都是守護執行緒,當前JVM將退出。(形象理解,脣亡齒寒)

執行緒的生命週期:

JDK中用Thread.State類定義了執行緒的幾種狀態,如下:

執行緒生命週期的階段 描述
新建 當一個Thread類或其子類的物件被宣告並建立時,新生的執行緒物件處於新建狀態
就緒 處於新建狀態的執行緒被start後,將進入執行緒佇列等待CPU時間片,此時它已具備了執行的條件,只是沒分配到CPU資源
執行 當就緒的執行緒被排程並獲得CPU資源時,便進入執行狀態,run方法定義了執行緒的操作和功能
阻塞 在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出CPU並臨時終止自己的執行,進入阻塞狀態
死亡 執行緒完成了它的全部工作或執行緒被提前強制性地中止或出現異常導致結束

在這裡插入圖片描述

執行緒的同步:在同步程式碼塊中,只能存在一個執行緒。

執行緒的安全問題:

什麼是執行緒安全問題呢?
執行緒安全問題是指,多個執行緒對同一個共享資料進行操作時,執行緒沒來得及更新共享資料,從而導致另外執行緒沒得到最新的資料,從而產生執行緒安全問題。

上述例子中:建立三個視窗賣票,總票數為100張票
1.賣票過程中,出現了重票(票被反覆的賣出,ticket未被減少時就列印出了)錯票。
2.問題出現的原因:當某個執行緒操作車票的過程中,尚未完成操作時,其他執行緒參與進來,也來操作車票。(將此過程的程式碼看作一個區域,當有執行緒進去時,裝鎖,不讓別的執行緒進去)
生動理解的例子:有一個廁所,有人進去了,但是沒有上鎖,於是別人不知道你進去了,別人也進去了對廁所也使用造成錯誤。
3.如何解決:當一個執行緒在操作ticket時,其他執行緒不能參與進來,直到此執行緒的生命週期結束
4.在java中,我們通過同步機制,來解決執行緒的安全問題。

方式一:同步程式碼塊
使用同步監視器(鎖)
Synchronized(同步監視器){
//需要被同步的程式碼
}
說明:

  1. 操作共享資料的程式碼(所有執行緒共享的資料的操作的程式碼)(視作衛生間區域(所有人共享的廁所)),即為需要共享的程式碼(同步程式碼塊,在同步程式碼塊中,相當於是一個單執行緒,效率低)
  2. 共享資料:多個執行緒共同操作的資料,比如公共廁所就類比共享資料
  3. 同步監視器(俗稱:鎖):任何一個的物件都可以充當鎖。(但是為了可讀性一般設定英文成lock)當鎖住以後只能有一個執行緒能進去(要求:多個執行緒必須要共用同一把鎖,比如火車上的廁所,同一個標誌表示有人)

Runable天生共享鎖,而Thread中需要用static物件或者this關鍵字或者當前類(window。class)來充當唯一鎖

方式二:同步方法
使用同步方法,對方法進行synchronized關鍵字修飾
將同步程式碼塊提取出來成為一個方法,用synchronized關鍵字修飾此方法。
對於runnable介面實現多執行緒,只需要將同步方法用synchronized修飾
而對於繼承自Thread方式,需要將同步方法用static和synchronized修飾,因為物件不唯一(鎖不唯一)

總結:1.同步方法仍然涉及到同步監視器,只是不需要我們顯示的宣告。
2.非靜態的同步方法,同步監視器是this
靜態的同步方法,同步監視器是當前類本身。繼承自Thread。class

方式三:JDK5.0新增的lock鎖方法

package com.example.paoduantui.Thread;
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
    private int ticket = 100;//定義一百張票
    //1.例項化鎖
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        
            while (true) {
                //2.呼叫鎖定方法lock
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "張票");
                    ticket--;
                } else {
                    break;
                }
            }
        }
}
public class LockTest {
    public static void main(String[] args){
       Window w= new Window();
       Thread t1 = new Thread(w);
       Thread t2 = new Thread(w);
       Thread t3 = new Thread(w);
       t1.setName("視窗1");
       t2.setName("視窗1");
       t3.setName("視窗1");
       t1.start();
       t2.start();
       t3.start();
    }
}

 

總結:Synchronized與lock的異同?

相同:二者都可以解決執行緒安全問題
不同:synchronized機制在執行完相應的程式碼邏輯以後,自動的釋放同步監視器
lock需要手動的啟動同步(lock()),同時結束同步也需要手動的實現(unlock())(同時以為著lock的方式更為靈活)

優先使用順序:
LOCK-》同步程式碼塊-》同步方法

判斷執行緒是否有安全問題,以及如何解決:

1.先判斷是否多執行緒
2.再判斷是否有共享資料
3.是否併發的對共享資料進行操作
4.選擇上述三種方法解決執行緒安全問題

例題:

	package com.example.paoduantui.Thread;
	
	/***
	 * 描述:甲乙同時往銀行存錢,存夠3000
	 *
	 *
	 * */
	
	//賬戶
	class Account{
	    private double balance;//餘額
	    //構造器
	    public Account(double balance) {
	        this.balance = balance;
	    }
	    //存錢方法
	    public synchronized void deposit(double amt){
	        if(amt>0){
	            balance +=amt;
	            try {
	                Thread.sleep(1000);
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	            System.out.println(Thread.currentThread().getName()+"存錢成功,餘額為:"+balance);
	        }
	    }
	}
	
	//兩個顧客執行緒
	class Customer extends Thread{
	     private Account acct;
	
	     public Customer(Account acct){
	         this.acct = acct;
	     }
	
	
	
	    @Override
	    public void run() {
	        for (int i = 0;i<3;i++){
	            acct.deposit(1000);
	        }
	    }
	}
	
	//主方法,之中new同一個賬戶,甲乙兩個存錢執行緒。
	public class AccountTest {
	
	    public static void main(String[] args){
	        Account acct = new Account(0);
	        Customer c1 = new Customer(acct);
	        Customer c2 = new Customer(acct);
	
	        c1.setName("甲");
	        c2.setName("乙");
	
	        c1.start();
	        c2.start();
	    }
	
	}

 

解決單例模式的懶漢式的執行緒安全問題:

單例:只能通過靜態方法獲取一個例項,不能通過構造器來構造例項
1.構造器的私有化:
private Bank(){}//可以在構造器中初始化東西
private static Bank instance = null;//初始化靜態例項

public static Bank getInstance(){
if(instance!=null){
instance = new Bank();
}
return instance;
}

假設有多個執行緒呼叫此單例,而呼叫的獲取單例的函式作為操作共享單例的程式碼塊並沒有解決執行緒的安全問題,會導致多個執行緒都判斷例項是否為空,此時就會導致多個例項的產生,也就是單例模式的執行緒安全問題。

解決執行緒安全問題的思路:

  1. 將獲取單例的方法改寫成同部方法,即加上synchronized關鍵字,此時同步監視器為當前類本身。(當有多個執行緒併發的獲取例項時,同時只能有一個執行緒獲取例項),解決了單例模式的執行緒安全問題。
  2. 用同步監視器包裹住同步程式碼塊的方式。

懶漢式單例模式的模型,例如:生活中的限量版的搶購:
當一群人併發的搶一個限量版的東西的時候,可能同時搶到了幾個人,他們同時進入了房間(同步程式碼塊內)
但是隻有第一個拿到限量版東西的人才能到手,其餘人都不能拿到,所以效率稍高的做法是,當東西被拿走時,我們在門外立一塊牌子,售罄。
這樣就減少了執行緒的等待。即下面效率稍高的懶漢式寫法:

package com.example.paoduantui.Thread;
public class Bank {
    //私有化構造器
    private Bank(){}
    //初始化靜態例項化物件
    private static  Bank instance = null;
    //獲取單例例項,此種懶漢式單例模式存線上程不安全問題(從併發考慮)
    public static  Bank getInstance(){
        if(instance==null){
            instance = new Bank();
        }
        return  instance;
    }
    //同步方法模式的執行緒安全
    public static synchronized Bank getInstance1(){
        if(instance==null){
            instance = new Bank();
        }
        return  instance;
    }
    //同步程式碼塊模式的執行緒安全(上鎖)
    public  static Bank getInstance2(){
        synchronized (Bank.class){
            if(instance==null){
                instance = new Bank();
            }
            return  instance;
        }
    }
    
    //效率更高的執行緒安全的懶漢式單例模式
    /**
     * 由於當高併發呼叫單例模式的時候,類似於萬人奪寶,只有第一個進入房間的人才能拿到寶物,
     * 當多個人進入這個房間時,第一個人拿走了寶物,也就另外幾個人需要在同步程式碼塊外等候,
     * 剩下的人只需要看到門口售罄的牌子即已知寶物已經被奪,可以不用進入同步程式碼塊內,提高了效率。
     * 
     * 
     * */
    public static Bank getInstance3(){
        if (instance==null){
            synchronized (Bank.class){
                if(instance==null){
                    instance = new Bank();
                }
            }
        }
        return  instance;
    }
}

 

  • 52
  • 53

執行緒的死鎖問題:

執行緒死鎖的理解:僵持,誰都不放手,一雙筷子,我一隻你一隻,都等對方放手(死鎖,兩者都進入阻塞,誰都吃不了飯,進行不了下面吃飯的操作)
出現死鎖以後,不會出現提示,只是所有執行緒都處於阻塞狀態,無法繼續

package com.example.paoduantui.Thread;
/**
 * 演示執行緒的死鎖問題
 *
 * */
public class Demo {
    public static void main(String[] args){
        final StringBuffer s1 = new StringBuffer();
        final StringBuffer s2 = new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                //先拿鎖一,再拿鎖二
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        //使用匿名內部類實現runnable介面的方式實現執行緒的建立
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

 

執行結果:
1.先呼叫上面的執行緒,再呼叫下面的執行緒:
在這裡插入圖片描述
2.出現死鎖:
在這裡插入圖片描述
3.先呼叫下面的執行緒,再呼叫上面的執行緒。
在這裡插入圖片描述

死鎖的解決辦法:

1.減少同步共享變數
2.採用專門的演算法,多個執行緒之間規定先後執行的順序,規避死鎖問題
3.減少鎖的巢狀。

執行緒的通訊

通訊常用方法:

通訊方法 描述
wait() 一旦執行此方法,當前執行緒就進入阻塞狀態,並釋放同步監視器
notify 一旦執行此方法,就會喚醒被wait的一個執行緒,如果有多個執行緒,就喚醒優先順序高的執行緒
notifyAll 一旦執行此方法,就會喚醒所有被wait()的執行緒

使用前提:這三個方法均只能使用在同步程式碼塊或者同步方法中。

package com.example.paoduantui.Thread;
/**
 * 執行緒通訊的例子:使用兩個執行緒列印1—100,執行緒1,執行緒2交替列印
 *
 * 當我們不採取執行緒之間的通訊時,無法達到執行緒1,2交替列印(cpu的控制權,是自動分配的)
 * 若想達到執行緒1,2交替列印,需要:
 * 1.當執行緒1獲取鎖以後,進入程式碼塊裡將number++(數字列印並增加)操作完以後,為了保證下個鎖為執行緒2所有,需要將執行緒1阻塞(執行緒1你等等wait())。(輸出1,number為2)
 * 2.當執行緒2獲取鎖以後,此時執行緒1已經不能進入同步程式碼塊中了,所以,為了讓執行緒1繼續搶佔下一把鎖,需要讓執行緒1的阻塞狀態取消(通知執行緒1不用等了notify()及notifyAll()),即應該在進入同步程式碼塊時取消執行緒1的阻塞。
 *
 * */
class Number implements Runnable{
    private int number = 1;//設定共享資料(執行緒之間對於共享資料的共享即為通訊)
    //對共享資料進行操作的程式碼塊,需要執行緒安全
    @Override
    public synchronized void run() {
        while(true){
            //使得執行緒交替等待以及通知交替解等待
            notify();//省略了this.notify()關鍵字
            if(number<100){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+number);
                number++;
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                break;
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args){
        //建立runnable物件
        Number number = new Number();
        //建立執行緒,並實現runnable介面
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        //給執行緒設定名字
        t1.setName("執行緒1");
        t2.setName("執行緒2");
        //開啟執行緒
        t1.start();
        t2.start();
    }
}

 

sleep和wait的異同:

相同點:一旦執行方法以後,都會使得當前的程式進入阻塞狀態
不同點:
1.兩個方法宣告的位置不同,Thread類中宣告sleep,Object類中宣告wait。
2.呼叫的要求不同,sleep可以在任何需要的場景下呼叫,wait必須使用在同步程式碼塊或者同步方法中
3.關於是否釋放同步監視器,如果兩個方法都使用在同步程式碼塊或同步方法中,sleep不會釋放,wait會釋放

經典例題:生產者/消費者問題:

生產者(Priductor)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品,店員一次只能持有固定數量的產品(比如20個),如果生產者檢視生產更多的產品,店員會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產:如果店中沒有產品了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。

這裡可能出現兩個問題:
生產者比消費者快的時候,消費者會漏掉一些資料沒有收到。
消費者比生產者快時,消費者會去相同的資料。

package com.example.paoduantui.Thread;
/**
 * 執行緒通訊的應用:生產者/消費者問題
 *
 * 1.是否是多執行緒問題?是的,有生產者執行緒和消費者執行緒(多執行緒的建立,四種方式)
 * 2.多執行緒問題是否存在共享資料? 存在共享資料----產品(同步方法,同步程式碼塊,lock鎖)
 * 3.多執行緒是否存線上程安全問題? 存在----都對共享資料產品進行了操作。(三種方法)
 * 4.是否存線上程間的通訊,是,如果生產多了到20時,需要通知停止生產(wait)。(執行緒之間的通訊問題,需要wait,notify等)
 *
 * */
	class Clerk{
	
	    private int productCount = 0;
	
	
	    //生產產品
	    public synchronized void produceProduct() {
	
	        if(productCount<20) {
	            productCount++;
	
	            System.out.println(Thread.currentThread().getName()+":開始生產第"+productCount+"個產品");
	            notify();
	        }else{
	            //當有20個時,等待wait
	            try {
	                wait();
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	    }
	
	    //消費產品
	    public synchronized void consumeProduct() {
	        if (productCount>0){
	            System.out.println(Thread.currentThread().getName()+":開始消費第"+productCount+"個產品");
	            productCount--;
	            notify();
	        }else{
	            //當0個時等待
	            try {
	                wait();
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	        }
	    }
	}
	
	class Producer extends Thread{//生產者執行緒
	
	    private Clerk clerk;
	
	    public Producer(Clerk clerk) {
	        this.clerk = clerk;
	    }
	
	    @Override
	    public void run() {
	
	        try {
	            sleep(10);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        System.out.println(Thread.currentThread().getName()+";開始生產產品......");
	
	        while(true){
	            clerk.produceProduct();
	        }
	    }
	}
	
	class Consumer implements Runnable{//消費者執行緒
	
	    private Clerk clerk;
	
	    public Consumer(Clerk clerk) {
	        this.clerk = clerk;
	    }
	
	    @Override
	    public void run() {
	
	        System.out.println(Thread.currentThread().getName()+":開始消費產品");
	
	        while(true){
	            try {
	                Thread.sleep(1);
	            } catch (InterruptedException e) {
	                e.printStackTrace();
	            }
	
	            clerk.consumeProduct();
	        }
	
	    }
	}
	
	public class ProductTest {
	
	    public static void main(String[] args){
	        Clerk clerk = new Clerk();
	
	        Producer p1 = new Producer(clerk);
	        p1.setName("生產者1");
	
	        Consumer c1 = new Consumer(clerk);
	        Thread t1 = new Thread(c1);
	        t1.setName("消費者1");
	
	        p1.start();
	        t1.start();
	
	    }
	
	}


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69980952/viewspace-2736996/,如需轉載,請註明出處,否則將追究法律責任。

相關文章