多線程與并發(fā)(一):線程的基本概念和用法

1. 基礎(chǔ)知識點回顧

1.1 并發(fā)和并行

并發(fā)(Concurrent):系統(tǒng)只有一個CPU,多個線程在操作時,在一段時間內(nèi)只有一個線程在運行,其他線程處于掛起狀態(tài)。
并發(fā)編程的本質(zhì):充分利用CPU的資源

并行(Parallel):系統(tǒng)有一個以上CPU時,多個線程可以同時執(zhí)行

* 應(yīng)用之異步調(diào)用

從方法調(diào)用的角度:

  • 需要等待結(jié)果返回,才能繼續(xù)運動就是同步
  • 不需要等待結(jié)果返回,就能繼續(xù)運行就是異步

注意:同步在多線程中,還有另外一層意思,讓多個線程步調(diào)一致

1) 設(shè)計
多線程可以讓方法執(zhí)行變?yōu)楫惒剑床灰砂桶偷戎热缱x取磁盤文件時,假設(shè)讀取操作花費了5秒,如果沒有線程調(diào)度機制,這五秒調(diào)用者什么都做不了,其他代碼都得暫停

2) 結(jié)論

  • 比如在項目中視頻文件需要轉(zhuǎn)換格式等操作比較費時時,這時開一個新線程處理視頻格式轉(zhuǎn)換,避免阻塞主線程
  • UI程序中,開線程進行其他操作,避免阻塞UI線程

方法三:FutureTask 配合 Thread
FutureTask 可以接受 Callable類型參數(shù),用來處理有返回結(jié)果的情況

//創(chuàng)建任務(wù)對象
FutureTask<Integer>  task3 = new FutureTask<>(()->{
    log.debug("hello");
    return 100;
});

new Thread(task3, "task3").start();

//主線程阻塞,等待task3執(zhí)行完畢
Integer result = task3.get();

3.4 線程運行原理

棧與棧幀

  • 每個棧由多個棧幀組成,對應(yīng)著每次方法調(diào)用所占用的內(nèi)存
  • 每個線程只能有一個活動棧幀,對應(yīng)著當前正在執(zhí)行的那個方法

線程上下文切換
因為以下一些原因?qū)е翪PU不再執(zhí)行當前線程,轉(zhuǎn)而執(zhí)行另一個線程的代碼

  • 線程的CPU時間片用完
  • 垃圾回收
  • 有更高優(yōu)先級的線程需要運行
  • 線程自己調(diào)用了sleep ,yield, wait, join, park, synchronized, lock等方法

當發(fā)生上下文切換時,需要由操作系統(tǒng)保存當前線程的狀態(tài),并恢復另一個線程的狀態(tài),Java中對應(yīng)的概念就是程序計數(shù)器,它的作用就是記住下一條jvm指令的執(zhí)行地址,是線程私有的

  • 狀態(tài)包括程序計數(shù)器、虛擬機棧中每個棧幀的信息,如局部變量表,操作數(shù)棧,返回地址等
  • 上下文切換頻繁發(fā)生會影響性能

3.5 常見方法

方法名 功能說明 注意
start() 啟動一個新的線程 start()方法只是讓線程進入就緒狀態(tài),里面的代碼不一定立刻運行(CPU時間片還沒分給它)。每個線程對象的start 方法只能調(diào)用一次,調(diào)用多次會出現(xiàn)IllegalThreadStateException
run() 新線程啟動后調(diào)用的方法
join() 等待線程運行結(jié)束 用在兩個線程通信時
join(long n) 等待線程運行結(jié)束,最多等待n毫秒
getId() 獲取線程id id唯一
getName() 獲取線程名
setName(String) 修改線程名
getPriority() 獲取線程優(yōu)先級
setPriority() 修改線程優(yōu)先級 java中規(guī)定線程優(yōu)先級是1~10的整數(shù),較大的優(yōu)先級能提高該線程被CPU調(diào)度的幾率
getState() 獲取線程狀態(tài) java中線程狀態(tài)是由6個enum表示,分別為:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING
isInterrupted() 判斷是否打斷 不會清除打斷標記
isAlive() 線程是否存活(還沒有運行完)
interrupt() 打斷線程 如果被打斷線程正在sleep, wait, join 會導致被打斷的線程拋出InterruptedException,并清除 打斷標記;如果打斷正在運行的程序,則會設(shè)置打斷標記;park線程被打斷,也會設(shè)置打斷標記
interrupted() 靜態(tài)方法,判斷當前線程是否被打斷 會清除打斷標記
sleep(long n) 靜態(tài)方法,讓當前執(zhí)行線程休眠n毫秒,休眠時讓出CPU時間片給其他線程
yield() 靜態(tài)方法,提示線程調(diào)度器讓出當前線程對CPU的使用 主要是為了測試和調(diào)試

