多線程設(shè)計(jì)模式:第一篇 - Java線程基礎(chǔ)

一,線程基礎(chǔ)

1,基礎(chǔ)概念

????一個(gè)線程就是運(yùn)行在一個(gè)進(jìn)程上下文中的一個(gè)邏輯流,而進(jìn)程是程序執(zhí)行的實(shí)例。系統(tǒng)中每個(gè)運(yùn)行著的程序都運(yùn)行在一個(gè)進(jìn)程上下文環(huán)境中,進(jìn)程上下文由程序正確運(yùn)行所必須的狀態(tài)組成,包括程序代碼,數(shù)據(jù),程序運(yùn)行棧,寄存器,指令計(jì)數(shù)器,環(huán)境變量以及進(jìn)程打開的文件描述符集合,這些都保存在進(jìn)程控制塊中。

????現(xiàn)代操作系統(tǒng)調(diào)度的最小單位是線程,也叫輕量級(jí)進(jìn)程,在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程,每個(gè)線程也有自己的運(yùn)行上下文環(huán)境,包括唯一的線程ID,??臻g,程序計(jì)數(shù)器等。多個(gè)線程運(yùn)行在同一個(gè)進(jìn)程環(huán)境中,因此共享進(jìn)程環(huán)境中的堆,代碼,共享庫和打開的文件描述符。

????Java是天生的多線程程序,main() 方法由一個(gè)被稱為主線程的線程調(diào)用,之后在 main() 方法中可以再生出更多的自定義線程。

2,線程啟動(dòng)和暫停

????Java線程啟動(dòng)有兩種方式,一種是通過繼承 Thread 類,一種是通過實(shí)現(xiàn) Runnable 接口,示例代碼如下:

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

