Java并發(fā)編程系列--并發(fā)基礎(chǔ)

java從誕生之日起,就明智的選擇了內(nèi)置對多線程的支持。

幾個概念
在開始寫并發(fā)之前,先介紹幾個簡單的概念:

  • 并發(fā)和并行: 并發(fā)指多個任務(wù)交替的執(zhí)行,并行指多個任務(wù)同時執(zhí)行
  • 臨界區(qū):表示一種公共資源或者共享數(shù)據(jù),一次只能有一個線程訪問它
  • JMM的特性: 原子性,可見性,有序性

程序、進(jìn)程、線程

  • 程序:具有某些功能的代碼。
  • 進(jìn)程:操作系統(tǒng)進(jìn)行資源分配和資源調(diào)度的基本單位。進(jìn)程是程序執(zhí)行的實(shí)體。
  • 線程:輕量級的進(jìn)程,程序執(zhí)行的最小單位,線程中含有獨(dú)立的計數(shù)器,堆棧和局部變量等屬性,必須擁有一個父進(jìn)程,并且共享進(jìn)程中所擁有的全部資源。

進(jìn)程之間不能共享內(nèi)存,線程共享內(nèi)存非常容易。

線程的狀態(tài)
線程是有生命周期的,生命周期的狀態(tài)如下:

  • NEW :新建
  • READY:就緒
  • RUNNABLE : 運(yùn)行
  • BLOCKED :阻塞
  • WAITING : 等待
  • TIME_WAITING:超時等待
  • TERMINATED : 終止

Java程序中的線程在自身的生命周期中,并不是固定地處于某個狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換。轉(zhuǎn)換圖如下:


線程生命周期狀態(tài)圖.png

新建狀態(tài)

  • 使用new關(guān)鍵字創(chuàng)建線程之后,線程就處于新建狀態(tài),此時JVM為其分配了內(nèi)存,并初始化了成員變量。此時并沒有表現(xiàn)出線程的任何動態(tài)特征,程序也不會執(zhí)行線程執(zhí)行體。

就緒狀態(tài)

  • 對象調(diào)用了start()方法后,該線程就處于就緒狀態(tài),JVM會為其創(chuàng)建方法調(diào)用棧和程序計數(shù)器,處于這個狀態(tài)的線程并沒有運(yùn)行,只是表示可以運(yùn)行了,何時運(yùn)行,取決于系統(tǒng)調(diào)度。

運(yùn)行狀態(tài)

  • 就緒狀態(tài)的線程,獲得CPU之后,就處于運(yùn)行狀態(tài),當(dāng)當(dāng)前的時間片用完,或者運(yùn)行yield()/sleep()方法時,線程就必須放棄CPU,結(jié)束運(yùn)行狀態(tài)。

阻塞狀態(tài)/等待/超時等待

  • 線程調(diào)用sleep()/join()/wait()方法,放棄CPU資源,線程被阻塞/等待
  • 線程調(diào)用了一個IO方法,在該方法返回前,被阻塞
  • 線程試圖獲取一個同步監(jiān)視器,但是失敗了,被阻塞
  • 線程在等待某個通知

線程結(jié)束

  • run()/call()方法執(zhí)行體執(zhí)行完成,線程正常結(jié)束
  • 拋出Exception/Error
  • 調(diào)用線程結(jié)束控制

上面提到了線程創(chuàng)建的三種方式,在代碼層級來看看線程的具體實(shí)現(xiàn):

線程的創(chuàng)建

繼承Thread類


public class ThreadTest extends  Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("我是線程執(zhí)行體 !");
    }
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
    }
}

實(shí)現(xiàn)Runnable接口


class RunnableTest implements Runnable{
    @Override
    public void run() {
        System.out.println("runnable 執(zhí)行體");
    }
    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableTest());
        thread.start();
    }
}

實(shí)現(xiàn)Callable接口和FutureTask

class callableTest implements Callable{

    @Override
    public Object call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return 3;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        callableTest callableTest = new callableTest();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(callableTest);
        Thread thread = new Thread(futureTask , "有返回值的線程");
        thread.start();
        System.out.println(Thread.currentThread().getName());
        System.out.println(futureTask.get());
    }
}

上面展示了三種創(chuàng)建線程的三種方式,下面做一下簡要的分析:

  • 實(shí)現(xiàn)接口的方式,還可以繼承其他的類
  • 多個線程可以共享同一個target對象,適合多個線程來處理同一份資源。但是編程稍微復(fù)雜
  • 繼承Thread類的方式不能在繼承其它的類,但是編寫簡單。

一般推薦使用實(shí)現(xiàn)接口的方式來創(chuàng)建多線程。

線程執(zhí)行的任務(wù)定義在了run()方法中,只有通過start()方法才能創(chuàng)建線程,這是為什么呢,通過源代碼來分析一下:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runable是一個函數(shù)式接口,定義也比較簡單,只是定義了一個抽象的方法。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable也是一個函數(shù)式接口,并且支持泛型,并返回泛型的類型。

看一下Thread類的實(shí)現(xiàn),Thread類的代碼比較多,這里只闡述一些重要的點(diǎn):

先來看Thread類的定義

public class Thread implements Runnable