3.7 sleep 與yield

sleep

1.調(diào)用sleep 會讓當前線程從RUNNING 進入 Timed_Waiting狀態(tài)

  1. 其他線程可以使用interrupt 方法打斷正在睡眠的線程,這時sleep 方法會拋出InterruptedException.
  2. 睡眠結(jié)束后的線程未必會立刻得到執(zhí)行
  3. 建議用TimeUnit 的sleep 代替Thread 的sleep 來獲得更好的可讀性

yield

  1. 調(diào)用yield 會讓當前線程從RUNNING 進入 RUNNABLE 狀態(tài),然后調(diào)度執(zhí)行其他同優(yōu)先級的線程。如果這段時間沒有同優(yōu)先級的線程,那么不能保證讓當前線程暫停的效果
  2. 具體的實現(xiàn)依賴于操作系統(tǒng)的任務(wù)調(diào)度

2.2 線程優(yōu)先級

  • 線程優(yōu)先級會提示調(diào)度器優(yōu)先調(diào)度該線程,但僅僅是一個提示,調(diào)度器可以忽略
  • 如果CPU比較忙,那么優(yōu)先級高的線程就會獲得更多的時間片,但CPU閑下來,優(yōu)先級幾乎沒用

3.8 join 方法詳解

為什么需要join

下面的代碼執(zhí)行,打印的r是什么?

public class JoinTest {

    static int r =0;

    public static void main(String[] args) {
        test();
    }

    private static void test() {

        Thread t = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });

        t.start();
        System.out.println(Thread.currentThread().getName() + " result is " + r);
    }
}

執(zhí)行結(jié)果:
main result is 0
Thread-0 start ...
Thread-0 end ...


分析:

  • 因為主線程和線程t是并行執(zhí)行的,t線程需要1秒之后才能計算出r=10;
  • 而主線程一開始就要打印結(jié)果,所以只能打印r = 0;

解決方法

  • 讓主線程睡一會,用sleep 行不行?為什么?
  • 用join,加在 t.start之后即可
public class JoinTest {

    static int r =0;

    public static void main(String[] args) {
        test();
    }

    private static void test() {

        Thread t = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });

        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " result is " + r);
    }
}

執(zhí)行結(jié)果:
Thread-0 start ...
Thread-0 end ...
main result is 10


join 方法源碼

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

* 應(yīng)用之同步(案例1)

調(diào)用方角度來講,如果

  • 需要等待結(jié)果返回,才能繼續(xù)運行就是同步
  • 不需要等待結(jié)果返回,就能繼續(xù)運行就是異步
public class JoinTest {
    static int r1 = 0;
    static int r2 = 0;

    public static void main(String[] args) {
        test2();
    }

    private static void test2() {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });


        Thread t2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r2 = 20;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });

        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();

        System.out.println("r1 is "+ r1 + " ; r2 is " + r2 + " ; time is " + (end - start)); //注意這個時間差值
    }
}

注意end - start 的時間差值

執(zhí)行結(jié)果:
Thread-0 start ...
Thread-1 start ...
Thread-0 end ...
Thread-1 end ...
r1 is 10 ; r2 is 20 ; time is 2002


分析如下:

  • 第一個join:等待t1 時,t2并沒有停止,而在運行
  • 第二個join: 1s后,執(zhí)行到此,t2也運行了 1s,因此只需再等待1s。
    如果顛倒兩個join 呢?
    最終輸出都是一樣的。

注意 :如果線程提前結(jié)束,join 也不必繼續(xù)等待。

有時效的join(long n)
public class JoinTest {

    static int r = 0;

    public static void main(String[] args) {
        test3();
    }

    private static void test3() {
        Thread t = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start ...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
            System.out.println(Thread.currentThread().getName() + " end ...");
        });

        t.start();
        try {
            t.join(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " result is " + r);
    }
}

