多線程設計模式:第一篇 - Java線程基礎

一,線程基礎

1,基礎概念

    一個線程就是運行在一個進程上下文中的一個邏輯流,而進程是程序執行的實例。系統中每個運行着的程序都運行在一個進程上下文環境中,進程上下文由程序正確運行所必須的狀態組成,包括程序代碼,數據,程序運行棧,寄存器,指令計數器,環境變量以及進程打開的文件描述符集合,這些都保存在進程控制塊中。

    現代操作系統調度的最小單位是線程,也叫輕量級進程,在一個進程裏可以創建多個線程,每個線程也有自己的運行上下文環境,包括唯一的線程ID,棧空間,程序計數器等。多個線程運行在同一個進程環境中,因此共享進程環境中的堆,代碼,共享庫和打開的文件描述符。

    Java是天生的多線程程序,main() 方法由一個被稱爲主線程的線程調用,之後在 main() 方法中可以再生出更多的自定義線程。

2,線程啓動和暫停

    Java線程啓動有兩種方式,一種是通過繼承 Thread 類,一種是通過實現 Runnable 接口,示例代碼如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * @author koma <[email protected]>
 * @date 2018-10-09 17:21
 */
public class StartThreadDemo {
    public static void main(String[] args) {
        StartThreadDemo startThreadDemo = new StartThreadDemo();
        startThreadDemo.testThread();
    }

    public void testThread() {
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        threadDemo1.start();

        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        new Thread(threadDemo2).start();
    }

    class ThreadDemo1 extends Thread {
        @Override
        public void run() {
            System.out.printf("%s run...\n", Thread.currentThread().getName());
        }
    }

    class ThreadDemo2 implements Runnable {
        @Override
        public void run() {
            System.out.printf("%s run...\n", Thread.currentThread().getName());
        }
    }
}

    通過示例代碼可以得知,由於同一個線程不能啓動多次(即調用多次 start() 方法),繼承 Thread 類實現的線程中的 run() 方法如果要在多個線程中執行,則需要 new 多次 ThreadDemo1,而對於實現 Runnable 接口類的 ThreadDemo2 則只需要 new 一次即可。這兩種方法各有適用場景,需靈活運用。

    Java多線程編程後期較爲常用的方式是使用 Executors 框架,利用該框架啓動線程的實例代碼如下(這會比較常見):

ThreadFactory threadFactory = Executors.defaultThreadFactory();
threadFactory.newThread(threadDemo2).start();

    這裏重用了 ThreadDemo2 的實例,同時框架內部使用了線程池技術,這個後續再討論。

    線程暫停最基本的方法是通過 Thread 類的 sleep 方法,這會讓線程進入到休眠狀態等待一段時間再運行,線程卻不會退出。讓線程退出的方法一種是通過中斷,另外一種則是設置標識位,這種方法相比是比較優雅的一種方式,因爲這給了線程充分的時間去執行現場清理工作,從容退出。三種方式的舉例代碼如下:

/**
 * @author koma <[email protected]>
 * @date 2018-10-09 17:21
 */
public class StartThreadDemo {
    public static void main(String[] args) {
        StartThreadDemo startThreadDemo = new StartThreadDemo();
        startThreadDemo.testThread();
    }

    public void testThread() {
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        ThreadDemo3 threadDemo3 = new ThreadDemo3();
        threadDemo1.start();
        threadDemo2.start();
        threadDemo3.start();

        //讓線程充分運行
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }

        threadDemo1.interrupt();
        threadDemo2.interrupt();
        threadDemo3.exit();

        //等待線程終止
        try {
            threadDemo2.join();
            System.out.println("threadDemo2 exit");
            threadDemo3.join();
            System.out.println("threadDemo3 exit");
            threadDemo1.join();
            System.out.println("threadDemo1 exit");
        } catch (InterruptedException e) {
        }
    }

    class ThreadDemo1 extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.printf("%s run...\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(5000); //線程暫停3秒之後繼續運行
                } catch (InterruptedException e) {
                }
            }
        }
    }

    class ThreadDemo2 extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.printf("%s run...\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    //線程在暫停中收到中斷信號,做出反應並退出
                    System.out.printf("%s will exit...\n", Thread.currentThread().getName());
                    break;
                }
            }
        }
    }

    class ThreadDemo3 extends Thread {
        private boolean flag = false;

        @Override
        public void run() {
            while (true) {
                System.out.printf("%s run...\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }

                if (flag) {
                    //線程運行中判斷標記位,當標記位被設置之後執行清理工作並退出
                    System.out.printf("%s will exit...\n", Thread.currentThread().getName());
                    break;
                }
            }
        }

        public void exit() {
            this.flag = true;
        }
    }
}

    運行實例代碼觀察輸出可以知道,threadDemo2 因爲收到中斷信號而退出,threadDemo3 因爲標誌位被設置而退出,只有 threadDemo1 在一直運行。

    觀察代碼運行還可以發現,調用線程的 interrupt() 方法會打斷 sleep 過程,即該方法可以使線程的 sleep 方法立即拋出一個 InterruptedException 異常,而不去關心 sleep 時間是否到期。