Thead類實(shí)現(xiàn)了Runable接口

構(gòu)造方法: Thread類提供了9個構(gòu)造方法,只闡述2個構(gòu)造方法,這兩個也是最常用的方法,如下。

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

//init 方法重載
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {

        this.name = name.toCharArray();
        //刪除了一些代碼,這些代碼會獲取一些父線程的屬性,并設(shè)置到當(dāng)前線程
        ....  

        this.target = target;  //敲黑板,畫重點(diǎn)
    }

上面的代碼是一個線程的創(chuàng)建過程,包括獲取一些父進(jìn)程的屬性,如設(shè)置線程的名字,設(shè)置是否是守護(hù)進(jìn)程,優(yōu)先級等。如果在創(chuàng)建線程的時候沒有指定線程的名字,那么線程的名字為:Thread-num的形式,就是通過上面的代碼來創(chuàng)建的,nextThreadNum()方法會返回一個數(shù)字,nextThreadNum()是一個被synchronized關(guān)鍵字修飾的方法,會將一個被static int類型修飾的整數(shù)進(jìn)行+1操作,并返回,這樣就保證了線程的名字不會重復(fù),當(dāng)然也可以自己指定線程的名字,在創(chuàng)建的時候傳入即可。

上面代碼target是一個私有的Runable變量,定義如下:

    private Runnable target;

看一下Thread類的start()方法:

public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);

        boolean started = false;
        try {
            start0();   //敲黑板,畫重點(diǎn),調(diào)用start0()方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
private native void start0();

start()方法是一個同步方法,并且會調(diào)用start0()來運(yùn)行,start0()方法是一個被native修飾的本地方法,將委托給操作系統(tǒng)來運(yùn)行,并調(diào)用run()方法。

定義如下:

private native void start0();

調(diào)用start()方法后,線程就處于了就緒狀態(tài),系統(tǒng)調(diào)度后,變?yōu)檫\(yùn)行狀態(tài),并調(diào)用run()方法, 看一下run()方法的定義:

 @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

Thread類實(shí)現(xiàn)了Runbale接口所以必須要實(shí)現(xiàn)run(),run()方法的實(shí)現(xiàn)比較簡單,首先判斷target對象是否為null,如果為null就什么也不做,如果不為null,則調(diào)用target對象的run()方法。

分析到這,應(yīng)該就能明白,有很多書上都說,無論是繼承Thread類,還是實(shí)現(xiàn)了Runable接口都必須要重寫run()方法,原因就在這。

繼承Thread類的對象,調(diào)用Thread類的無參構(gòu)造方法,此時target對象為空,但是它重寫了run()方法,它會調(diào)用自己的run方法,實(shí)現(xiàn)了Runnable接口的對象,需要借助Thread(Runable target ) 構(gòu)造方法運(yùn)行 , 此時target對象不為空,會調(diào)用target的run()方法。

關(guān)于Callable和FutureTask的實(shí)現(xiàn)方式,這里簡單說一下, FutureTask實(shí)現(xiàn)了 RunnableFuture接口, 而RunnableFuture接口繼承了Runable接口, 所以它能通過new Thread(Runnable target)構(gòu)造函數(shù)來運(yùn)行。會調(diào)用FutureTask的run()方法,在run()方法中調(diào)用了Callable對象的call()方法,獲得了其返回值,換句話說 FutureTask對象封裝了該Callable對象的返回值。通過FutureTask的get()方法來獲得子線程執(zhí)行結(jié)束后的返回值。

線程控制

  • join線程:讓一個線程等待另一個線程執(zhí)行完成的方法 join() ,主線程創(chuàng)建了一個子線程,并且調(diào)用了join()方法,那么主線程只有等待子線程執(zhí)行完成后,才能向下運(yùn)行。如果調(diào)用了join(long millis)主線程會在等到millis時間后向下執(zhí)行。
  • 守護(hù)線程(daemon):一個線程設(shè)置成為守護(hù)線程后,會隨著前臺線程的結(jié)束而結(jié)束。GC(垃圾回收)就是一個非常典型的守護(hù)進(jìn)程。
  • 線程睡眠sleep():使線程進(jìn)入阻塞狀態(tài),不在運(yùn)行
  • 線程讓步y(tǒng)ield():yield和sleep有點(diǎn)類似,不同是它進(jìn)入的不是阻塞狀態(tài),而是就緒狀態(tài)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 該文章轉(zhuǎn)自:http://blog.csdn.net/evankaka/article/details/44153...
    加來依藍(lán)閱讀 7,380評論 3 87
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,754評論 18 399
  • 寫在前面的話: 這篇博客是我從這里“轉(zhuǎn)載”的,為什么轉(zhuǎn)載兩個字加“”呢?因?yàn)檫@絕不是簡單的復(fù)制粘貼,我花了五六個小...
    SmartSean閱讀 4,783評論 12 45
  • Java多線程學(xué)習(xí) [-] 一擴(kuò)展javalangThread類 二實(shí)現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 2,986評論 1 18
  • 本文主要講了java中多線程的使用方法、線程同步、線程數(shù)據(jù)傳遞、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等。 首先講...
    李欣陽閱讀 2,492評論 1 15