Java 多線程 詳解

前言:

併發和並行

併發指的是在同一時刻只能有一個進程運行,但多個進程可以來回迅速切換,使得在宏觀上具有多個進程同時運行的效果,而並行是同一時刻有多條指令在多個處理器上同時執行。

線程和進程

線程也叫輕量級進程,線程是進程的組成部分,一個進程可以擁有多個線程,一個線程必須有一個父進程,線程共享父進程的所有資源
線程是獨立運行的,不知道有其他線程運行,執行是搶佔式的。
線程的調度和管理由進程本身完成

多線程編程的優點:

  1. 進程之間不能共享內存,但線程之間共享內存很容易
  2. 系統創建進程時要爲進程重新分配資源,但創建線程要容易的多,代價小得多,因此使用多線程來實現任務併發要比多進程效率高
  3. Java內置了多線程功能支持,簡化了編程

什麼是線程安全

當多個線程訪問某個方法時,不管你通過怎樣的調用方式、或者說這些線程如何交替地執行,我們在主程序中不需要去做任何的同步,這個類的結果行爲都是我們設想的正確行爲,那麼我們就可以說這個類是線程安全的。

線程的創建和啓動

繼承Thread的線程創建:

一、 繼承並定義Thread的子類,重寫該類的run()方法,run就是該線程要執行的內容
二、創建子類的實例方法
三、調用線程對象的start()方法來啓動線程

package com.company;

public class FirstThread extends Thread {
    private int i ;

    @Override
    public void run() {

        for(;i<100;i++){
            System.out.println(getName()+" "+ i);
        }
    }

    public static void main(String[] args) {

        for(int i = 0;i < 100;i ++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i == 20){

               new FirstThread().start();
               new FirstThread().start();


            }
        }
    }
}

main 24
Thread-1 1
Thread-0 1
Thread-1 2
main 25
Thread-1 3
Thread-0 2
Thread-1 4
main 26
Thread-1 5
Thread-0 3
Thread-1 6
main 27

線程運行時除了用戶自定義的線程還有main的默認主線程
可以看到兩個線程的值是不連續的各走各的,因爲使用繼承Thread類的方法來創建線程時,多個線程之間無法共享線程類的實例變量。

實現Runnable接口來創建線程類

一、定義Runnable接口的實現類,重寫該接口的run()方法,該方法是該線程的方法執行體
二、創建接口實現類的實例,並以此實例作爲Thread的target來創建Thread對象(真正的線程對象)利用有參構造器傳入,也可在構造器中傳入該線程的自定義名字

package com.company;

public class FirstThread implements Runnable {
    private int i ;

    @Override
    public void run() {

        for(;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+ i);
        }
    }

    public static void main(String[] args) {

        for(int i = 0;i < 100;i ++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i == 20){
                FirstThread fs =new FirstThread();
               new Thread(fs,"the first thread").start();
               new Thread(fs,"the second thread").start();


            }
        }
    }
}

C:\java\jdk\bin\java.exe "-javaagent:E:\IntelliJ IDEA 2019.2\lib\idea_rt.jar=63578:E:\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=GBK -classpath C:\java\jdk\jre\lib\charsets.jar;C:\java\jdk\jre\lib\deploy.jar;C:\java\jdk\jre\lib\ext\access-bridge-64.jar;C:\java\jdk\jre\lib\ext\cldrdata.jar;C:\java\jdk\jre\lib\ext\dnsns.jar;C:\java\jdk\jre\lib\ext\jaccess.jar;C:\java\jdk\jre\lib\ext\jfxrt.jar;C:\java\jdk\jre\lib\ext\localedata.jar;C:\java\jdk\jre\lib\ext\nashorn.jar;C:\java\jdk\jre\lib\ext\sunec.jar;C:\java\jdk\jre\lib\ext\sunjce_provider.jar;C:\java\jdk\jre\lib\ext\sunmscapi.jar;C:\java\jdk\jre\lib\ext\sunpkcs11.jar;C:\java\jdk\jre\lib\ext\zipfs.jar;C:\java\jdk\jre\lib\javaws.jar;C:\java\jdk\jre\lib\jce.jar;C:\java\jdk\jre\lib\jfr.jar;C:\java\jdk\jre\lib\jfxswt.jar;C:\java\jdk\jre\lib\jsse.jar;C:\java\jdk\jre\lib\management-agent.jar;C:\java\jdk\jre\lib\plugin.jar;C:\java\jdk\jre\lib\resources.jar;C:\java\jdk\jre\lib\rt.jar;H:\ideaproject\temp1\out\production\temp1;H:\ideaproject\temp1\lib\HdfsBrowser-20171108.jar com.company.FirstThread
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
main 21
main 22
main 23
main 24
the second thread 0
the first thread 0
main 25
the first thread 2
the second thread 1
the second thread 4
the second thread 5
the second thread 6
the second thread 7
the second thread 8
the second thread 9
the first thread 3
main 26
the first thread 11
the second thread 10
the second thread 13
the first thread 12
main 27
main 28
main 29
main 30
main 31
main 32
the first thread 15
.......

