1. 程式人生 > >Java之多執行緒(一)

Java之多執行緒(一)

一,前言

​ 今天總結一些關於執行緒方面的知識,說到執行緒可謂是無人不知,畢竟這東西不管是在工作開發中,還是實際生活中都時時存在著。關於執行緒方面的內容非常多,從簡單的單執行緒,多執行緒,執行緒安全以及到高併發等等,當然也包括資訊通訊。

​ 當然這次從執行緒的基本開始,後面也會慢慢的補充執行緒的高階使用,這也算是讓自己再複習一次了(哈哈)。

​ 以下內容包括:

  • 二,執行緒介紹
  • 三,執行緒的建立
  • 四,執行緒安全
  • 五,執行緒池

二,執行緒介紹

​ 先來介紹幾個關於執行緒方面的概念。

2.1,並行與併發

  • 併發:指兩個或多個事件在同一個時間段內發生。
  • 並行:指兩個或多個事件在同一時刻發生(同時發生)。

​ 在作業系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時執行,這在單 CPU 系統中,每一時刻只能有一道程式執行,即微觀上這些程式是分時的交替執行,只不過是給人的感覺是同時執行,那是因為分時交替執行的時間是非常短的,CPU在多個程式之間高速切換。

​ 而在多個 CPU 系統中,則這些可以併發執行的程式便可以分配到多個處理器上(CPU),實現多工並行執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核越多,並行處理的程式越多,能大大的提高電腦執行的效率。

注意:

單核處理器的計算機肯定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發執行。同理,執行緒也是一樣的,從巨集觀角度上理解執行緒是並行執行的,但是從微觀角度上分析卻是序列執行的,即一個執行緒一個執行緒的去執行,當系統只有一個CPU時,執行緒會以某種順序執行多個執行緒,我們把這種情況稱之為執行緒排程。

2.2 ,執行緒與程序

  • 程序:是指一個記憶體中執行的應用程式,每個程序都有一個獨立的記憶體空間,一個應用程式可以同時執行多個程序;程序也是程式的一次執行過程,是系統執行程式的基本單位;系統執行一個程式即是一個程序從建立、執行到消亡的過程。

  • 執行緒:執行緒是程序中的一個執行單元,負責當前程序中程式的執行,一個程序中至少有一個執行緒。一個程序中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。

    一個程式執行後至少有一個程序,一個程序中可以包含多個執行緒 。

​ 我們可以再電腦底部工作列,右鍵----->開啟工作管理員,可以檢視當前任務的程序:

執行緒排程:

  • 分時排程

    所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。

  • 搶佔式排程

    優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個(執行緒隨機性),Java使用的為搶佔式排程。

三,執行緒的建立

​ 建立執行緒有兩種方式:

  • 繼承java.lang.Thread類,重寫run方法實現執行緒建立。
  • 實現java.lang.Runnable介面,例項化其實現類物件建立執行緒。

3.1,Thread

​ 先來看看API文件的說明:

Thread是一個類,但同時也實現了Runnable介面。

接著使用Thread建立執行緒。

public class ThreadMain {
    public static void main(String[] args) {
        // 1,例項化ThreadMode物件
        ThreadMode thread = new ThreadMode();
        // 2,呼叫start()方法啟動執行緒
        thread.start();
    }
}
/**
 * 繼承Thread類
 */
class ThreadMode extends Thread {
    // 1,重寫父類的run方法。
    @Override
    public void run() {
        System.out.println("使用Thread建立執行緒!");
    }
}

3.2,構造方法

public Thread():分配新的 Thread 物件。

public Thread(Runnable target):分配一個帶有指定目標的新執行緒。

public Thread(String name):分配一個帶有名字的新執行緒。

public Thread(Runnable target, String name):分配一個帶有指定目標的新執行緒,並指定執行緒的名字。

3.3,Runnable

java.lang.Runnable:Runnable 介面該由那些打算通過某一執行緒其例項的類來實現。類必須定義一個稱為run的無參方法。

實現步驟:
​ 1,建立一個Runnable介面的實現類。
​ 2,在實現類中重寫Runnable介面中的run方法,設定執行緒的任務。
​ 3,建立一個Runnable實現類的物件。
​ 4,建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件。
​ 5,呼叫Thread類中start方法,開啟執行緒執行run方法。

public class ThreadInterface implements Runnable{
    @Override
    public void run() {
        System.out.println("使用Runnable建立執行緒!");
    }
}
 // 在main方法中,建立實現類物件並開啟執行緒
ThreadInterface thread = new ThreadInterface();
 new Thread(thread,"runnable").start();

​ 說到這裡Thread類和Runnable介面都可以建立新的執行緒,那麼它們之間又有什麼區別呢?

​ 使用Runnable介面的好處:

  • 避免了單繼承的侷限性一個類只能繼承一個父類,如果建立執行緒選擇繼承Thread類,那麼就不能再繼承別的父類。而採用實現Runnable介面,則還可以再繼承,且再實現別的介面。
  • 增強了程式的擴充套件性,降低了程式的耦合性(解耦)實現Runnable介面的方式,把設定執行緒任務和開啟執行緒的任務進行了分離。

