b java 的線程模型介紹

—> go to 總目錄

基本概念

什麼是線程

可以參考《現代操作系統》
經典的unix具有強進程觀念,後續的linux弱化了線程與進程的隔閡。進程是內核的概念,而線程的實現可以是內核+應用態,也可以是純應用態的邏輯。
不管怎麼說,現代操作系統調度的最小單元是線程,每個線程擁有各自的計數器,堆棧,和局部變量屬性,能夠訪問共享變量。而處理器高速的切換,造成並行運行的假象

爲什麼要使用多線程

  • 更多的處理器核心
    現代處理器向多核發展,程序的邏輯被分配到不同核心,擁有更快的處理速度。
  • 更快的響應時間
    程序複雜的邏輯串行運行十分緩慢,有效的並行能擁有更快的響應時間
  • 更好的編程模型
    併發編程有很多成熟、高效的模型。並不需要程序員重新發明輪子,稍作修改就能使用。

線程

現代OS基本採用時分的形式調度運行的線程,CPU分成時間片。同時線程(進程)被設定有優先級,高優的線程能分配到更多的CPU資源。

java的優先級

在Java線程中,通過一個整型成員變量priority來控制優先級,優先級的範圍從1~10,在線程構建的時候可以通過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分配時間片的數量要多於優先級低的線程。設置線程優先級時,針對頻繁阻塞(休眠或者I/O操作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較低的優先級,確保處理器不會被獨佔。在不同的JVM以及操作系統上,線程規劃會存在差異

         Thread thread =new Thread(new Runnable() {
            @Override
            public void run() {
                
            }
        });
        // 設置優先級
        thread.setPriority(6);

線程的狀態

線程擁有6種狀態NEWRUNNABLEBLOCKEDwAITINGTIME_WAITINGTERMINATED
在這裏插入圖片描述
使用jstack查看線程信息
狀態機
在這裏插入圖片描述

Daemon 守護線程

當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。可以通過調用Thread.setDaemon(true)將線程設置爲Daemon線程。Daemon屬性需要在啓動線程之前設置,不能在啓動線程之後設置。Daemon線程被用作完成支持性工作,但是在Java虛擬機退出時Daemon線程中的finally塊並不一定會執行,示例如代碼清單4-5所示。

java 線程的運行

//繼承 Thread
class newThread extends Thread{}
//實現 Runnable接口
new Thread(new Runnable({
  @Override
  public void run(){
    //paas
  }
}));
//設置線程的屬性(節選自java.lang.Thread 中線程初始化)
private void init(ThreadGroup g, Runnable target, String name,long stackSize,
AccessControlContext acc) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    // 當前線程就是該線程的父線程
    Thread parent = currentThread();
    this.group = g;
    // 將daemon、priority屬性設置爲父線程的對應屬性
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    this.name = name.toCharArray();
    this.target = target;
    setPriority(priority);
    // 將父線程的InheritableThreadLocal複製過來
    if (parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.
    inheritableThreadLocals);
    // 分配一個線程ID
    tid = nextThreadID();
}

不難看出,一個新線程的構造是由其parent線程來進行空間分配,而child線程繼承了parent是否爲Daemon、優先級和加載資源的contextClassLoader以及可繼承ThreadLocal,同時還會分配一個唯一的ID來標識這個child線程。至此,一個能夠運行的線程對象就初始化好了,在堆內存中等待着運行。

線程的中斷

中斷可以理解爲線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進行了中斷操作。中斷好比其他線程對該線程打了個招呼,其他線程通過調用該線程的interrupt()
方法對其進行中斷操作。線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以調用靜態方法Thread.interrupted()對當前線程的中斷標識位進行復位。如果該線程已經處於終結狀態,即使該線程被中斷過,在調用該線程對象的isInterrupted()時依舊會返回false。從Java的API中可以看到,許多聲明拋出InterruptedException的方法(例如Thread.sleep(longmillis)方法)這些方法在拋出InterruptedException之前,Java虛擬機會先將該線程的中斷標識位清除,然後拋出InterruptedException,此時調用isInterrupted()方法將會返回false。