由運行結果可以發現兩個線程由於是公用一個實例化對象fs,所以值是連續的,不是各走各的,說明採用Runnable接口的方式創建的多個線程可以共享線程類的實例屬性,因爲多個線程共享同一個target。

使用Callable和Future創建線程

從Java5開始,提供了Callable接口,是Runnable接口的升級版,他的call()方法比 Runnable的 run()方法強大的多
callable

@see Executor
 * @since 1.5
 * @author Doug Lea
 * @param <V> the result type of method {@code call}
 */
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

可以看出
call 方法可以有返回值。
call 方法可以拋出異常。

Runnable

* @see     java.lang.Thread
 * @see     java.util.concurrent.Callable
 * @since   JDK1.0
 */
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

創建過程:

一、實現Callable接口,實現call()方法(線程執行體),該call()方法有返回值。
二、創建 Callable實現類的實例,並拿FutureTask包裝
三、將Futuretask對象作爲Thread的target創建並啓用線程
四、調用FutureTask對象的gat()獲取 線程結束後的返回值。

注意:

運行Callable任務可以拿到一個Future對象,Future 表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並獲取計算的結果。計算完成後只能使用 get 方法來獲取結果,如果線程沒有執行完,Future.get()方法可能會阻塞當前線程的執行;如果線程出現異常,Future.get()會throws InterruptedException或者ExecutionException;如果線程已經取消,會跑出CancellationException。取消由cancel 方法來執行。isDone確定任務是正常完成還是被取消了。一旦計算完成,就不能再取消計算。如果爲了可取消性而使用 Future 但又不提供可用的結果,則可以聲明Future<?> 形式類型、並返回 null 作爲底層任務的結果。Future接口的定義如下:

Future模式
Future模式在請求發生時,會先產生一個Future憑證給發出請求的客戶,它的作用就像是Proxy物件,
同時,由一個新的執行線程持續進行目標物件的生成(Thread-Per-Message),真正的目標物件生
成之後,將之設定至Future之中,而當客戶端真正需要目標物件時,目標物件也已經準備好,可以讓
客戶提取使用。結合JDK的Future來看,就是你run線程後,你可以把線程的返回值賦給Future並返回
一個Future對象。這時你可以立	即拿到這個對象,然後進行下面的邏輯。但是如果你要get這個Future
中的線程結果,就會被阻塞直到線程結束。就相當於現在的期房,你把手續和錢都交上去了,就可以
馬上拿到合同,但只有合同沒有房子。這個時候你已經是有房	一族了,你可以先去買家電買裝修
(走下面的其他邏輯)。但是你要把家電和裝修放進去,就必須等到房子完工(阻塞)。



		[這裏引用了:]https://blog.csdn.net/HEYUTAO007/article/details/19072675

說白了就是Callable的實例對象必須要有一個返回值,而這個返回值不能馬上得到,要等線程方法call()跑完了才能得到,不能直接拿Thread的target去盛,這裏就用Future去轉接一下,Future就是一個類似於花唄系統的東西會get出來你未來要還的東西,然後先把錢給你花。。。

