Java併發與多線程(2):線程、同步

一、線程

1、多線程原理

我們已經寫過一版多線程的代碼,那麼我們今天來體現一下多線程程序的執行原理
代碼如下:

/**
 * 自定義一個線程類extends Thread
 * @author Mango
 */
public class MainThread extends Thread{

    /**
     * 利用繼承的特點將 線程名字 設置
     * @param name
     */
    MainThread(String name) {
        super(name);
    }

    /**
     * 重寫的run方法,線程要執行的語句
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //getName()來自Thread
            System.out.println(getName() + ":正在執行!"+ i);
        }
    }

}
/**
 * 測試類
 * @author Mango
 */
public class Dome1 {

    public static void main(String[] args) {

        System.out.println("這裏是Main線程");
        
        MainThread mainThread = new MainThread("新線程");

        //開啓一個新線程
        mainThread.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("main線程" + i);
        }
    }
}

程序啓動運行main時候,JVM啓動一個進程,主線程main在main()調用時候被創建。隨着調用mainThread的對象的start方法,另外一個新的線程也啓動了,這樣,整個應用就在多線程下運行。

多線程的執行流程,那麼爲什麼可以完成併發執行呢?我們再來講一講原理。
多線程執行時,到底在內存中是如何運行的呢?以上個程序爲例,進行圖解說明:

在這裏插入圖片描述
多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間。進行方法的壓棧和彈棧。

當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那麼進程就結束了。

2、Thread類

API中該類中定義了有關線程的一些方法,具體如下:
(1)構造方法:

public Thread() :分配一個新的線程對象。
public Thread(String name) :分配一個指定名字的新的線程對象。
public Thread(Runnable target) :分配一個帶有指定目標新的線程對象。
public Thread(Runnable target,String name) :分配一個帶有指定目標新的線程對象並指定名字。

(2)常用方法:

public String getName() :獲取當前線程名稱。
public void start() :導致此線程開始執行; Java虛擬機調用此線程的run方法。
public void run() :此線程要執行的任務在此處定義代碼。
public static void sleep(long millis) :使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
public static Thread currentThread() :返回對當前正在執行的線程對象的引用。

創建線程的方式總共有兩種,一種是繼承Thread類方式,一種是實現Runnable接口方式
方式一已經明白,接下來講解方式二實現的方式。

3、Runnable創建線程

採用java.lang.Runnable (通過實現runnable接口,來作爲參數傳遞)也是非常常見的一種,我們只需要重寫run方法即可。

/**
 * 創建一個Runnable接口的實現類
 * @author Mango
 */
public class DomeRunnable implements Runnable {

    /**
     * 重寫接口的run方法,設置線程任務
     */
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i );
        }
    }
}

/**
 * 測試類
 * @author Mango
 */
public class Dome1 {

    public static void main(String[] args) {

        //創建一個Runnable接口實現類對象
        DomeRunnable runnable = new DomeRunnable();

        //採用傳遞runnable的構造方法創建
        Thread thread = new Thread(runnable);

        //開啓新的線程run方法
        thread.start();

        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i );
        }
    }
}

通過實現Runnable接口,使得該類有了多線程類的特徵。run()方法是多線程程序的一個執行目標。所有的多線程代碼都在run方法裏面。Thread類實際上也是實現了Runnable接口的類。

在啓動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然後調用Thread對象的start()方法來運行多線程代碼。

實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。

tips:Runnable對象僅僅作爲Thread對象的target,Runnable實現類裏包含的run()方法僅作爲線程執行體。而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其target的run()方法。

4、Thread和Runnable的區別

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。

實現Runnable接口比繼承Thread類所具有的優勢:

  1. 適合多個相同的程序代碼的線程去共享同一個資源。
  2. 可以避免java中的單繼承的侷限性。
  3. 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享(作參數被多個thread調用),代碼和線程獨立。
  4. 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。

擴充:在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。因爲每當使用
java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM其實在就是在操作系統中啓動了一個進
程。

二、線程安全

1、線程安全

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣
的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

我們通過一個案例,實現多個窗口同時賣電影票。演示線程的安全問題:
在這裏插入圖片描述
(1)需要票,Runnable接口子類來模擬

/**
 * 模擬票
 * @author Mango
 */
public class Ticket implements Runnable {
	//線程共享的數據
    private int ticket = 100;

    /**
     * 賣票操作
     */
    @Override
    public void run() {
        while(true) {
            if(ticket > 0) {
                try {
                    Thread.sleep(100);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣" + ticket--);
            }
        }
    }
}

(2)需要窗口,採用線程對象來模擬;

/**
 * 測試類
 * @author Mango
 */
public class Dome1 {