3.4,匿名內部類方式建立執行緒

​ 作用:
​ 1,簡化程式碼
​ 2,把子類繼承父類,重寫父類的方法,建立子類物件一步合成。
​ 3,把實現類介面,重寫介面中的方法,建立實現類物件一步合成。
​ 格式:
​ new 父類/介面(){}

​ Thread

new Thread(){
            @Override
            public void run() {
                super.run();
            }
        }.start();

​ 使用lambda表示式寫法(JDK8特性,後面會分享該特性):

new Thread(() -> System.out.println("Thread匿名內部類")).start();

​ Runnable

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Runnable匿名內部類");
    }
};
// 開啟執行緒
 new Thread(runnable).start();

​ 使用lambda表示式:

Runnable runnable = () -> System.out.println("Runnable匿名內部類");
// 開啟執行緒的第一種方式
new Thread(runnable).start();

四,執行緒安全

​ 執行緒安全通常有3種解決方式:

​ 1,同步程式碼塊

​ 2,同步方法

​ 3,鎖機制(lock)

​ 我們以賣車票為案例,用三種方式去解決車票的重複售賣,超賣情況。

4.1,同步程式碼塊

​ 格式:
​ synchronized(鎖物件){
​ 可能會出現執行緒安全問題的程式碼(訪問了共享資料的程式碼)
​ }

​ 注意:
1.通過程式碼塊中的鎖物件,可以使用任意的物件。
2.但是必須保證多個執行緒使用的鎖物件是同一個。
3.鎖物件作用: 把同步程式碼塊鎖住,只讓一個執行緒在同步程式碼塊中執行。

public class RunnableImpl implements Runnable{
    //定義一個多個執行緒共享的票源
    private  int ticket = 100;
    //建立一個鎖物件
    Object obj = new Object();
    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死迴圈,讓賣票操作重複執行
        while(true){
           //同步程式碼塊
            synchronized (obj){
                //先判斷票是否存在
                if(ticket>0){
                    //提高安全問題出現的概率,讓程式睡眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //票存在,賣票 ticket--
              System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                    ticket--;
                }
            }
        }
    }
}

​ 在main方法中呼叫執行緒,並模擬三個視窗同時售票。

public class Demo01Ticket {
    public static void main(String[] args) {
        //建立Runnable介面的實現類物件
        RunnableImpl run = new RunnableImpl();
        //建立Thread類物件,構造方法中傳遞Runnable介面的實現類物件
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //呼叫start方法開啟多執行緒
        t0.start();
        t1.start();
        t2.start();
    }
}

4.2,同步方法

​ 使用步驟:
​ 1.把訪問了共享資料的程式碼抽取出來,放到一個方法中
​ 2.在方法上新增synchronized修飾符

​ 格式:定義方法的格式
​ 修飾符 synchronized 返回值型別 方法名(引數列表){
可能會出現執行緒安全問題的程式碼(訪問了共享資料的程式碼)

​ }

public class RunnableImpl implements Runnable{
    //定義一個多個執行緒共享的票源
    private static int ticket = 100;
    //設定執行緒任務:賣票
    @Override
    public void run() {
        System.out.println("this:"+this);
        //使用死迴圈,讓賣票操作重複執行
        while(true){
            payTicketStatic();
        }
    }
    /*
        靜態的同步方法
        鎖物件應該是誰?
        不能是this
        this是建立物件之後產生的,靜態方法優先於物件
        靜態方法的鎖物件是本類的class屬性-->class檔案物件(反射)
     */
    public static /*synchronized*/ void payTicketStatic(){
        synchronized (RunnableImpl.class){
            //先判斷票是否存在
            if(ticket>0){
                //提高安全問題出現的概率,讓程式睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,賣票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                ticket--;
            }
        }
    }
    /*
        定義一個同步方法
        同步方法也會把方法內部的程式碼鎖住
        只讓一個執行緒執行
        同步方法的鎖物件是誰?
        就是實現類物件 new RunnableImpl()
        也是就是this
     */
    public /*synchronized*/ void payTicket(){
        synchronized (this){
            //先判斷票是否存在
            if(ticket>0){
                //提高安全問題出現的概率,讓程式睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,賣票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                ticket--;
            }
        }
    }
}

​ 在main函式中呼叫該執行緒,其程式碼與上述一樣。

4.3,同步鎖(Lock)

java.util.concurrent.locks.Lock介面
Lock 實現提供了比使用 synchronized方法和語句可獲得的更廣泛的鎖定操作。
Lock介面中的方法:
​ void lock()獲取鎖。
​ void unlock() 釋放鎖。

​ 使用步驟:
​ 1.在成員位置建立一個ReentrantLock物件。
​ 2.在可能會出現安全問題的程式碼前呼叫Lock介面中的方法lock獲取鎖。
​ 3.在可能會出現安全問題的程式碼後呼叫Lock介面中的方法unlock釋放鎖 。