package com.company;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class SecondThread implements Callable<Integer> {
    int i;
    @Override
    public Integer call() throws Exception {
        for(;i < 100;i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
        return i;
    }

    public static void main(String[] args) {
        SecondThread sc =new SecondThread();
        FutureTask<Integer> future = new FutureTask<Integer>(sc);
        for(int i = 0;i < 100;i ++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i == 20){
                new Thread(future,"帶返回值的方法").start();

                try {
                    System.out.println("子線程的返回值"+future.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

比較三種線程創建方法:

Runnable 和 Callable都是接口實現,可以繼承其他類
這兩種方式下 多個線程可以共享一個target對象
Callable可以返回線程運行後的結果和自己拋出異常,Runnable只能內部消化

線程的生命週期

線程一旦啓動,不會立馬進入執行狀態,也不是一直處於執行狀態。

線程的五種狀態

新建 
就緒
運行
阻塞
死亡

新建和運行線程

new 一個Thread 創建線程,線程就新建好了,如果讓線程運行則需要啓用start()方法

start和run方法的不同:

run只是一個線程執行的函數,線程的啓動則應該用start而不是run,如果用了run,則只不過是運行線程的執行體,單線程執行不會併發的執行,只是簡單的順序的程序。
注意: 只能對處於新建狀態的線程調用start()方法

運行和阻塞狀態

阻塞:

  1. 線程調用了sleep()方法主動放棄了所佔用的處理器資源
  2. 線程調用了一個阻塞式IO方法,在該方法返回前,該線程被阻塞

死亡

isAlive()判斷線程是否存在,
新建和死亡時返回false
阻塞和運行時返回true

線程的控制

join方法

join是一個讓線程等線程的方法,當某個線程執行流中調用其他線程的join()方法,該線程會被阻塞,直到被join的方法的join線程執行完畢。

public class FirstThread extends Thread {
    class SecondThread extends Thread{
        @Override
        public void run() {
            for(int i = 0;i<200;i++){
                System.out.println("我是第二個join");
            }
        }
    }
    private int i;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"啓動了");
        for(int i=0;i<100;i++){
            System.out.println(getName()+"他的i = "+i);
            if(i==20){

                try {
                    SecondThread fs2 = new SecondThread();
                    fs2.start();
                    fs2.join();

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

Thread-0他的i = 19
Thread-0他的i = 20
我是第二個join
我是第二個join
我是第二個join
我是第二個join
我是第二個join
我是第二個join
我是第二個join
我是第二個join
我是第二個join
我是第二個join
Thread-0他的i = 21

後臺線程

將一個線程設爲後臺線程後他會成爲守護線程,直到所有線程都結束,他才能結束
調用Thread的setDaemon(true)方法可將指定線程設爲後臺線程

Sleep

讓當前的線程暫停一會並進入阻塞狀態,可以通過調用Thread的靜態sleep()來實現。
sleep的方法有兩種重載形式。
沒啥用,關於時間精度問題。

線程讓步 :yield

yield:v.生產;獲利;屈服;彎下去

n.投資收益;生產量
他是Thread類提供的一個靜態方法,通過它,可以暫停線程,讓線程由運行狀態轉入到就緒狀態,並不會使該線程阻塞。
yield()只是讓系統的線程調度器對該線程重新調度了一下,
如果a和b和其他線程一起運行,在某個時刻,a調用了yield()a由運行轉爲了就緒,接下來a可能繼續霸佔資源立馬又繼續運行,也可能把運行的資源讓給比他高優先級的線程運行,注意這裏說的都是可能,
舉個例子:一幫朋友在排隊上公交車,輪到Yield的時候,他突然說:我不想先上去了,咱們大家來競賽上公交車。然後所有人就一塊衝向公交車,

有可能是其他人先上車了,也有可能是Yield先上車了。

但是線程是有優先級的,優先級越高的人,就一定能第一個上車嗎?這是不一定的,優先級高的人僅僅只是第一個上車的概率大了一點而已,

最終第一個上車的,也有可能是優先級最低的人。並且所謂的優先級執行,是在大量執行次數中才能體現出來的。

sleep 和yield 的區別

  1. sleep 暫停當前線程後會直接讓給別的線程不會理會其他線程的優先級,但是yield方法只會給優先級和自己相同或者優先級高於自己的線程
  2. sleep()方法暫停線程後會進入阻塞,經過一段時間的阻塞纔會轉入就緒狀態,而yield()方法直接強制線程進入就緒狀態,很有可能,一個調用yield的線程經過短暫的暫停後直接又被處理器執行
  3. sleep()方法拋出InrerruptedException異常,而yield方法沒有拋出任何異常的能力
  4. sleep()比yield()有更好的移植性,所以一般情況下使用sleep();

改變線程的優先級

高優先級的線程會被高概率執行,低優先級的線程會被低概率執行
父線程的優先級和他的子線程的優先級是一樣的,
Thread類提供了setPriority 和getPriority Priority的優先級有十級,但是不同的操作系統有不同的優先級,所以用下面三個常量來設置優先級
對優先級進行操作,
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5

線程同步

線程安全問題

銀行取錢問題:
如果不考慮線程安全問題,實現一個取錢的業務邏輯並且運行多次:
建立用戶類:建立用戶id號,和餘額字段

package com.company.bank;

import java.util.Objects;

public class Account {
    public int getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(int accountNo) {
        this.accountNo = accountNo;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    private int accountNo;
    private int balance;

    public Account(int accountNo, int balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return accountNo == account.accountNo &&
                balance == account.balance;
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountNo, balance);
    }
}

建立取錢業務:

package com.company.bank;

public class DrawThread extends Thread{
    private String name;
    private int drawAccount;
    private Account account;
    public DrawThread(String name,int drawAccount,Account account){
        this.name = name;
        this.account = account;
        this.drawAccount = drawAccount;
    }

    @Override
    public void run() {
        super.run();
            if (drawAccount <= account.getBalance()) {
                account.setBalance(account.getBalance() - drawAccount);
                System.out.println(name + "取錢成功---賬戶餘額=" + account.getBalance());
            } else {
                System.out.println(name + "取錢失敗,餘額不足");
            }

    }
}

運行:

public class Test {
    public static void main(String[] args) {
        Account account = new Account(1,1000);
        new DrawThread("A",800,account).start();
        new DrawThread("B",100,account).start();
        //System.out.println("餘額爲"+account.getBalance());

    }
}

失敗案例:

B取錢成功---賬戶餘額=200
A取錢成功---賬戶餘額=200

Process finished with exit code 0

很明顯是錯的。

同步代碼塊

爲了解決兩個併發的線程對同一對象資源進行改寫所帶來的的錯誤,java多線程支持引入同步監視器來解決這種問題。
關於sychornized關鍵字的詳解,參見synchronized底層如何實現及應用

sychronized(account){
		...
}

同步方法

用 sychronized關鍵字修飾某個方法,該方法稱爲同步方法,無須指定同步監視器,監視器就是他自己this的對象本身。

線程安全的類有以下特徵:

  • 該類的對像可被多個線程安全的訪問
  • 每個線程調用該對象的任意方法都會得到正確的結果
  • 每個線程調用該類的方法後,該對象狀態依然保持合理的狀態

不可變的類總是線程安全的,而可變的類會有線程不安全

可變類的線程安全是以犧牲程序運行效率爲代價的,爲了減少程序線程安全所帶來的負面影響,可以採用以下措施

  • 不要對線程安全類的所有方法都進行同步,只對改變競爭資源的方法進行同步,
  • 如果可變類有兩種運行環境:單線程和多線程,應該爲可變類提供兩種版本,線程呢個安全版本和不安全版本,讓不安全版本在單線程環境中保持良好的性能,例如:StringBuilder,StringBuffer單線程下應該用StringBuilder來保證性能,多線程下用StringBuffer保持同步和線程安全

釋放同步監視器的鎖定

程序無法顯式地釋放對同步監視器的鎖定,在以下情況下會自動釋放

  • 當前同步代碼塊運行結束
  • 線程在同步代碼塊同步方法中遇到break,return 終止了代碼塊,方法的執行
  • 程序執行了同步監視器的wait()方法,當前線程暫停

以下情況情況,線程不會釋放同步監視器

  • 調用sleep(),yield()
  • suspend()掛起線程

同步鎖

臨界區

當兩個線程競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。導致競態條件發生的代碼區稱作臨界區。在臨界區中使用適當的同步就可以避免競態條件。
界區實現方法有兩種,一種是用synchronized,一種是用Lock顯式鎖實現。

有臨界區是爲了讓更多的其它線程能安全夠訪問資源。
說白了:臨界區就是修改對象狀態標記的代碼區

一、Lock和synchronized有以下幾點不同:

1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現,synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將 unLock()放到finally{} 中;

2)synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

3)Lock可以讓等待鎖的線程響應中斷,線程可以中斷去幹別的事務,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;

4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

5)Lock可以提高多個線程進行讀操作的效率。

在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。

舉個例子:當有多個線程讀寫文件時,讀操作和寫操作會發生衝突現象,寫操作和寫操作會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。

但是採用synchronized關鍵字來實現同步的話,就會導致一個問題:

如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。

因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發生衝突,通過Lock就可以辦到。

另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的

二、ReentrantLock獲取鎖定與三種方式:
  a) lock(), 如果獲取了鎖立即返回,如果別的線程持有鎖,當前線程則一直處於休眠狀態,直到獲取鎖
  b) tryLock(), 如果獲取了鎖立即返回true,如果別的線程正持有鎖,立即返回false;
  c)tryLock(long timeout,TimeUnit unit), 如果獲取了鎖定立即返回true,如果別的線程正持有鎖,會等待參數給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false;
  d) lockInterruptibly:如果獲取了鎖定立即返回,如果沒有獲取鎖定,當前線程處於休眠狀態,直到或者鎖定,或者當前線程被別的線程中斷

package com.company.bank2;

import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
    private final Lock lock = new ReentrantLock();
    public int getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(int accountNo) {
        this.accountNo = accountNo;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    private int accountNo;
    private int balance;

    public Account(int accountNo, int balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return accountNo == account.accountNo &&
                balance == account.balance;
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountNo, balance);
    }
    public void draw (int drawAccount){
        lock.lock();

        if (drawAccount <= this.getBalance()) {
            this.setBalance(this.getBalance() - drawAccount);
            System.out.println(Thread.currentThread().getName() + "取錢成功---賬戶餘額=" + this.getBalance());
        } else {
            System.out.println(Thread.currentThread().getName() + "取錢失敗,餘額不足");
        }
        lock.unlock();
    }

}

package com.company.bank2;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DrawThread extends Thread{
    Account account;
    int Balance;

    public DrawThread(Account account, int balance) {
        this.account = account;
        Balance = balance;
    }
    void run1(){
        account.draw(this.Balance);
    }
}

package com.company.bank2;

public class Test {
    public static void main(String[] args) {
        Account account = new Account(1,1000);
        new Thread(){
            @Override
            public void run() {
                new DrawThread(account,800).run1();
            }

        }.start();
        new Thread(){
            @Override
            public void run() {
                new DrawThread(account,800).run1();
            }
        }.start();

        //System.out.println("餘額爲"+account.getBalance());

    }
}

死鎖

創建兩個字符串a和b,再創建兩個線程A和B,讓每個線程都用synchronized鎖住字符串(A先鎖a,再去鎖b;B先鎖b,再鎖a),如果A鎖住a,B鎖住b,A就沒辦法鎖住b,B也沒辦法鎖住a,這時就陷入了死鎖。

package com.company.bank;


public class DeadLock {
        public static String obj1 = "obj1";
        public static String obj2 = "obj2";
        public static void main(String[] args){
            Thread a = new Thread(new Lock1());
            Thread b = new Thread(new Lock2());
            a.start();
            b.start();
        }
    }
    class Lock1 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock1 running");
                while(true){
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock1 lock obj1");
                        Thread.sleep(3000);//獲取obj1後先等一會兒,讓Lock2有足夠的時間鎖住obj2
                        synchronized(DeadLock.obj2){
                            System.out.println("Lock1 lock obj2");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    class Lock2 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock2 running");
                while(true){
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock2 lock obj2");
                        Thread.sleep(3000);
                        synchronized(DeadLock.obj1){
                            System.out.println("Lock2 lock obj1");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }

Lock1 running
Lock1 lock obj1
Lock2 running
Lock2 lock obj2

Process finished with exit code -1

線程通信

一、用while輪序的手段,比較低端,效率不高:

package com.company.bank;

import java.util.ArrayList;
import java.util.List;

public class MyList {
    private volatile List<String> list = new ArrayList<String>();
    public void add(){
        list.add("abc");
    }
    public int size(){
        return list.size();
    }
    static class ThreadA implements Runnable{
        private MyList list;
        public ThreadA(MyList list){
            this.list = list;

        }
        @Override
        public void run() {
            for(int i = 0;i < 3;i ++) {
                System.out.println(Thread.currentThread().getName() + "正在執行。。");
                list.add();
                System.out.println("添加了" + (i + 1) + "個元素");

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    static class ThreadB implements Runnable{
        private MyList list ;

        public ThreadB(MyList list) {
            this.list = list;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "正在執行。。");
                    if (list.size() == 2) {
                        System.out.println("線程B要退出了?");

                        throw new InterruptedException();

                        //throw new InterruptedException();
                    }
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        MyList list = new MyList();
        ThreadA a = new ThreadA(list);
        ThreadB b = new ThreadB(list);
        new Thread(a).start();
        new Thread(b).start();
    }

}

結果:

Thread-1正在執行。。
Thread-1正在執行。。
Thread-1正在執行。。
Thread-0正在執行。。
添加了2個元素
Thread-1正在執行。。
線程B要退出了?
java.lang.InterruptedException
	at com.company.bank.MyList$ThreadB.run(MyList.java:50)
	at java.lang.Thread.run(Thread.java:745)
Thread-0正在執行。。
添加了3個元素

二、用wait\notify機制

wait/notify 機制
在這之前,線程間通過共享數據來實現通信,即多個線程主動地讀取一個共享數據,通過 同步互斥訪問機制保證線程的安全性。等待/通知機制主要由Object類中的wait()、notify() 和 notifyAll()三個方法來實現,這三個方法均非Thread類中所聲明的方法,而是Object類中聲明的方法。原因是每個對象都擁有monitor(鎖),所以讓當前線程等待某個對象的鎖,當然應該通過這個對象來操作,而不是用當前線程來操作,因爲當前線程可能會等待多個線程的鎖,如果通過線程來操作,就非常複雜了。

1.wait()——讓當前線程 (Thread.concurrentThread() 方法所返回的線程) 釋放對象鎖並進入等待(阻塞)狀態。
2.notify()——喚醒一個正在等待相應對象鎖的線程,使其進入就緒隊列,以便在當前線程釋放鎖後競爭鎖,進而得到CPU的執行。
3.notifyAll()——喚醒所有正在等待相應對象鎖的線程,使它們進入就緒隊列,以便在當前線程釋放鎖後競爭鎖,進而得到CPU的執行。
從以上描述可以得出:wait()、notify() 和 notifyAll()方法是 本地方法,並且爲 final 方法,無法被重寫;調用某個對象的 wait() 方法能讓 當前線程阻塞,並且當前線程必須擁有此對象的monitor(即鎖);調用某個對象的 notify() 方法能夠喚醒 一個正在等待這個對象的monitor的線程,如果有多個線程都在等待這個對象的monitor,則只能喚醒其中一個線程;調用notifyAll()方法能夠喚醒所有正在等待這個對象的monitor的線程。

package com.company;

public class Comunicate {
    static Object object = new Object();

    public static void main(String[] args) {
        ThreadA  a = new ThreadA();
        ThreadB b = new ThreadB();
        a.start();
        b.start();
        //System.out.println("是否停止1?="+a.interrupted());
        //if(Thread.currentThread().isInterrupted()) System.out.println(Thread.currentThread().getName()+"阻塞了");

    }

    static class ThreadA extends Thread {

        public Boolean isInterrupt(){
            return this.isInterrupted();
        }
        public void run() {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "獲得了鎖");

                try {
                    System.out.println(Thread.currentThread().getName() + "釋放該鎖進入阻塞");
                    
                    object.wait();


                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "線程結束");


            }


        }


    }
    static class ThreadB extends Thread{
        public void run(){
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"該線程獲得了鎖");
                object.notify();
                System.out.println(Thread.currentThread().getName()+"該線程喚醒了正在等待的線程");
                System.out.println("線程執行完畢");
            }
        }
    }
}

Thread-0獲得了鎖
Thread-0釋放該鎖進入阻塞
Thread-1該線程獲得了鎖
Thread-1該線程喚醒了正在等待的線程
線程執行完畢
Thread-0線程結束

1、每個對象都有一個鎖來控制同步訪問,Synchronized關鍵字可以和對象的鎖交互,來實現同步方法或同步塊。sleep()方法正在執行的線程主動讓出CPU(然後CPU就可以去執行其他任務),在sleep指定時間後CPU再回到該線程繼續往下執行(注意:sleep方法只讓出了CPU,而並不會釋放同步資源鎖!!!);wait()方法則是指當前線程讓自己暫時退讓出同步資源鎖,以便其他正在等待該資源的線程得到該資源進而運行,只有調用了notify()方法,之前調用wait()的線程纔會解除wait狀態,可以去參與競爭同步資源鎖,進而得到執行。(注意:notify的作用相當於叫醒睡着的人,而並不會給他分配任務,就是說notify只是讓之前調用wait的線程有權利重新參與線程的調度);

2、sleep()方法可以在任何地方使用;wait()方法則只能在同步方法或同步塊中使用;

3、sleep()是線程線程類(Thread)的方法,調用會暫停此線程指定的時間,但監控依然保持,不會釋放對象鎖,到時間自動恢復;wait()是Object的方法,調用會放棄對象鎖,進入等待隊列,待調用notify()/notifyAll()喚醒指定的線程或者所有線程,纔會進入鎖池,不再次獲得對象鎖纔會進入運行狀態;

三、Condition

Condition是在java 1.5中才出現的,它用來替代傳統的Object的wait()、notify()實現線程間的協作,相比使用Object的wait()、notify(),使用Condition的await()、signal()這種方式實現線程間協作更加安全和高效。因此通常來說比較推薦使用Condition,阻塞隊列實際上是使用了Condition來模擬線程間協作。

Condition是個接口,基本的方法就是await()和signal()方法;
Condition依賴於Lock接口,生成一個Condition的基本代碼是lock.newCondition()

調用Condition的await()和signal()方法,都必須在lock保護之內,就是說必須在lock.lock()和lock.unlock之間纔可以使用
Conditon中的await()對應Object的wait();

Condition中的signal()對應Object的notify();

Condition中的signalAll()對應Object的notifyAll()。
在這裏插入圖片描述

實現一個簡單的消費者生產者模型:

package com.company.bank;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Conditon1 {
    static final Lock lock = new ReentrantLock();
    static final Condition con = lock.newCondition();

    public static void main(String[] args) {
        new Consumer().start();
        new Producer().start();
    }

    static class Consumer  extends Thread{
        @Override
        public void run() {
            super.run();
            consume();
        }

        private void consume() {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"我在等一個新信號");
            try {
                con.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println(Thread.currentThread().getName()+"拿到一個新信號");
                System.out.println(Thread.currentThread().getName()+"完事了");
                lock.unlock();
            }


        }
    }
    static class Producer extends Thread{
        @Override
        public void run() {
            super.run();
            produce();
        }

        private void produce() {
            try{lock.lock();
            System.out.println(Thread.currentThread().getName()+"我拿到一個鎖");
            con.signalAll();
            System.out.println("我發出一個鎖");
        }finally {
                System.out.println(Thread.currentThread().getName()+"完事了");
                lock.unlock();
            }
        }
    }
}


Thread-0我在等一個新信號
Thread-1我拿到一個鎖
我發出一個鎖
Thread-1完事了
Thread-0拿到一個新信號
Thread-0完事了

Process finished with exit code 0

這裏的原理其實就是consumer 拿到一個鎖,然後await交出鎖,阻塞,給了producer

producer拿到鎖,進行一些操作,然後再還回來,producer線程結束,還回來的鎖又給了阻塞的consumer,consumer拿到鎖然後結束,

鎖就是一個菜單,消費者拿到然後給廚師,廚師拿到菜單做飯,然後把需要做的菜做好再還會給消費者,整個過程結束

實現一個標準的消費者生產者模型

package com.company.bank;

import java.util.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Condition2 {
    private static final Lock lock = new ReentrantLock();
    private static final Condition un_empty = lock.newCondition();
    private static final Condition un_full = lock.newCondition();
    static private Queue<Integer> queue = new LinkedList<Integer>();

    public static void main(String[] args) {
        new Consumer().start();
        new Produce().start();
    }

    static class Consumer extends Thread {
        @Override
        public void run() {
            super.run();
            while(true){
                consume();
            }

        }

        private void consume() {
            lock.lock();
            try {
                while (queue.size()==0) {
                    System.out.println("隊列空,阻塞消費線程");
                    un_empty.await();
                }
                queue.poll();
                System.out.println(Thread.currentThread().getName() + "取走一個元素" + "還剩" + queue.size());
                System.out.println("喚醒生產線程");
                un_full.signal();

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    static class Produce extends Thread {
        @Override
        public void run() {
            super.run();
            while(true) {
                produce();
            }
        }
        private void produce() {
            lock.lock();
            try {
                while (queue.size() == 10) {

                    System.out.println("隊列已滿,生產者被阻塞,等待空間");
                    un_full.await();
                }
                queue.offer(1);
                System.out.println("向隊列取中插入一個元素,隊列剩餘空間:" + (10 - queue.size()));
                un_empty.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }


}

隊列空,阻塞消費線程
向隊列取中插入一個元素,隊列剩餘空間:9
向隊列取中插入一個元素,隊列剩餘空間:8
向隊列取中插入一個元素,隊列剩餘空間:7
向隊列取中插入一個元素,隊列剩餘空間:6
向隊列取中插入一個元素,隊列剩餘空間:5
向隊列取中插入一個元素,隊列剩餘空間:4
向隊列取中插入一個元素,隊列剩餘空間:3
向隊列取中插入一個元素,隊列剩餘空間:2
向隊列取中插入一個元素,隊列剩餘空間:1
向隊列取中插入一個元素,隊列剩餘空間:0
隊列已滿,生產者被阻塞,等待空間
Thread-0取走一個元素還剩9
喚醒生產線程
Thread-0取走一個元素還剩8
喚醒生產線程
Thread-0取走一個元素還剩7
喚醒生產線程
Thread-0取走一個元素還剩6
喚醒生產線程
Thread-0取走一個元素還剩5
喚醒生產線程
Thread-0取走一個元素還剩4
喚醒生產線程
Thread-0取走一個元素還剩3
喚醒生產線程
Thread-0取走一個元素還剩2
喚醒生產線程
Thread-0取走一個元素還剩1
喚醒生產線程
Thread-0取走一個元素還剩0
喚醒生產線程
隊列空,阻塞消費線程
向隊列取中插入一個元素,隊列剩餘空間:9
向隊列取中插入一個元素,隊列剩餘空間:8
向隊列取中插入一個元素,隊列剩餘空間:7
向隊列取中插入一個元素,隊列剩餘空間:6
向隊列取中插入一個元素,隊列剩餘空間:5
向隊列取中插入一個元素,隊列剩餘空間:4

blockQueue 實現生產者消費者

package com.company;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueuetest {
    public static void main(String[] args) {
        BlockingQueue<String>blockingQueue = new ArrayBlockingQueue<String>(3);
         new BlockingQueuetest().new Producer(blockingQueue).start();
//         new BlockingQueuetest().new Producer(blockingQueue).start();
//         new BlockingQueuetest().new Producer(blockingQueue).start();
         new BlockingQueuetest().new Consumer(blockingQueue).start();
    }
    class Producer extends Thread{
        private BlockingQueue<String>blockingQueue;

        public Producer(BlockingQueue<String> blockingQueue) {
            this.blockingQueue = blockingQueue;
        }
        public void run(){
            String strArr[] = {
                    "a",
                    "b",
                    "c"
            };
            for(int i = 0;i < 999999999;i++){
                System.out.println(getName()+"生產者準備生產集合元素!");
                try {
                    Thread.sleep(1000);
                    blockingQueue.put(strArr[i%3]);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName() + "生產完成"+blockingQueue);
            }



        }
    }
    class Consumer extends Thread{
        private BlockingQueue<String>blockingQueue;

        public Consumer(BlockingQueue<String> blockingQueue) {
            this.blockingQueue = blockingQueue;
        }
        public void run(){
            while ((true)){
                System.out.println(getName()+"消費者準備消費集合元素");
                try {
                    Thread.sleep(2000);
                    blockingQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName()+"消費完成"+blockingQueue);

            }



        }
    }
}

線程組和未處理的異常

線程組

Java用ThreadGroup來表示線程組,他可以對一批線程組進行分類管理,對線程組的控制等於對這批線程的控制,
默認:如果線程A創建了線程B,線程B屬於A線程所在的線程組
,一旦確認了線程所屬的線程組就無法改變,直到線程死亡。

ThreadGroup和Thread就好比文件夾與文件的關係,Thread不能獨立於ThreadGroup存在,就像所有的文件都會有個目錄一樣。而ThreadGroup能管理着旗下的ThreadGroup和Thread,也類似於文件夾管理子文件夾和文件。

Thread提供了構造器來設置新的線程屬於哪個線程組:

  • Thread(ThreadGroup group,Runnable target)
  • Thread(ThreadGroup group,Runnable target,String name)
  • Thread(ThreadGroup group,String name)
    Thread類不提供setThreadGroup()來改變所屬線程組,只提供getThreadGroup()來返回該線程所屬的線程組

線程池

我們在用線程時,就去創建一個線程。
如果並法度很高,有些線程創建完了運行完立馬結束,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。
那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務?

在Java中可以通過線程池來達到這樣的效果。

ThreadPoolExecutor

Executor框架是一種將線程的創建和執行分離的機制。它基於Executor和ExecutorService接口,及這兩個接口的實現類ThreadPoolExecutor展開,Executor有一個內部線程池,並提供了將任務傳遞到池中線程以獲得執行的方法,可傳遞的任務有如下兩種:通過Runnable接口實現的任務和通過Callable接口實現的任務。在這兩種情況下,只需要傳遞任務到執行器,執行器即可使用線程池中的線程或新創建的線程來執行任務。執行器也決定了任務執行時間。

  • 線程池:
    • 可緩存線程池newCachedThreadPool
package com.company;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i = 0;i < 10;i++){
            final int index =  i ;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"的i="+index);
                }
            });


        }
    }
}

  • 定長線程池 newFixedThreadPool
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
    static int count;
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for(int i =0;i < 12;i++){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在運行");
                    try {
                        count++;
                        Thread.sleep(100);
                        System.out.println(count);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        System.out.println(Thread.currentThread().getName()+"運行結束");
                    }
                }
            });
            if(executorService.isTerminated()) System.out.println("全部結束");


        }
    }


}

  • 定長線程池支持定時和週期性任務newScheduledThreadPool
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        for(int i =0;i<10;i++) {
            scheduledExecutorService.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"Three seconds delay");
                }
            }, 3, TimeUnit.SECONDS);
        }
  • 單線程化線程池newSingleThreadExecutor
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for(int i = 0;i < 10;i ++){
            final int index = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+" "+index);

                }
            });
        }

