天天看點

多線程(二)多線程(二)線程安全線程狀态

多線程(二)

在多線程(一)中,我們說到了第一種方式建立多線程的方式。

補充一點: 說到線程的排程方式,搶占式排程,那麼優先級是怎麼區分的呢,其實我們可以設定線程的優先級,在java中,一般是0-10.

建立線程的方式二

采用 java.lang.Runnable 也是非常常見的一種,我們隻需要重寫run方法即可。

步驟如下:

  1. 定義Runnable接口的實作類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
  2. 建立Runnable實作類的執行個體,并以此執行個體作為Thread的target來建立Thread對象,該Thread對象才是真正 的線程對象。
  3. 調用線程對象的start()方法來啟動線程。

代碼如下:

public class MyRunnable implements Runnable{
 	@Override 
 	public void run() {
 	 	for (int i = 0; i < 20; i++) { 			
 	 		System.out.println(Thread.currentThread().getName()+" "+i); 
 	 	} 
 	 }
}
           
public class Demo { 
	public static void main(String[] args) { 
		//建立自定義類對象 線程任務對象 
		MyRunnable mr = new MyRunnable(); 
		//建立線程對象 
		Thread t = new Thread(mr, "小強"); 
		t.start(); 
		for (int i = 0; i < 20; i++) { 
			System.out.println("旺财 " + i); 
		} 
	} 
}
           

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

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

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

注意

:Runnable對象僅僅作為Thread對象的target,Runnable實作類裡包含的run()方法僅作為線程執行體。 而實際的線程對象依然是Thread執行個體,隻是該Thread線程負責執行其target的run()方法。

Thread和Runnable的差別

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

總結:
實作Runnable接口比繼承Thread類所具有的優勢:
  1. 适合多個相同的程式代碼的線程去共享同一個資源。
  2. 可以避免java中的單繼承的局限性。
  3. 增加程式的健壯性,實作解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
  4. 線程池隻能放入實作Runable或Callable類線程,不能直接放入繼承Thread的類。

    注意

    在java中,每次程式運作至少啟動2個線程。一個是main線程,一個是垃圾收集線程。因為每當使用 java指令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在作業系統中啟動了一個進 程。

匿名内部類方式實作線程的建立

使用線程的内匿名内部類方式,可以友善的實作每個線程執行不同的線程任務操作。

使用匿名内部類的方式實作Runnable接口,重新Runnable接口中的run方法:

public class NoNameInnerClassThread {
	public static void main(String[] args) {
		// new Runnable(){ 
			// public void run(){ 
				// for (int i = 0; i < 20; i++) { 
					// System.out.println("張宇:"+i); 
				// } 
			// } 
		// }; 
		//‐‐‐這個整體 相當于new MyRunnable()
		Runnable r = new Runnable(){ 
			public void run(){ 
				for (int i = 0; i < 20; i++) { 
					System.out.println("張宇:"+i); 
				}
			}
		};
		new Thread(r).start();
		for (int i = 0; i < 20; i++) { 
			System.out.println("費玉清:"+i); 
		}
	}
}
           

線程安全

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

我們通過一個案例,示範線程的安全問題:

電影院要賣票,我們模拟電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共100個 (本場電影隻能賣100張票)。

我們來模拟電影院的售票視窗,實作多個視窗同時賣 “葫蘆娃大戰奧特曼”這場電影票(多個視窗一起賣這100張票) 需要視窗,采用線程對象來模拟;需要票,Runnable接口子類來模拟 模拟票:

package com.example.demo;

public class Ticket implements Runnable{
    private int ticket = 100;
    public void run() {
        //每個視窗賣票的操作
        // 視窗 永遠開啟
        while (true) {
            if (ticket > 0) {
                //有票 可以賣
                // 出票操作
                // 使用sleep模拟一下出票時間
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e)  {
                    e.printStackTrace();
                }
                //擷取目前線程對象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣:" + ticket--);
            }
        }
    }
}

           
package com.example.demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


public class DemoApplicationTests {

    public static void main(String[] args) {
        //建立線程任務對象
        Ticket ticket = new Ticket();
        //建立三個視窗對象
        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票,是不存在的。

這種問題,幾個視窗(線程)票數不同步了,這種問題稱為線程不安全。

線程安全問題都是由全局變量及靜态變量引起的。若每個線程中對全局變量、靜态變量隻有讀操作,而無寫 操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步, 否則的話就可能影響線程安全。

線程同步

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

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

根據案例簡述:

視窗1線程進入操作的時候,視窗2和視窗3線程隻能在外等着,視窗1操作結束,視窗1和視窗2和視窗3
才有機會進入代碼 去執行。也就是說在某個線程修改共享資源的時候,其他線程不能修改該資源,等待
修改完畢同步之後,才能去搶奪CPU 資源,完成對應的操作,保證了資料的同步性,解決了線程不安全
的現象。
           

為了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。

那麼怎麼去使用呢?有三種方式完成同步操作:

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

同步代碼塊

  • 同步代碼塊:

    synchronized

    關鍵字可以用于方法中的某個區塊中,表示隻對這個區塊的資源實行互斥通路。 格式:
synchronized(同步鎖){ 
	需要同步操作的代碼
}
           

同步鎖:

對象的同步鎖隻是一個概念,可以想象為在對象上标記了一個鎖.

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

注意:

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

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

package com.example.demo;

public class Ticket implements Runnable{
    private int ticket = 100;
    Object lock = new Object();
    public void run() {
        //每個視窗賣票的操作
        // 視窗 永遠開啟
        while (true) {
            synchronized (lock){
                if (ticket > 0) {
                    //有票 可以賣
                    // 出票操作
                    // 使用sleep模拟一下出票時間
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e)  {
                        e.printStackTrace();
                    }
                    //擷取目前線程對象的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在賣:" + ticket--);
                }
            }

        }
    }
}

           

當使用了同步代碼塊後,上述的線程的安全問題,解決了。

同步方法