優雅的停止線程

在4.2.3節中提到的中斷狀態是線程的一個標識位,而中斷操作是一種簡便的線程間交互方式,而這種交互方式最適合用來取消或停止任務。除了中斷以外,還可以利用一個boolean變量來控制是否需要停止任務並終止該線程。在代碼清單4-9所示的例子中,創建了一個線程CountThread,它不斷地進行變量累加,而主線程嘗試對其進行中斷操作和停止操作

import java.util.concurrent.TimeUnit;

public class Shutdown {
    public static void main(String[] args) throws Exception {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        // 睡眠1秒,main線程對CountThread進行中斷,使CountThread能夠感知中斷而結束
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        // 睡眠1秒,main線程對Runner two進行取消,使CountThread能夠感知on爲false而結束
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void cancel() {
            on = false;
        }
    }
}

示例在執行過程中,main線程通過中斷操作和cancel()方法均可使CountThread得以終止。這種通過標識位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是武斷地將線程停止,因此這種終止線程的做法顯得更加安全和優雅。

線程間的通信

線程開始運行,擁有自己的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步地執行,直到終止。但是,每個運行中的線程,如果僅僅是孤立地運行,那麼沒有一點兒價值,或者
說價值很少,如果多個線程能夠相互配合完成工作,這將會帶來巨大的價值。

volatile和synchronized關鍵字

使用這個兩個關鍵字可以使線程共享資源同步,略。算是一種通信

等待/通知機制

等待/通知的相關方法是任意Java對象都具備的,因爲這些方法被定義在所有對象的超類java.lang.Object上,方法和描述如表4-2所示。
在這裏插入圖片描述

等待/通知機制,是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B調用了對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。

用法實例

import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;
public class WaitNotify {
    static boolean flag = true;
    static Object lock = new Object();
    public static void main(String[] args) throws Exception {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();
        TimeUnit.SECONDS.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();
    }
    static class Wait implements Runnable {
        public void run() {
            // 加鎖,擁有lock的Monitor
            synchronized (lock) {
                // 當條件不滿足時,繼續wait,同時釋放了lock的鎖
                while (flag) {
                    try {

                        lock.wait();
                    } catch (InterruptedException e) {
                    }
                }
                // 條件滿足時,完成工作

            }
        }
    }
}
import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;

public class WaitNotify {
    static boolean flag = true;
    static Object lock = new Object();
    public static void main(String[] args) throws Exception {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();
        TimeUnit.SECONDS.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();
    }
    static class Wait implements Runnable {
        public void run() {
            // 加鎖,擁有lock的Monitor
            synchronized (lock) {
                // 當條件不滿足時,繼續wait,同時釋放了lock的鎖
                while (flag) {
                    try {
                        System.out.println(Thread.currentThread() +
                        "flag is true. wait@" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        lock.wait();
                    } catch (InterruptedException e) {
                    }
                }
                // 條件滿足時,完成工作
                System.out.println(Thread.currentThread() + 
                "flag is false. running@" + 
                new SimpleDateFormat("HH:mm:ss").format(new Date()));
            }
        }
    }
    static class Notify implements Runnable {
        public void run() {
            // 加鎖,擁有lock的Monitor
            synchronized (lock) {
                // 獲取lock的鎖,然後進行通知,通知時不會釋放lock的鎖,
                // 直到當前線程釋放了lock後,WaitThread才能從wait方法中返回
                System.out.println(Thread.currentThread() + 
                " hold lock. notify @ " +
                new SimpleDateFormat("HH:mm:ss").format(new Date()));
                lock.notifyAll();
                flag = false;
                SleepUtils.second(5);
            }
            // 再次加鎖
            synchronized (lock) {
                System.out.println(Thread.currentThread() + 
                " hold lock again. sleep@ " + 
                new SimpleDateFormat("HH:mm:ss").format(new Date()));
                SleepUtils.second(5);
            }
        }
    }
}

輸出如下(輸出內容可能不同,主要區別在時間上)。

Thread[WaitThread,5,main] flag is true. wait @ 22:23:03
Thread[NotifyThread,5,main] hold lock. notify @ 22:23:04
Thread[NotifyThread,5,main] hold lock again. sleep @ 22:23:09
Thread[WaitThread,5,main] flag is false. running @ 22:23:14

備註

實例演示的是,waitThred線程和notifyThread線程競爭lock對象,wait首先獲取鎖後主動調用waitThred釋放鎖。notifyThread獲取鎖後,主動調用lock.notifyAll()試圖喚醒等待在lock上的
waitThred線程。但是必須得是notifyThread執行結束後(等待5s動作),wait纔會繼續

等待/通知的經典範式

可以提煉出等待/通知的經典範式,該範式分爲兩部分,分別針對等待方(消費者)和通知方(生產者)。
等待方遵循如下原則。
1)獲取對象的鎖。
2)如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
3)條件滿足則執行對應的邏輯。