這四類線程池類底層都是ThreadPoolExecutor類進行初始化的

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }


    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());//裏面使用了父類的構造函數,下面就是本類和父類的繼承關係,看看他的父類是什麼,你就明白了
    }

所以所有的線程池都是基於ThreadPoolExecutor實現的


    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
	/*
 
	corePoolSize:核心池大小,意思是當超過這個範圍的時候,就需要將新的線程放到等待隊列中了即workQueue;
 
	maximumPoolSize:線程池最大線程數量,表明線程池能創建的最大線程數
 
	keepAlivertime:當活躍線程數大於核心線程數,空閒的多餘線程最大存活時間。
 
	unit:存活時間的單位
 
	workQueue:存放任務的隊列---阻塞隊列
 
	handler:超出線程範圍(maximumPoolSize)和隊列容量的任務的處理程序
 
	
	我們執行線程時都會調用到ThreadPoolExecutor的execute()方法,現在我們來看看這個方法的源碼(就是下面這段代碼了,這裏面有一些註釋解析),我直接來解釋一下吧:在這段代碼中我們至少要看懂一個邏輯:噹噹前線程數小於核心池線程數時,只需要添加一個線程並且啓動它,如果線程數數目大於核心線程池數目,我們將任務放到workQueue中,如果連workQueue滿了,那麼就要拒絕任務了。詳細的函數我就不介紹了
 */
	
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
發佈了124 篇原創文章 · 獲贊 9 · 訪問量 2482
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章