    public static void main(String[] args) {

        Ticket ticket = new Ticket();
		//同時開啓線程,執行run方法
        Thread t1 = new Thread(ticket,"窗口1");
        Thread t2 = new Thread(ticket,"窗口2");
        Thread t3 = new Thread(ticket,"窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

結果中有一部分這樣現象,發現程序出現了兩個問題:

  1. 相同的票數,比如5這張票被賣了兩回。
  2. 不存在的票,比如0票與-1票,是不存在的。

這種問題,幾個窗口(線程)票數不同步了,這種問題稱爲線程不安全。
在這裏插入圖片描述
線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
在這裏插入圖片描述

2、線程同步

當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題

要解決上述多線程併發訪問一個資源(ticket)的安全性問題:也就是解決重複票與不存在票問題,Java中提供了同步機制(synchronized) 來解決。

在這裏插入圖片描述

也就是說在某個線程修改共享資源的時候,其他線程不能修改該資源,等待修改完畢同步之後,才能去搶奪CPU
資源,完成對應的操作,保證了數據的同步性,解決了線程不安全的現象。爲了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。

有三種方式完成同步操作:

  1. 同步代碼塊。
  2. 同步方法。
  3. 鎖機制。

3、同步代碼塊

synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
格式:

//同步代碼塊
 synchronized (lock) {
     if(ticket > 0) {
     try {
         Thread.sleep(100);

     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     String name = Thread.currentThread().getName();
     System.out.println(name + "正在賣" + ticket--);
 }
 }

同步鎖:
對象的同步鎖只是一個概念,可以想象爲在對象上標記了一個鎖.

  1. 鎖對象 可以是任意類型。
    2. 多個線程對象 要使用同一把鎖

注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等着(BLOCKED)。

使用同步代碼塊解決代碼:

/**
 * 模擬票
 * @author Mango
 */
public class Ticket implements Runnable {

    /**
     * 多線程共享數據
     */
    private int ticket = 100;

    //創建一個鎖對象
    Object lock = new Object();


    /**
     * 賣票操作
     */
    @Override
    public void run() {
        while(true) {
            //同步代碼塊
            synchronized (lock) {
                if(ticket > 0) {
                try {
                    Thread.sleep(100);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣" + ticket--);
            }
            }
        }
    }
}

4、同步方法

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等着。
格式:

public synchronized void method(){
	可能會產生線程安全問題的代碼
}

同步鎖是誰?
對於非static方法,同步鎖就是this。
對於static方法,我們使用當前方法所在類的字節碼對象(類名.class)。

/**
 * 模擬票
 * @author Mango
 */
public class Ticket implements Runnable {

    /**
     * 多線程共享數據
     */
    private int ticket = 100;

    /**
     * 賣票操作
     */
    @Override
    public void run() {
        while(true) {
           sellTicket();
        }
    }

    /**
     * 誰調用這個方法就是鎖對象
     * 隱含鎖對象就是這個this
     */
    public synchronized void sellTicket() {
         //同步方法
        if(ticket > 0) {
            try {
                Thread.sleep(100);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在賣" + ticket--);
        }

    }
}

5、Lock鎖(常用)

java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。

Lock鎖也稱同步鎖,加鎖與釋放鎖 方法化了,如下:
public void lock() :加同步鎖。
public void unlock() :釋放同步鎖。

/**
 * 模擬票
 * @author Mango
 */
public class Ticket implements Runnable {

    /**
     * 多線程共享數據
     */
    private int ticket = 100;

    //創建一個lock鎖對象
    Lock lock = new ReentrantLock();


    /**
     * 賣票操作
     */
    @Override
    public void run() {
        while(true) {
            lock.lock();
            //同步方法
            if(ticket > 0) {
                try {
                    Thread.sleep(100);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣" + ticket--);
            }
            lock.unlock();
        }
    }

}

三、線程狀態

1、線程狀態概述

當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中這各個線程狀態發生的條件,下面將會對每種狀態進行詳細解析:
在這裏插入圖片描述
(1)Runnable(可運行)
線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操作系統處理器(cpu)

(2)Blocked(鎖阻塞)
當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。

(3)Waiting(無限等待)
一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒

我們只需知道在做線程操作中存在這樣的狀態。那我們怎麼去理解這幾個狀態呢,新建與被終止還是很容易理解的,我們就研究一下線程從Runnable(可運行)狀態與非運行狀態之間的轉換問題

2、Timed Waiting(計時等待)

Timed Waiting在API中的描述爲:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處於這一狀態。在我們寫賣票的案例中,爲了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就強制當前正在執行的線程休眠(暫停執行),以“減慢線程”

sleep就是等待。

/**
     * 賣票操作
     */
    @Override
    public void run() {
        while(true) {
            //開啓線程鎖
            lock.lock();
          
            if(ticket > 0) {
                try {
                    Thread.sleep(100);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣" + ticket--);
            }
            //關閉線程鎖
            lock.unlock();
        }
    }

我們需要記住下面幾點:

  1. 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協
    作關係。
  2. 爲了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程
    中會睡眠
  3. sleep與鎖無關,線程睡眠到期自動甦醒,並返回到Runnable(可運行)狀態
    在這裏插入圖片描述

3、BLOCKED(鎖阻塞)

Blocked狀態在API中的介紹爲:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處於這一狀態。
比如,線程A與線程B代碼中使用同一鎖,如果線程A獲取到鎖,線程A進入到Runnable狀態,那麼線程B就進入到Blocked鎖阻塞狀態。
在這裏插入圖片描述

4、Waiting(無限等待)

一個正在無限期等待另一個線程執行一個特別的(喚醒)動作的線程處於這一狀態。
在這裏插入圖片描述
當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運行)狀態中調用了wait()方法那麼A線程就進入了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了notify()方法,那麼就會將無限等待的A線程喚醒。注意是喚醒,如果A獲取到鎖對象,那麼A線程喚醒後就進入Runnable(可運行)狀態;如果沒有獲取鎖對象,那麼就進入到Blocked(鎖阻塞狀態)。

5、補充

線程狀態整體概括:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章