synchronized(對象) {
    while(條件不滿足) {
    對象.wait();
    }
    對應的處理邏輯
}

通知方遵循如下原則。
1)獲得對象的鎖。
2)改變條件。
3)通知所有等待在對象上的線程。

synchronized(對象) {
    改變條件
    對象.notifyAll();
}

管道輸入/輸出流

管道輸入/輸出流和普通的文件輸入/輸出流或者網絡輸入/輸出流不同之處在於,它主要用於線程之間的數據傳輸,而傳輸的媒介爲內存。管道輸入/輸出流主要包括瞭如下4種具體實現:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前兩種面向字節,而後兩種面向字符。在代碼清單4-12所示的例子中,創建了printThread,它用來接受main線程的輸入,任何main線程的輸入均通過PipedWriter寫入,而printThread在另一端通過PipedReader將內容讀出並打印。

import java.io.IOException;
import java.io.PipedReader;

public class Piped {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 將輸出流和輸入流進行連接,否則在使用時會拋出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }
    static class Print implements Runnable {
        private PipedReader in;
        public Print(PipedReader in) {
            this.in = in;
        }
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }
}

對於Piped類型的流,必須先要進行綁定,也就是調用connect()方法,如果沒有將輸入/輸
出流綁定起來,對於該流的訪問將會拋出異常。

Thread.jion()使用