執(zhí)行結(jié)果:
Thread-0 start ...
main result is 0
Thread-0 end ...


2.4 interrupt 方法詳解

打斷 sleep, wait , join 的線程

打斷sleep 的線程,會清空打斷狀態(tài),以sleep為例

public class SleepTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
        System.out.println("interrupt is " + t.isInterrupted());
    }
}

執(zhí)行結(jié)果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.lily.base.SleepTest.lambdamain0(SleepTest.java:9)
at java.lang.Thread.run(Thread.java:748)
interrupt is false


public class SleepTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t.start();
        t.interrupt();
        System.out.println("interrupt is " + t.isInterrupted()); //還沒有睡眠
    }
}

執(zhí)行結(jié)果:
interrupt is true(還沒有睡眠打斷的是正常的線程)
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.lily.base.SleepTest.lambdamain0(SleepTest.java:9)
at java.lang.Thread.run(Thread.java:748)


打斷正常的線程

打斷正常運行的線程,不會清空打斷標記

public class InterruptTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            while (true) {

            }
        });

        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.interrupt();
    }
}

調(diào)用interrupt()方法打斷仍然繼續(xù)運行,但是此時 打斷標記為真,可根據(jù)該字段結(jié)束線程

public class InterruptTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            while (true) {
                if (Thread.currentThread().isInterrupted()){
                    break;
                }
            }
        });

        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.interrupt();
    }
}

由此發(fā)現(xiàn)打斷線程可以實現(xiàn)停止線程。

打斷park線程

打斷park 線程,不會清除打斷標記

public class ParkInterruptTest {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("park...");
            LockSupport.park();
            System.out.println("unpark...");
            System.out.println("打斷狀態(tài) " + Thread.currentThread().isInterrupted());
        });

        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

執(zhí)行結(jié)果:
park...
unpark...
打斷狀態(tài) true


如果打斷標記已經(jīng)是true ,則park 會失效。

public class ParkInterruptTest {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("park...");
            LockSupport.park();
            System.out.println("unpark...");
            System.out.println("打斷狀態(tài) " + Thread.currentThread().isInterrupted());

            LockSupport.park();
            System.out.println("unpark 1...");
        });

        t.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

執(zhí)行結(jié)果:
park...
unpark...
打斷狀態(tài) true
unpark 1...


2.5 不推薦方法

有一些不推薦使用的方法,方法已過時,容易破壞同步代碼塊,造成死鎖

方法名 static 功能說明
stop() 停止線程運行
suspend() 掛起(暫停)線程運行
resume 恢復線程運行

3. 主線程和守護線程

默認情況下,java進程需要等待所有 線程都運行結(jié)束,才會結(jié)束。有一種特殊的線程叫守護線程。只要其他非守護線程運行結(jié)束了,即使守護線程的代碼沒有執(zhí)行完,也會強制結(jié)束

public class DeamonTest {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " start");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " end");
        });

        t.setDaemon(true);
        t.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " end");
    }
}

執(zhí)行結(jié)果:
Thread-0 start
main end


注意:垃圾回收器就是一種守護線程

4. 線程五種狀態(tài)

從操作系統(tǒng)層面描述


線程五種狀態(tài).jpg
  • 初始狀態(tài):僅是在語言層面創(chuàng)建了線程對象,還未與操作系統(tǒng)線程關(guān)聯(lián)
  • 可運行狀態(tài):指該線程已經(jīng)被創(chuàng)建(與操作系統(tǒng)線程關(guān)聯(lián)),可以由cpu調(diào)度執(zhí)行
  • 運行狀態(tài):指獲取了CPU時間片運行中的狀態(tài)
    • 當CPU時間片用完,會從運行狀態(tài)轉(zhuǎn)換至可運行狀態(tài),會導致線程上下文切換
  • 阻塞狀態(tài)
    • 如果調(diào)用了阻塞API,如BIO讀寫文件,這時該線程實際不會用到CPU,會導致線程上下文切換,進入阻塞狀態(tài)
    • 等BIO操作完,會由操作系統(tǒng)喚醒阻塞的線程,轉(zhuǎn)換至可運行狀態(tài)
  • 終止狀態(tài):表示線程已經(jīng)執(zhí)行完畢,生命周期已經(jīng)結(jié)束,不會再轉(zhuǎn)換為其他狀態(tài)