​ 請看如下API說明:

public class RunnableImpl implements Runnable{
    //定義一個多個執行緒共享的票源
    private  int ticket = 100;
    //1.在成員位置建立一個ReentrantLock物件
    Lock l = new ReentrantLock();
    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死迴圈,讓賣票操作重複執行
        while(true){
            //2.在可能會出現安全問題的程式碼前呼叫Lock介面中的方法lock獲取鎖
            l.lock();
            //先判斷票是否存在
            if(ticket>0){
                try {
                    //票存在,賣票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3.在可能會出現安全問題的程式碼後呼叫Lock介面中的方法unlock釋放鎖
                    l.unlock();//無論程式是否異常,都會把鎖釋放
                }
            }
        }
    }
}

五,執行緒池

​ 如果併發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁建立執行緒就會大大降低系統的效率,因為頻繁建立執行緒和銷燬執行緒是需要時間的。

​ 在JDK5之前,對於執行緒池的使用是需要程式設計師用集合來自己進行建立。在JDK5之後就不再需要手動去建立,JDK已經幫我們封裝好了。

5.1,執行緒池概念

  • 執行緒池:其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁建立執行緒物件的操作,無需反覆建立執行緒而消耗過多資源。

​ 用一張簡單的圖來理解下執行緒池工作的原理。

合理利用執行緒池能夠帶來三個好處:

  1. 降低資源消耗。減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  3. 提高執行緒的可管理性。可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體,而把伺服器累趴下(每個執行緒需要大約1MB記憶體,執行緒開的越多,消耗的記憶體也就越大,可能最後宕機)。

5.2,使用方式

​ Java裡面執行緒池的頂級介面是java.util.concurrent.Executor,但是嚴格意義上講Executor並不是一個執行緒池,而只是一個執行執行緒的工具。真正的執行緒池介面是java.util.concurrent.ExecutorService

要配置一個執行緒池是比較複雜的,尤其是對於執行緒池的原理不是很清楚的情況下,很有可能配置的執行緒池不是較優的,因此在java.util.concurrent.Executors執行緒工廠類裡面提供了一些靜態工廠,生成一些常用的執行緒池。官方建議使用Executors工程類來建立執行緒池物件。

Executors類中有個建立執行緒池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回執行緒池物件。(建立的是有界執行緒池,也就是池中的執行緒個數可以指定最大數量)

獲取到了一個執行緒池ExecutorService 物件,那麼怎麼使用呢,在這裡定義了一個使用執行緒池物件的方法如下:

  • public Future<?> submit(Runnable task):獲取執行緒池中的某一個執行緒物件,並執行

    Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池建立與使用。

使用執行緒池中執行緒物件的步驟:

  1. 建立執行緒池物件。
  2. 建立Runnable介面子類物件。(task)
  3. 提交Runnable介面子類物件。(take task)
  4. 關閉執行緒池(一般不做,因為再次使用的時候執行緒池中就沒有執行緒了)。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒: " + Thread.currentThread().getName());
    }
}
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 建立執行緒池物件
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個執行緒物件
        // 建立Runnable例項物件
        MyRunnable r = new MyRunnable();
        // 從執行緒池中獲取執行緒物件,然後呼叫MyRunnable中的run()
        service.submit(r);
        // 再獲取個執行緒物件,呼叫MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法呼叫結束後,程式並不終止,是因為執行緒池控制了執行緒的關閉。
        // 將使用完的執行緒又歸還到了執行緒池中
    }
}

六,sleep()與wait()

​ 1,所屬分類不同,sleep屬於Thread類中,而wait屬於Object類中。

​ 2,鎖控制不同,sleep不會釋放鎖,而wait會釋放鎖且不會影響其他執行緒進入同步程式碼塊或同步方法中。也就是說sleep會佔用資源,wait不會佔用資源。

​ 3,sleep可以在任意地方使用,而wait需在同步程式碼塊或者同步方法中使用。

七,volatile與synchronized

​ 1,volatile效能比synchronized要好,因為volatile是執行緒同步的輕量級實現。

​ 2,volatile只能修飾變數,synchronized可以修飾方法以及程式碼塊。

​ 3,多執行緒訪問volatile不會發生阻塞,synchronized會出現阻塞。

​ 4,volatile能保證資料的可見性,但不能保證原子性。synchronized可以保證原子性,也可以間接的保證可見性。

​ 5,volatile解決的是變數在多執行緒之間的可見性。synchronized解決的是多執行緒之間訪問資源的同步性。

​ 6,volatile防止指令重排。

八,總結

​ 似乎覺得本人的每一篇部落格的篇幅都好長,可能是因為都是一些很基礎的知識點吧,所以就會涉及到很多方方面面,寫著寫著就很多了。不過這樣記下來時間久了還可以再回來看看,多少也算有點印象(哈哈)。

​ 如果你閱讀到此,很感謝您的耐心。以上總結的內容,如有不適之處,歡迎留言指正。

感謝閱讀