如果一個線程A執行了thread.join()語句,其含義是:當前線程A等待thread線程終止之後才從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(longmillis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示,如果線程thread在給定的超時時間裏沒有終止,那麼將會從該超時方法中返回。在代碼清單4-13所示的例子中,創建了10個線程,編號0~9,每個線程調用前一個線程的join()方法,也就是線程0結束了,線程1才能從join()方法中返回,而線程0需要等待main線程結束。

import java.util.concurrent.TimeUnit;

public class Join {
    public static void main(String[] args) throws Exception {
        Thread previous = Thread.currentThread();
        for (int i = 0; i < 10; i++) {
            // 每個線程擁有前一個線程的引用,需要等待前一個線程終止,才能從等待中返回
            Thread thread = new Thread(new Domino(previous), String.valueOf(i));
            thread.start();
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }
    static class Domino implements Runnable {
        private Thread thread;
        public Domino(Thread thread) {
            this.thread = thread;
        }
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}
main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.

從上述輸出可以看到,每個線程終止的前提是前驅線程的終止,每個線程等待前驅線程終止後,才從join()方法返回,這裏涉及了等待/通知機制(等待前驅線程結束,接收前驅線程結束通知)。代碼清單4-14是JDK中Thread.join()方法的源碼(進行了部分調整)。

// 加鎖當前線程對象
public final synchronized void join() throws InterruptedException {
// 條件不滿足,繼續等待
while (isAlive()) {
wait(0);
}
// 條件符合,方法返回
}

當線程終止時,會調用線程自身的notifyAll()方法,會通知所有等待在該線程對象上的線程。可以看到join()方法的邏輯結構與4.3.3節中描述的等待/通知經典範式一致,即加鎖、循環和處理邏輯3個步驟。

ThreadLocal的使用**

普通變量

ThreadLocal,即線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。可以通過set(T)方法來設置一個值,在當前線程下再通過get()方法獲取到原先設置的值。在代碼清單4-15所示的例子中,構建了一個常用的Profiler類,它具有begin()和end()兩個方法,而end()方法返回從begin()方法調用開始到end()方法被調用時的時間差,單位是毫秒。

import java.util.concurrent.TimeUnit;

public class Profiler {
    // 第一次get()方法調用時會進行初始化(如果set方法沒有調用),每個線程會調用一次
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };
    public static final void begin() {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }
    public static final long end() {
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }
    public static void main(String[] args) throws Exception {
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Cost: " + Profiler.end() + " mills");
    }
}

Profiler可以被複用在方法調用耗時統計的功能上,在方法的入口前執行begin()方法,在方法調用後執行end()方法,好處是兩個方法的調用不用在一個方法或者類中,比如在AOP(面向方面編程)中,可以在方法調用前的切入點執行begin()方法,而在方法調用後的切入點執行end()方法,這樣依舊可以獲得方法的執行耗時。

對象變量

不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否是null,如果是null,則說明當前線程還沒有對應的Connection對象,這時創建一個Connection對象並添加到本地線程變量中;如果不爲null,則說明當前的線程已經擁有了Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其它線程的Connection。因此,這個TopicDao就可以做

package com.test;  
  
import java.sql.Connection;  
import java.sql.SQLException;  
import java.sql.Statement;  
  
public class TestDaoNew {  
    // ①使用ThreadLocal保存Connection變量  
    private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();  
  
    public static Connection getConnection() {  
        // ②如果connThreadLocal沒有本線程對應的Connection創建一個新的Connection,  
        // 並將其保存到線程本地變量中。  
        if (connThreadLocal.get() == null) {  
            Connection conn = getConnection();  
            connThreadLocal.set(conn);  
            return conn;  
        } else {  
            return connThreadLocal.get();// ③直接返回線程本地變量  
        }  
    }  
  
    public void addTopic() throws SQLException {  
        // ④從ThreadLocal中獲取線程對應的Connection  
        Statement stat = getConnection().createStatement();  
    }  

到singleton共享了。
當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共享Connection時不發生線程安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。

package com.test;  
  
import java.sql.Connection;  
import java.sql.DriverManager;  
import java.sql.SQLException;  
  
public class ConnectionManager {  
  
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
        @Override  
        protected Connection initialValue() {  
            Connection conn = null;  
            try {  
                conn = DriverManager.getConnection(  
                        "jdbc:mysql://localhost:3306/test", "username",  
                        "password");  
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
            return conn;  
        }  
    };  
  
    public static Connection getConnection() {  
        return connectionHolder.get();  
    }  
  
    public static void setConnection(Connection conn) {  
        connectionHolder.set(conn);  
    }  
}  

經典應用:超時等待

開發人員經常會遇到這樣的方法調用場景:調用一個方法時等待一段時間(一般來說是給
定一個時間段),如果該方法能夠在給定的時間段之內得到結果,那麼將結果立刻返回,反之,
超時返回默認結果。

// 對當前對象加鎖
public synchronized Object get(long mills) throws InterruptedException {
    long future = System.currentTimeMillis() + mills;
    long remaining = mills;
// 當超時大於0並且result返回值不滿足要求
    while ((result == null) && remaining > 0) {
    wait(remaining);
    remaining = future - System.currentTimeMillis();
}
return result;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章