5. 線程六種狀態(tài)

這時從JAVA api層面描述的
根據(jù)Thread.State 枚舉,分為六種狀態(tài)


線程狀態(tài)轉(zhuǎn)換.jpg

源碼:

public enum State {
    NEW,
   
   /**
    * 可運行狀態(tài)。線程對象調(diào)用start()方法
    * 如果處于Runnable的線程調(diào)用yield()讓出JVM資源,那么就會進入New狀態(tài)和其他New狀態(tài) 
    * 線程進行競爭重新進入Runnable
    */
    RUNNABLE,

   /**
    * 阻塞狀態(tài)。阻塞線程不釋放當前占有的系統(tǒng)資源,
    * 當sleep()結(jié)束或者join()等待其他線程來到,當前線程進入Runnable狀態(tài)等待JVM分配資 
    * 源。
    * 線程對象調(diào)用sleep()或者join()方法
    */
    BLOCKED,

   /**
    * 等待狀態(tài)。
    *
    *  {@link Object#wait() Object.wait} with no timeout
    *  {@link #join() Thread.join} with no timeout
    *  {@link LockSupport#park() LockSupport.park}
    * 
    *一個線程已經(jīng)調(diào)用了Object.wait(),會等待另一個線程調(diào)用Object.notify()或Object.notify()
    *一個已經(jīng)調(diào)用Thread.join()的線程將等待這個指定線程終止
    */
    WAITING,

   /**
    *  定時等待狀態(tài):
    *  當線程進入Runnable狀態(tài),但還沒有開始運行的時候,此時發(fā)現(xiàn)需要的資源處于同步狀態(tài) 
    *  synchronized,這個時候線程將會進入Timed_waiting,JVM會使用隊列對這些線程進行控 
    *  制,是這樣狀態(tài)的線程會先得到JVM資源進行執(zhí)行進入Waiting
    *   {@link #sleep Thread.sleep}
    *   {@link Object#wait(long) Object.wait} with timeout
    *   {@link #join(long) Thread.join} with timeout
    *   {@link LockSupport#parkNanos LockSupport.parkNanos}
    *   {@link LockSupport#parkUntil LockSupport.parkUntil}
    * 
    */
    TIMED_WAITING,

    TERMINATED;
    }
  • NEW : 線程剛被創(chuàng)建,但是還沒有調(diào)用start()方法
  • RUNNABLE: 當調(diào)用了start() 方法之后,注意,java API層面的RUNNABLE 狀態(tài)涵蓋了操作系統(tǒng)層面的 可運行狀態(tài)、運行狀態(tài)阻塞狀態(tài)(由于BIO導致的線程阻塞,在Java中無法區(qū)分,仍然認為是可運行的)
  • BLOCKED : WAITING TIMED_WAITING 都是java API層面對阻塞狀態(tài)的細分
  • TERMINATED : 當線程代碼運行結(jié)束

六種狀態(tài)演示

public class StateTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            System.out.println("running");
        },"t1");
        Thread t2 = new Thread(()->{
            while(true){

            }
        },"t2");
        t2.start();

        Thread t3 = new Thread(()->{
            System.out.println("running");
        },"t3");
        t3.start();

        Thread t4 = new Thread(()->{
            synchronized (StateTest.class){
                try{
                    Thread.sleep(100000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"t4");
        t4.start();

        Thread t5 = new Thread(()->{
            try{
                t2.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }

        },"t5");

        t5.start();

        Thread t6 = new Thread(()->{
            synchronized (StateTest.class){
                try{
                    Thread.sleep(100000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }

        },"t6");

        t6.start();

        try{
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("t1 state is " + t1.getState());
        System.out.println("t2 state is " + t2.getState());
        System.out.println("t3 state is " + t3.getState());
        System.out.println("t4 state is " + t4.getState());
        System.out.println("t5 state is " + t5.getState());
        System.out.println("t6 state is " + t6.getState());
    }
}

執(zhí)行結(jié)果:
running
t1 state is NEW
t2 state is RUNNABLE
t3 state is TERMINATED
t4 state is TIMED_WAITING
t5 state is WAITING
t6 state is BLOCKED


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

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