3,線程互斥

    多線程程序中的各個線程的運行時機是由操作系統調度確定的,而不能進行人工干預,因此當多個線程操作同一個堆實例時由於運行時機的不確定性導致運行結果不可預測,這在某些情況下會引發程序錯誤。

    這種由於多個線程同時操作而引起錯誤的情況稱爲數據競爭或競態條件,這種情況下就需要進行線程互斥處理。在 Java 中最簡單的互斥操作是通過 synchronized 關鍵字。

import java.util.Random;

/**
 * @author koma <[email protected]>
 * @date 2018-10-09 23:26
 */
public class SyncThreadDemo {
    public static void main(String[] args) {
        SyncThreadDemo syncThreadDemo = new SyncThreadDemo();
        syncThreadDemo.test();
    }

    public void test() {
        Thread1 thread1 = new Thread1(new Data());
        //啓動四個線程
        new Thread(thread1).start();
        new Thread(thread1).start();
        new Thread(thread1).start();
        new Thread(thread1).start();
    }

    class Thread1 implements Runnable {
        private Data data;
        private Random random = new Random();

        public Thread1(Data data) {
            this.data = data;
        }

        @Override
        public void run() {
            while (true) {
                this.changeData();
            }
        }

        private void changeData() {
            int count = this.data.getCount();
            //睡眠隨機時間,模擬線程被搶佔
            try {
                Thread.sleep(this.random.nextInt(500));
            } catch (InterruptedException e) {
            }
            this.data.setCount(count+1);
            System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
        }
    }

    class Data {
        private int count = 0;

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }
    }
}

    上面代碼示例中,changeData() 這個方法在沒有做線程互斥時,打印的 count 值變化混亂,沒有按預期多線程自增。當給其增加線程互斥之後才能實現預期效果,如下:

private synchronized void changeData() {
    int count = this.data.getCount();
    //睡眠隨機時間,模擬線程被搶佔
    try {
        Thread.sleep(this.random.nextInt(500));
    } catch (InterruptedException e) {
    }
    this.data.setCount(count+1);
    System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
}

    synchronized 關鍵字實現的原理是在執行其包含的方法前,線程會先去嘗試獲得一把鎖,只有成功獲得鎖的線程才能執行方法,而沒有獲得鎖的線程則會等待,直到方法被執行完成返回之後鎖被釋放,其它線程才能再去競爭鎖,這樣就保證了方法每次只運行一個線程執行,實際上在這裏把並行的邏輯串行化了。

    上述示例代碼中的 run() 方法還可以寫成下面這樣:

@Override
public void run() {
    while (true) {
        synchronized (this) { //同步代碼塊
            int count = this.data.getCount();
            //睡眠隨機時間,模擬線程被搶佔
            try {
                Thread.sleep(this.random.nextInt(500));
            } catch (InterruptedException e) {
            }
            this.data.setCount(count+1);
            System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
        }
    }
}

    兩種寫法說明,synchronized 關鍵字加在方法聲明上實際上持有的鎖是 this 對象的鎖。但是當方法同時聲明爲 static 時, synchronized 持有的鎖就變成了類的鎖,這和 this 對象的鎖存在明顯差異。因爲 this 對象的鎖是類實例的鎖,那麼類實例化一次就會有一把鎖,而類始終只有一個,因此類的鎖總是隻有一把。

    把上述實例代碼中的 test() 方法和 run() 方法改寫,如下:

public void test() {
    Data data = new Data();
    new Thread(new Thread1(data)).start();
    new Thread(new Thread1(data)).start();
    new Thread(new Thread1(data)).start();
    new Thread(new Thread1(data)).start();
}

@Override
public void run() {
    while (true) {
        synchronized (Thread1.class) { //使用類的鎖
        //synchronized (this) { //使用類實例鎖
            int count = this.data.getCount();
            //睡眠隨機時間,模擬線程被搶佔
            try {
                Thread.sleep(this.random.nextInt(500));
            } catch (InterruptedException e) {
            }
            this.data.setCount(count+1);
            System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
        }
    }
}

    這裏在 synchronized 代碼塊中使用類的鎖和在 static 方法上加上 synchronized 關鍵字聲明意義一樣,因此這裏使用代碼塊方式說明。如上代碼描述中所示,在 synchronized 代碼塊上如果繼續使用 this 鎖,則依然無法達到預期效果,根本原因是現在每個線程都有一個類實例,導致每個線程中的 this 鎖是獨立的,而使用類的鎖時,則代碼會如預期運行,原因就是類的鎖只有一把,和類實例個數無關。

