Java基礎-多線程

多線程

Java基礎的第五篇,也是最後一篇-多線程

1.線程的創建和啓動

通過集成Thread類創建線程類

  1. 定義Thread類的子類,並重寫run()方法
  2. 創建Tread子類的實例
  3. 調用start()方法啓動線程

舉個例子:

public class FirstThread extends Thread {

    private int i;
    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());
            if (i == 20)
            {
                // 創建並啓動第一個線程
                new FirstThread().start();
                // 創建並啓動第二個線程
                new FirstThread().start();
            }
        }
    }
}

運行結果

img

可以看到一共有三個線程:main Thread0 Thread1 後面兩個是新建的。main是程序執行後創建的。

實現Runnable接口創建線程類

  1. 定義Runnable接口的實現類,並重寫run()方法
  2. 創建Runnable實現類的實例
  3. 調用start()方法啓動線程

舉個例子:

public class SecondThread 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)
            {
                SecondThread st = new SecondThread();
                new Thread(st,"新線程1").start();
                new Thread(st,"新線程2").start();
            }
        }
    }
}

運行結果和FirstThread類似,就不詳細描述了。

這裏有一點區別:FirstThread裏面新建Thread是可以直接調用start()方法,因爲是Tread的子類,但是Runnable裏面只是線程對象的target,不能直接調用runnable.start()

使用Callable和Future創建線程

  1. 創建Callable接口實現類,並實現call()方法
  2. 創建Callable實例使用FutureTask包裝
  3. 使用FutureTask對象作爲Thread對象的target創建並啓動線程
  4. 調用FutureTask的get()方法獲得返回值

舉個例子:

public class ThirdThread implements Callable<Integer> {

    @Override
    public Integer call(){

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

    public static void main(String[] args) {

        ThirdThread rt = new ThirdThread();

        FutureTask<Integer> task = new FutureTask<>(rt);

        for (int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 20){
                new Thread(task,"有返回值的線程").start();
            }
        }
        try {
            System.out.println("子線程返回值:" + task.get());
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
}

運行結果和前面的類似,不過最後會輸出call()方法的返回值

說完了Thread,Runnable,Callable三種創建線程的方式,我們來比較一下

採用Runnable、Callable接口的方式:

  • 線程只是實現接口,還可以繼承其他類
  • 多個線程可以共享一個target對象,適合多個相同線程處理同一份資源的情況
  • 劣勢:需要使用Thread.currentThread()方法訪問當前進程

採用Thread的優勢正好是上面兩種方法的劣勢。

2.線程的生命週期

新建和就緒狀態

使用new關鍵字創建對象就處於新建狀態,使用start()方法之後就處於就緒狀態,至於什麼時候開始執行,要看JVM的調度。

運行和阻塞狀態

調用了sleep()方法,調用了一個阻塞式IO方法,等待某個通知…都會讓線程阻塞

相對應的就是運行狀態,這一塊知識點有點像操作系統的CPU輪換。

線程死亡

  • run()或call()方法執行完成,線程正常結束
  • 線程拋出未捕獲的異常
  • 直接調用stop()

這三種情況都會讓線程結束

3.線程同步

線程安全問題

在這裏我們可以用一個經典的問題-銀行取錢問題,來進行講解。

  1. 用戶輸入賬戶密碼,系統判斷是否正確
  2. 用戶輸入取款金額
  3. 系統判斷餘額是否大於取款金額
  4. 大於則取款成功,小於則取款失敗

首先定義Account類,具有賬戶名和餘額兩個屬性

public class Account {

    private String accountNo;

    private double balance;

    public Account(){}

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

    public int hashCode(){

        return accountNo.hashCode();

    }

    public String getAccountNo() {
        return accountNo;
    }

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

    public double getBalance() {
        return balance;
    }

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

    public boolean equals(Object obj){
        if (this == obj){
            return true;
        }
        if (obj != null && obj.getClass() == Account.class){
            Account target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }
        return false;
    }
}

然後定義一個取錢的線程類

public class DrawThread extends Thread{

    private Account account;

    private double drawAmount;

    public DrawThread(String name, Account account, double drawAmount){
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    public void run(){
        if (account.getBalance() >= drawAmount){
            System.out.println("取錢成功:" + drawAmount);
            account.setBalance(account.getBalance() - drawAmount);
            System.out.println("餘額爲:" + account.getBalance());
        }else{
            System.out.println(getName() + "取錢失敗,餘額不足");
        }
    }
}

最後還有主程序:

public class DrawTest {
    public static void main(String[] args) {
        Account acct = new Account("1234567",1000);
        new DrawThread("甲",acct,800).start();
        new DrawThread("乙",acct,800).start();
    }
}

啓動兩個子線程取錢,會出現什麼結果呢?

img

這種結果明顯是不對的,這就是我們上面所說的線程同步問題。

之所以出現這樣的結果,是因爲run()方法不具有同步安全性,一旦程序併發修改Account對象,就很容易出現這種錯誤結果。

爲了解決這個問題,Java多線程引入了同步監視器。語法如下:

synchronized(obj) 
{ 
    // 同步代碼塊
} 

我們再修改一下DrawThread的代碼:

public void run(){
    // 使用account作爲同步監視器,任何進程進入以下同步代碼塊之前
    // 必須先獲得對account賬戶的鎖定- 其他縣城無法獲得鎖,也就無法修改它
    // 這種做法符合 加鎖-修改-釋放 的邏輯
    synchronized (account) {
        if (account.getBalance() >= drawAmount) {
            System.out.println("取錢成功:" + drawAmount);
            account.setBalance(account.getBalance() - drawAmount);
            System.out.println("餘額爲:" + account.getBalance());
        } else {
            System.out.println(getName() + "取錢失敗,餘額不足");
        }
    }
}

再次運行就能得到正確結果:

img

同步鎖(Lock)

Java 5開始,Java提供另一個線程同步機制-通過顯示定義同步鎖對象實現同步。

通常使用格式如下:

class x 
{ 
    public void m(){
        lock.lock(); // 加鎖
        try{
            // 需要線程安全的代碼
        }finally{
            lock.unlock();
        }
    }
} 

通過lock和unlock來顯示加鎖,釋放鎖。

除了上面所說的知識點,還有線程池,死鎖,線程通信等,由於這些知識點都屬於高級Java特性,我會在後面的進階篇再進行總結。

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