  • **同步方法:**使用

    synchronized

    修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程隻能在方法外 等着。

    格式:

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

同步鎖是誰?

對于非static方法,同步鎖就是this。

對于static方法,我們使用目前方法所在類的位元組碼對象(類名.class)。

使用同步方法代碼如下:

package com.example.demo;

public class Ticket implements Runnable{
    private int ticket = 100;
    Object lock = new Object();
    public void run() {
        //每個視窗賣票的操作
        // 視窗 永遠開啟
        while (true) {
            sellTicket();
        }
    }

    private synchronized void sellTicket() {
        if (ticket > 0) {
            //有票 可以賣
            // 出票操作
            // 使用sleep模拟一下出票時間
            try {
                Thread.sleep(100);
            } catch (InterruptedException e)  {
                e.printStackTrace();
            }
            //擷取目前線程對象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在賣:" + ticket--);
        }

    }
}

           

Lock鎖

java.util.concurrent.locks.Lock

機制提供了比

synchronized

代碼塊和

synchronized

方法更廣泛的鎖定操作, 同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更展現面向對象。

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:

  • public void lock()

    :加同步鎖。
  • public void unlock()

    :釋放同步鎖
package com.example.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket implements Runnable{
    private int ticket = 100;
    Lock lock = new ReentrantLock();
    public void run() {
        //每個視窗賣票的操作
        // 視窗 永遠開啟
        while (true) {
            lock.lock();
            sellTicket();
            lock.unlock();
        }
    }

    private void sellTicket() {
        if (ticket > 0) {
            //有票 可以賣
            // 出票操作
            // 使用sleep模拟一下出票時間
            try {
                Thread.sleep(100);
            } catch (InterruptedException e)  {
                e.printStackTrace();
            }
            //擷取目前線程對象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在賣:" + ticket--);
        }

    }
}

           

線程狀态

線程狀态概述

當線程被建立并啟動以後,它既不是一啟動就進入了執行狀态,也不是一直處于執行狀态。線上程的生命周期中, 有幾種狀态呢?在API中

java.lang.Thread.State

這個枚舉中給出了六種線程狀态:

/**
     * A thread state.  A thread can be in one of the following states:
     * <ul>
     * <li>{@link #NEW}<br>
     *     A thread that has not yet started is in this state.
     *     </li>
     * <li>{@link #RUNNABLE}<br>
     *     A thread executing in the Java virtual machine is in this state.
     *     </li>
     * <li>{@link #BLOCKED}<br>
     *     A thread that is blocked waiting for a monitor lock
     *     is in this state.
     *     </li>
     * <li>{@link #WAITING}<br>
     *     A thread that is waiting indefinitely for another thread to
     *     perform a particular action is in this state.
     *     </li>
     * <li>{@link #TIMED_WAITING}<br>
     *     A thread that is waiting for another thread to perform an action
     *     for up to a specified waiting time is in this state.
     *     </li>
     * <li>{@link #TERMINATED}<br>
     *     A thread that has exited is in this state.
     *     </li>
     * </ul>
     *
     * <p>
     * A thread can be in only one state at a given point in time.
     * These states are virtual machine states which do not reflect
     * any operating system thread states.
     *
     * @since   1.5
     * @see #getState
     */
    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }
           
線程狀态 導緻狀态發生條件
NEW(建立) 線程剛被建立,但是并未啟動。還沒調用start方法。
Runnable(可運作) 線程可以在java虛拟機中運作的狀态,可能正在運作自己代碼,也可能沒有,這取決于操 作系統處理器。
Blocked(鎖阻塞) 當一個線程試圖擷取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀 态;當該線程持有鎖時,該線程将變成Runnable狀态。
Waiting(無限等待) 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀态。進入這個 狀态後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。
Timed Waiting(計時等待) 同waiting狀态,有幾個方法有逾時參數,調用他們将進入Timed Waiting狀态。這一狀态 将一直保持到逾時期滿或者接收到喚醒通知。帶有逾時參數的常用方法有Thread.sleep 、 Object.wait。
Teminated(被終止) 因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。

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

Timed Waiting(計時等待)

Timed Waiting在API中的描述為:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處于這一狀态。單獨 的去了解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀态了,在哪裡呢?