/**
 * @author koma <komazhang@foxmail.com>
 * @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());
        }
    }
}

????通過示例代碼可以得知,由于同一個(gè)線程不能啟動(dòng)多次(即調(diào)用多次 start() 方法),繼承 Thread 類實(shí)現(xiàn)的線程中的 run() 方法如果要在多個(gè)線程中執(zhí)行,則需要 new 多次 ThreadDemo1,而對(duì)于實(shí)現(xiàn) Runnable 接口類的 ThreadDemo2 則只需要 new 一次即可。這兩種方法各有適用場(chǎng)景,需靈活運(yùn)用。

????Java多線程編程后期較為常用的方式是使用 Executors 框架,利用該框架啟動(dòng)線程的實(shí)例代碼如下(這會(huì)比較常見):

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

????這里重用了 ThreadDemo2 的實(shí)例,同時(shí)框架內(nèi)部使用了線程池技術(shù),這個(gè)后續(xù)再討論。

????線程暫停最基本的方法是通過 Thread 類的 sleep 方法,這會(huì)讓線程進(jìn)入到休眠狀態(tài)等待一段時(shí)間再運(yùn)行,線程卻不會(huì)退出。讓線程退出的方法一種是通過中斷,另外一種則是設(shè)置標(biāo)識(shí)位,這種方法相比是比較優(yōu)雅的一種方式,因?yàn)檫@給了線程充分的時(shí)間去執(zhí)行現(xiàn)場(chǎng)清理工作,從容退出。三種方式的舉例代碼如下:

/**
 * @author koma <komazhang@foxmail.com>
 * @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();

        //讓線程充分運(yùn)行
        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秒之后繼續(xù)運(yùn)行
                } 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) {
                    //線程在暫停中收到中斷信號(hào),做出反應(yīng)并退出
                    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) {
                    //線程運(yùn)行中判斷標(biāo)記位,當(dāng)標(biāo)記位被設(shè)置之后執(zhí)行清理工作并退出
                    System.out.printf("%s will exit...\n", Thread.currentThread().getName());
                    break;
                }
            }
        }

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

????運(yùn)行實(shí)例代碼觀察輸出可以知道,threadDemo2 因?yàn)槭盏街袛嘈盘?hào)而退出,threadDemo3 因?yàn)闃?biāo)志位被設(shè)置而退出,只有 threadDemo1 在一直運(yùn)行。

????觀察代碼運(yùn)行還可以發(fā)現(xiàn),調(diào)用線程的 interrupt() 方法會(huì)打斷 sleep 過程,即該方法可以使線程的 sleep 方法立即拋出一個(gè) InterruptedException 異常,而不去關(guān)心 sleep 時(shí)間是否到期。

3,線程互斥

????多線程程序中的各個(gè)線程的運(yùn)行時(shí)機(jī)是由操作系統(tǒng)調(diào)度確定的,而不能進(jìn)行人工干預(yù),因此當(dāng)多個(gè)線程操作同一個(gè)堆實(shí)例時(shí)由于運(yùn)行時(shí)機(jī)的不確定性導(dǎo)致運(yùn)行結(jié)果不可預(yù)測(cè),這在某些情況下會(huì)引發(fā)程序錯(cuò)誤。

????這種由于多個(gè)線程同時(shí)操作而引起錯(cuò)誤的情況稱為數(shù)據(jù)競(jìng)爭(zhēng)或競(jìng)態(tài)條件,這種情況下就需要進(jìn)行線程互斥處理。在 Java 中最簡(jiǎn)單的互斥操作是通過 synchronized 關(guān)鍵字。

import java.util.Random;

/**
 * @author koma <komazhang@foxmail.com>
 * @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());
        //啟動(dòng)四個(gè)線程
        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();
            //睡眠隨機(jī)時(shí)間,模擬線程被搶占
            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() 這個(gè)方法在沒有做線程互斥時(shí),打印的 count 值變化混亂,沒有按預(yù)期多線程自增。當(dāng)給其增加線程互斥之后才能實(shí)現(xiàn)預(yù)期效果,如下:

private synchronized void changeData() {
    int count = this.data.getCount();
    //睡眠隨機(jī)時(shí)間,模擬線程被搶占
    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 關(guān)鍵字實(shí)現(xiàn)的原理是在執(zhí)行其包含的方法前,線程會(huì)先去嘗試獲得一把鎖,只有成功獲得鎖的線程才能執(zhí)行方法,而沒有獲得鎖的線程則會(huì)等待,直到方法被執(zhí)行完成返回之后鎖被釋放,其它線程才能再去競(jìng)爭(zhēng)鎖,這樣就保證了方法每次只運(yùn)行一個(gè)線程執(zhí)行,實(shí)際上在這里把并行的邏輯串行化了。

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

@Override
public void run() {
    while (true) {
        synchronized (this) { //同步代碼塊
            int count = this.data.getCount();
            //睡眠隨機(jī)時(shí)間,模擬線程被搶占
            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 關(guān)鍵字加在方法聲明上實(shí)際上持有的鎖是 this 對(duì)象的鎖。但是當(dāng)方法同時(shí)聲明為 static 時(shí), synchronized 持有的鎖就變成了類的鎖,這和 this 對(duì)象的鎖存在明顯差異。因?yàn)?this 對(duì)象的鎖是類實(shí)例的鎖,那么類實(shí)例化一次就會(huì)有一把鎖,而類始終只有一個(gè),因此類的鎖總是只有一把。

????把上述實(shí)例代碼中的 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) { //使用類實(shí)例鎖
            int count = this.data.getCount();
            //睡眠隨機(jī)時(shí)間,模擬線程被搶占
            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 關(guān)鍵字聲明意義一樣,因此這里使用代碼塊方式說明。如上代碼描述中所示,在 synchronized 代碼塊上如果繼續(xù)使用 this 鎖,則依然無法達(dá)到預(yù)期效果,根本原因是現(xiàn)在每個(gè)線程都有一個(gè)類實(shí)例,導(dǎo)致每個(gè)線程中的 this 鎖是獨(dú)立的,而使用類的鎖時(shí),則代碼會(huì)如預(yù)期運(yùn)行,原因就是類的鎖只有一把,和類實(shí)例個(gè)數(shù)無關(guān)。

4,線程協(xié)作

????所謂線程間協(xié)作,一種是上面說的線程之間在某一刻要互斥的順序運(yùn)行,一種則是類似于生產(chǎn)者-消費(fèi)者模式,線程之間合作完成任務(wù),當(dāng)任務(wù)狀態(tài)滿足或者不滿足時(shí)需要線程之間相互通知。這種機(jī)制就是通知/等待機(jī)制,使用 java Object 對(duì)象的 wait(),notify(),notifyAll() 方法來實(shí)現(xiàn),下面的代碼示例使用該機(jī)制實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的生產(chǎn)者-消費(fèi)者模式。

/**
 * @author koma <komazhang@foxmail.com>
 * @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 充分運(yùn)行
        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() 方法是讓當(dāng)前線程進(jìn)入到調(diào)用 wait() 方法的對(duì)象的等待隊(duì)列中,而 notify(),notifyAll() 方法則是從調(diào)用對(duì)象的等待隊(duì)列中喚醒一個(gè)或全部線程。規(guī)定調(diào)用 wait(),notify(),notifyAll() 前需要先獲取調(diào)用對(duì)象的鎖,同時(shí)在調(diào)用 wait() 方法之后,剛剛獲取到的對(duì)象的鎖會(huì)被釋放,以便其它線程有機(jī)會(huì)去競(jìng)爭(zhēng)鎖,而在調(diào)用 notify(),notifyAll() 方法之后則不會(huì)主動(dòng)釋放鎖,因?yàn)榭赡茉谶@之后當(dāng)前線程還有別的工作需要做完才能釋放鎖。

????由于在調(diào)用 wait() 之后線程會(huì)阻塞在當(dāng)前位置,當(dāng)調(diào)用 notify 之后線程會(huì)從當(dāng)前位置繼續(xù)往下執(zhí)行,但是由于這時(shí)有可能 product 的狀態(tài)恰好又被其它線程改變,那么當(dāng)前線程繼續(xù)往下執(zhí)行就會(huì)產(chǎn)生意外的情況,因此我們通常的調(diào)用 wait() 的方法是放到一個(gè) while 循環(huán)中,像下面這樣:

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

????這種方法會(huì)使得我們的代碼更加健壯。另外一個(gè)會(huì)使代碼更加健壯的做法是盡量使用 notifyAll() 而不是 notify(),因?yàn)檎{(diào)用 notify() 方法只喚醒等待隊(duì)列中的一個(gè)線程,那么對(duì)于等待隊(duì)列中既有消費(fèi)者,又有生產(chǎn)者時(shí),那么當(dāng)消費(fèi)者線程調(diào)用 notify() 有可能會(huì)還是喚醒消費(fèi)者線程,如果這種情況的概率較大,則程序便會(huì)停止但是不報(bào)錯(cuò)。

5,線程狀態(tài)轉(zhuǎn)換

????線程包括 NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED 這幾種狀態(tài),可以通過 Thread 的 getState() 方法獲取到。線程在整個(gè)生命周期中經(jīng)歷的狀態(tài)轉(zhuǎn)換都可以包括到這張圖中

二,多線程程序的評(píng)價(jià)標(biāo)準(zhǔn)

1,安全性

????安全性是指不損壞對(duì)象,即對(duì)象的狀態(tài)或值一定要復(fù)合預(yù)期設(shè)計(jì)。當(dāng)一個(gè)類被多線程調(diào)用時(shí),如果也能保證對(duì)象的安全性,則該類稱為線程安全類,否則稱為線程不安全類。

2,生存性

????生存性是指在任何時(shí)刻,程序的必要處理一定能夠完成,這也是程序正常運(yùn)行的必要條件,也稱為程序的活性。常見的場(chǎng)景是程序運(yùn)行存在死鎖或活鎖,導(dǎo)致程序不能夠正常運(yùn)行,這就違反了線程的生存性。

3,可復(fù)用性

????可復(fù)用性是指類能夠重復(fù)利用,主要目標(biāo)是提高程序的質(zhì)量。

4,性能

????性能是指程序能夠快速的,大批量的執(zhí)行處理,主要目標(biāo)是提高程序的質(zhì)量。性能的主要指標(biāo)包括:吞吐量 - 單位時(shí)間內(nèi)完成的處理數(shù)量,越大表示性能越高;響應(yīng)性 - 指從發(fā)出請(qǐng)求到收到請(qǐng)求響應(yīng)的時(shí)間間隔,越短表示性能越高;容量 - 是指可同時(shí)進(jìn)行的處理數(shù)量,越多表示性能越高;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 進(jìn)程和線程 進(jìn)程 所有運(yùn)行中的任務(wù)通常對(duì)應(yīng)一個(gè)進(jìn)程,當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行時(shí),即變成一個(gè)進(jìn)程.進(jìn)程是處于運(yùn)行過程中...
    勝浩_ae28閱讀 5,265評(píng)論 0 23
  • 進(jìn)程和線程 進(jìn)程 所有運(yùn)行中的任務(wù)通常對(duì)應(yīng)一個(gè)進(jìn)程,當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行時(shí),即變成一個(gè)進(jìn)程.進(jìn)程是處于運(yùn)行過程中...
    小徐andorid閱讀 2,995評(píng)論 3 53
  • 本文主要講了java中多線程的使用方法、線程同步、線程數(shù)據(jù)傳遞、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等。 首先講...
    李欣陽閱讀 2,602評(píng)論 1 15
  • (這是一篇寫於八十年代初期并一直放在抽屜里的舊文,今天偶然翻閱,從中仍隱隱感覺得到那個(gè)時(shí)代的生活氣息) 巷子,彎彎...
    南山老李閱讀 463評(píng)論 2 8
  • 我一直覺得漢文帝劉恒才是公務(wù)員的典范,可能介于他超級(jí)大地主的身份,一直沒有得到應(yīng)有的宣傳。這個(gè)人是我崇拜的一流當(dāng)權(quán)...
    冷瀟湘閱讀 446評(píng)論 0 0

友情鏈接更多精彩內(nèi)容