4,線程協作

    所謂線程間協作,一種是上面說的線程之間在某一刻要互斥的順序運行,一種則是類似於生產者-消費者模式,線程之間合作完成任務,當任務狀態滿足或者不滿足時需要線程之間相互通知。這種機制就是通知/等待機制,使用 java Object 對象的 wait(),notify(),notifyAll() 方法來實現,下面的代碼示例使用該機制實現了一個簡單的生產者-消費者模式。

/**
 * @author koma <[email protected]>
 * @date 2018-10-10 00:35
 */
public class CooperationThread {
    public static void main(String[] args) {
        CooperationThread cooperationThread = new CooperationThread();
        cooperationThread.test();
    }

    public void test() {
        Product product = new Product();
        new Thread(new Consumer(product)).start();
        new Thread(new Consumer(product)).start();
        //讓 Consumer 充分運行
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
        new Thread(new Producer(product)).start();
    }

    class Product {
        private int count = 0;

        public void produce() {
            count++;
            System.out.println("produce: "+count);
        }

        public void consume() {
            count--;
            System.out.println("consume: "+count);
        }

        public boolean canConsume() {
            return this.count > 0;
        }

        public boolean canproduce() {
            return this.count == 0;
        }
    }

    class Producer implements Runnable {
        private Product product;
        public Producer(Product product) {
            this.product = product;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (this.product) {
                    while (!this.product.canproduce()) {
                        try {
                            this.product.wait();
                        } catch (InterruptedException e) {
                        }
                    }

                    this.product.produce();
                    this.product.notifyAll();
                }
            }
        }
    }

    class Consumer implements Runnable {
        private Product product;
        public Consumer(Product product) {
            this.product = product;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (this.product) {
                    while (!this.product.canConsume()) {
                        try {
                            this.product.wait();
                        } catch (InterruptedException e) {
                        }
                    }

                    this.product.consume();
                    this.product.notifyAll();
                }
            }
        }
    }
}

    wait() 方法是讓當前線程進入到調用 wait() 方法的對象的等待隊列中,而 notify(),notifyAll() 方法則是從調用對象的等待隊列中喚醒一個或全部線程。規定調用 wait(),notify(),notifyAll() 前需要先獲取調用對象的鎖,同時在調用 wait() 方法之後,剛剛獲取到的對象的鎖會被釋放,以便其它線程有機會去競爭鎖,而在調用 notify(),notifyAll() 方法之後則不會主動釋放鎖,因爲可能在這之後當前線程還有別的工作需要做完才能釋放鎖。

    由於在調用 wait() 之後線程會阻塞在當前位置,當調用 notify 之後線程會從當前位置繼續往下執行,但是由於這時有可能 product 的狀態恰好又被其它線程改變,那麼當前線程繼續往下執行就會產生意外的情況,因此我們通常的調用 wait() 的方法是放到一個 while 循環中,像下面這樣:

while (!this.product.canConsume()) {
    try {
        this.product.wait();
    } catch (InterruptedException e) {
    }
}

    這種方法會使得我們的代碼更加健壯。另外一個會使代碼更加健壯的做法是儘量使用 notifyAll() 而不是 notify(),因爲調用 notify() 方法只喚醒等待隊列中的一個線程,那麼對於等待隊列中既有消費者,又有生產者時,那麼當消費者線程調用 notify() 有可能會還是喚醒消費者線程,如果這種情況的概率較大,則程序便會停止但是不報錯。

5,線程狀態轉換

    線程包括 NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED 這幾種狀態,可以通過 Thread 的 getState() 方法獲取到。線程在整個生命週期中經歷的狀態轉換都可以包括到這張圖中

二,多線程程序的評價標準

1,安全性

    安全性是指不損壞對象,即對象的狀態或值一定要複合預期設計。當一個類被多線程調用時,如果也能保證對象的安全性,則該類稱爲線程安全類,否則稱爲線程不安全類。

2,生存性

    生存性是指在任何時刻,程序的必要處理一定能夠完成,這也是程序正常運行的必要條件,也稱爲程序的活性。常見的場景是程序運行存在死鎖或活鎖,導致程序不能夠正常運行,這就違反了線程的生存性。

3,可複用性

    可複用性是指類能夠重複利用,主要目標是提高程序的質量。

4,性能

    性能是指程序能夠快速的,大批量的執行處理,主要目標是提高程序的質量。性能的主要指標包括:吞吐量 - 單位時間內完成的處理數量,越大表示性能越高;響應性 - 指從發出請求到收到請求響應的時間間隔,越短表示性能越高;容量 - 是指可同時進行的處理數量,越多表示性能越高;

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