在我們寫賣票的案例中,為了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就 強制目前正在執行的線程休眠(暫停執行),以“減慢線程”。

其實當我們調用了sleep方法之後,目前執行的線程就進入到“休眠狀态”,其實就是所謂的Timed Waiting(計時等 待),那麼我們通過一個案例加深對該狀态的一個了解。

實作一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字輸出一個字元串

package com.example.demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


public class DemoApplicationTests extends Thread{


    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new DemoApplicationTests().start();
    }

}

           

通過案例可以發現,sleep方法的使用還是很簡單的。我們需要記住下面幾點:

  1. 進入 TIMED_WAITING 狀态的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協 作關系。
  2. 為了讓其他線程有機會執行,可以将Thread.sleep()的調用**放線程run()**之内。這樣才能保證該線程執行過程中會睡眠。
  3. sleep與鎖無關,線程睡眠到期自動蘇醒,并傳回到Runnable(可運作)狀态。

    注意

    sleep()中指定的時間是線程不會運作的最短時間。是以,sleep()方法不能保證該線程睡眠到期後就 開始立刻執行。

    Timed Waiting 線程狀态圖:

多線程(二)多線程(二)線程安全線程狀态

BLOCKED(鎖阻塞)

Blocked狀态在API中的介紹為:一個正在阻塞等待一個螢幕鎖(鎖對象)的線程處于這一狀态。

我們已經學完同步機制,那麼這個狀态是非常好了解的了。比如,線程A與線程B代碼中使用同一鎖,如果線程A獲 取到鎖,線程A進入到Runnable狀态,那麼線程B就進入到Blocked鎖阻塞狀态。

這是由Runnable狀态進入Blocked狀态。除此Waiting以及Time Waiting狀态也會在某種情況下進入阻塞狀态,而 這部分内容作為擴充知識點帶領大家了解一下

Blocked 線程狀态圖

多線程(二)多線程(二)線程安全線程狀态

Waiting(無限等待)

Wating狀态在API中介紹為:一個正在無限期等待另一個線程執行一個特别的(喚醒)動作的線程處于這一狀态。

那麼我們之前遇到過這種狀态嗎?答案是并沒有,但并不妨礙我們進行一個簡單深入的了解。我們通過一段代碼來 學習一下

package com.example.demo;

public class WaitingTest {
    public static Object obj = new Object();

    public static void main(String[] args) {
        // 示範waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        try {
                            System.out.println( Thread.currentThread().getName() +"=== 擷取到鎖對象,調用wait方法,進入waiting狀态,釋放鎖對象");
                            obj.wait(); //無限等待
                            //obj.wait(5000); //計時等待, 5秒 時間到,自動醒來
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread().getName() + "=== 從waiting狀 态醒來,擷取到鎖對象,繼續執行了");
                    }
                }
            }

        },"等待線程").start();


        new Thread(new Runnable() {

            @Override
            public void run() {
                while (true) { //每隔3秒 喚醒一次
                    try {
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒鐘");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (obj){
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 擷取到鎖對 象,調用notify方法,釋放鎖對象");
                        obj.notify();
                    }
                }
            }
        },"喚醒線程").start();


    }



}

           

通過上述案例我們會發現,一個調用了某個對象的 Object.wait 方法的線程會等待另一個線程調用此對象的 Object.notify()方法 或 Object.notifyAll()方法。

其實waiting狀态并不是一個線程的操作,它展現的是多個線程間的通信,可以了解為多個線程之間的協作關系, 多個線程會争取鎖,同時互相之間又存在協作關系。就好比在公司裡你和你的同僚們,你們可能存在晉升時的競 争,但更多時候你們更多是一起合作以完成某些任務。

當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運作)狀态中調用了wait()方法那麼A線程就進入 了Waiting(無限等待)狀态,同時失去了同步鎖。假如這個時候B線程擷取到了同步鎖,在運作狀态中調用了 notify()方法,那麼就會将無限等待的A線程喚醒。注意是喚醒,如果擷取到鎖對象,那麼A線程喚醒後就進入 Runnable(可運作)狀态;如果沒有擷取鎖對象,那麼就進入到Blocked(鎖阻塞狀态)。

多線程(二)多線程(二)線程安全線程狀态
多線程(二)多線程(二)線程安全線程狀态

注意

我們在翻閱API的時候會發現Timed Waiting(計時等待) 與 Waiting(無限等待) 狀态聯系還是很緊密的, 比如Waiting(無限等待) 狀态中wait方法是空參的,而timed waiting(計時等待) 中wait方法是帶參的。 這種帶參的方法,其實是一種倒計時操作,相當于我們生活中的小鬧鐘,我們設定好時間,到時通知,可是 如果提前得到(喚醒)通知,那麼設定好時間在通知也就顯得多此一舉了,那麼這種設計方案其實是一舉兩 得。如果沒有得到(喚醒)通知,那麼線程就處于Timed Waiting狀态,直到倒計時完畢自動醒來;如果在倒 計時期間得到(喚醒)通知,那麼線程從Timed Waiting狀态立刻喚醒。