線程

線程的概念:

一個線程是指程序中完成一個任務的執行流,java中可以在一個程序中併發地運行多個線程,這些線程可以同時在多個處理器上運行
多個線程在多個CPU上運行
在單CPU系統中,多個線程分享CPU的時間,操作系統負責CPU資源的調度和分配
多個線程在單個CPU上運行



多線程可以使程序的反應更快,交互性更強,執行效率更高。當程序作爲一個應用程序(application)運行時,jvm會爲main方法創建一個線程,當程序不再需要時,jvm就會創建一個線程來進行垃圾回收的工作。所以一個完整的應用程序最少含有兩個線程。

2. 創建線程

2.1 繼承父類Thread

  1. 將類聲明爲Thread的子類
  2. 重寫Thread類的run()方法
  3. 創建Thread的子類對象,並且調用start()方法,啓動線程

public class MyThread extends Thread {

    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        for(int i=0;i<100;i++){
            System.out.println("main-->"+i);
        }
    }
    public void run() {

        for(int i=0;i<100;i++){
            System.out.println("自定義-->"+i);
        }
    }
}

2.2 實現Runnable接口

  1. 創建類並實現Runnable接口
  2. 實現Runnable接口裏的run方法,完成自定義線程的任務代碼
  3. 實例化類
  4. 創建Thread對象,參考Thread構造方法:Thread(Runnable target)
  5. 調用start()方法,啓動線程
public class MyThread3 implements Runnable {

    public void run() {

        for(int i=1;i<100;i++){
            System.out.println(Thread.currentThread()+":"+i);
        }
    }

    public static void main(String[] args) {

        //創建類實例
        MyThread3 thread3 = new MyThread3();
        //創建Thread類的實例,把Runnable作爲實參傳遞
        Thread thread = new Thread(thread3,"線程-C");
        //調用thread的start()方法啓動線程
        thread.start();

        for(int i=1;i<100;i++){
            System.out.println(Thread.currentThread()+":"+i);
        }
    }
}

Note:因爲java是單繼承、多實現的,所以建議使用實現Runnable接口的方法來實現線程

3. 線程常見的方法

  1. 設置線程的名字
    可以使用帶參數的構造方法Thread(String name) 或者thread.setName(“myThread”)方法,爲線程命名。同樣getName()可以得到線程的名字。
  2. 睡眠線程
    注意:sleep是靜態方法,所以哪一個線程執行了含有sleep方法的代碼,哪一個線程就會睡眠

考慮以下代碼是哪一個線程睡眠了?

public class MyThread2 extends Thread {

    //自定義線程
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println(i);
        }
    }

    //主線程
    public static void main(String[] args) throws InterruptedException {
        MyThread2 thread2 = new MyThread2();
        thread2.sleep(5 * 1000);
        thread2.setName("線程-A");
        thread2.start();    
    }
}

“thread2.sleep(5 * 1000);”是在主線程中執行,所以主線程會被睡眠

public class MyThread2 extends Thread {

    //自定義線程
    public void run() {

        try {
            Thread.sleep(2 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<100;i++){
            System.out.println(i);
        }
    }

    //主線程
    public static void main(String[] args) throws InterruptedException {
        MyThread2 thread2 = new MyThread2();
        thread2.start();    
    }
}

“Thread.sleep(2 * 1000);”是在自定義線程中執行,所以自定義線程會被睡眠
3. 得到當前線程對象
靜態方法,哪個線程執行了含有currentThread()的代碼,就返回哪個線程的對象

public class MyThread2 extends Thread {

    public MyThread2(String name) {

        super(name);
    }
    //自定義線程
    public void run() {
        System.out.println("this:"+this);
        System.out.println("2:"+Thread.currentThread());
    }

    //主線程
    public static void main(String[] args) throws InterruptedException {
        MyThread2 thread2 = new MyThread2("線程-B");
        thread2.start();    
        System.out.println("1:"+thread2.currentThread());
    }
}

運行後控制檯輸出如下:


1:Thread[main,5,main]
this:Thread[線程-B,5,main]
2:Thread[線程-B,5,main]
4. 設置線程的優先級
優先級越高的線程獲取CPU資源的機率越大

//設置優先級1~10之間,數值越大優先級越高,默認的優先級爲5
thread2.setPriority(10);

優先級越高的線程不會100%優先執行,只是優先執行的概率更大而已
5. isAlive()
isAlive()是用來判斷線程的狀態,當線程處於就緒、臨時阻塞、運行狀態,則返回true;如果線程處於新建並且沒有啓動的狀態,或者線程已經執行結束,則返回false.

4. 線程的生命週期

線程生命週期

  • 新建狀態:new Thread()後,線程就進入了新建狀態
  • 調用start()方法啓動線程後,線程就會進入可運行狀態<此時線程是可以運行的,但是還沒有真正的運行,只有等待CPU爲其分配資源後線程纔開始運行>
  • 臨時阻塞狀態,當線程執行了sleep或者wait方法,就會進入臨時阻塞狀態。
  • 死亡狀態(結束狀態):如果一個線程執行完了run()方法,就進入結束狀態。

5. 線程安全問題

單一線程時,它只能在同一時間進行一項操作,所以永遠不必擔心有兩個實體同時使用相同的資源。但是進入多線程環境後,它們就不再是孤立的,可能多個線程試圖在同一時間訪問同一個資源。比如兩個線程同時從一個銀行賬戶取款!

舉一個多線程中常見的例子,三個售票窗口同時賣票,一共50張票,使用以下代碼模擬:

class SaleTicket extends Thread {

    // 設置爲靜態變量,因爲是三個線程的貢獻資源數據
    static int num = 50;

    public SaleTicket(String name) {
        super(name);
    }

    @Override
    public void run() {

        while (true) {

            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "售出了"
                        + num + "號票");
                num--;
            } else {
                System.out.println("售罄");
                break;
            }
        }
    }
}

public class Demo1 {

    public static void main(String[] args) {
        SaleTicket a = new SaleTicket("A窗口");
        SaleTicket b = new SaleTicket("B窗口");
        SaleTicket c = new SaleTicket("C窗口");
        a.start();
        b.start();
        c.start();
    }
}

控制檯輸出如下:

A窗口售出了50號票
B窗口售出了50號票
B窗口售出了48號票
...
...
...

可以發現50號票被A、B窗口都賣出了,顯然是錯誤的。這裏就出現了線程安全問題,分析如下:
線程安全問題
由控制檯的輸出我們可以做以下假設:
假設A窗口先搶到了CPU的資源,在第5行判斷num>0,打印第6行的內容,此時CPU的資源被B窗口搶走,注意A窗口並沒有執行num–,然後B窗口在第5行判斷num>0,打印第6行的內容,執行num–,此時num等於49,然後CPU的資源被A窗口搶走,此時A窗口應該執行第6行代碼,num–。現在num = 48,B窗口搶奪了CPU的資源,判斷num>0,執行第6行,就會打印出”B窗口售出了48號票”。

在什麼情況下會出現線程安全問題?
**

  1. 存在多線程
  2. 存在共享的資源(比如上例中的票)
  3. 存在多個任務來操作共享資源,當前任務進行到一半時,CPU的資源被別的線程搶奪

**

線程安全問題的解決辦法:
同步機制:調用synchronized方法時對象會被鎖定,不能被其他的線程訪問,除非當前線程完成了被同步的任務,並解除鎖定。
①:同步代碼塊

synchronized("鎖對象"){
    //需要被同步的代碼
}

同步代碼塊要注意的事項:
- “鎖對象”可以是任意的一個對象
- 多線程操作的鎖對象必須是唯一的、共享的
- 在同步代碼塊中調用sleep方法,並不會釋放鎖
- 只有存在線程安全問題的時候才使用同步代碼塊,否則會降低線程執行的效率

使用同步代碼塊完成賣票,並解決線程安全問題:

class SaleTicket extends Thread {

    // 設置爲靜態變量,因爲是三個線程的貢獻資源數據
    static int num = 50;
    static Object o = new Object();

    public SaleTicket(String name) {
        super(name);
    }

    @Override
    public void run() {

        while (true) {
            // 同步代碼塊
            synchronized (o) {

                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "售出了"
                            + num + "號票");
                    num--;
                } else {
                    System.out.println("售罄");
                    break;
                }
            }
        }
    }
}

public class Demo1 {

    public static void main(String[] args) {
        SaleTicket a = new SaleTicket("A窗口");
        SaleTicket b = new SaleTicket("B窗口");
        SaleTicket c = new SaleTicket("C窗口");
        a.start();
        b.start();
        c.start();
    }
}

②:同步函數
- 使用synchronized 修飾函數
- 如果是非靜態的同步函數,鎖對象是當前對象;如果是靜態的同步函數,鎖對象是當前函數所屬的類的class對象。


考慮以下代碼能不能解決線程安全的問題?

class BankThread extends Thread {

    static int count = 5000;

    public BankThread(String name) {
        super(name);
    }

    @Override
    public synchronized void run() {
        while (true) {

            if (count > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "取走了1000元,還剩餘" + (count - 1000) + "元");
                count = count - 1000;
            } else {
                System.out.println("賬戶餘額不足...");
                break;
            }

        }
    }
}

public class Demo2 {

    public static void main(String[] args) {

        BankThread grandFather = new BankThread("爺爺");
        BankThread grandMother = new BankThread("奶奶");
        grandMother.start();
        grandFather.start();
    }

}

控制檯輸出如下:

爺爺取走了1000元,還剩餘4000元
奶奶取走了1000元,還剩餘4000元
奶奶取走了1000元,還剩餘2000元
爺爺取走了1000元,還剩餘1000元
奶奶取走了1000元,還剩餘0元
賬戶餘額不足...
爺爺取走了1000元,還剩餘-1000元
賬戶餘額不足...

因爲synchronized 修飾的函數是非靜態的,所以當線程grandMother執行取錢任務時,鎖對象是grandMother;當grandFather 執行取錢任務時,鎖對象是grandFather 對象。鎖對象不是唯一的就不能解決線程安全問題

推薦使用同步代碼塊解決線程安全問題!!!

6. 死鎖

有時兩個或多個線程需要鎖定幾個共享對象。這時可能引起死鎖(deadlock) ,也就是說,每個線程已經鎖定一個對象,正在等待鎖定另一個對象。考慮一種有兩個線程和兩個對象的情形。線程1已經鎖定了 object1 ,線程2鎖定了 object2 。現在線程1等待鎖定 object2 ,線程2等待鎖定object1 。每個線程都等待另一個線程釋放自己需要的資源,結果導致兩個線程都無法繼續運行。
例如老公拿着銀行卡,但是沒有密碼;老婆記着密碼,但是沒有銀行卡,參考以下代碼:

class DeadLock extends Thread {

    public DeadLock(String name) {
        super(name);
    }

    @Override
    public void run() {
        if ("老公".endsWith(Thread.currentThread().getName())) {

            synchronized ("銀行卡") {
                System.out.println("老公拿到了銀行卡,等待密碼...");
                synchronized ("密碼") {
                    System.out.println("老公拿到了密碼...");
                    System.out.println("老公可以去銀行取錢了!!!");
                }
            }

        } else if ("老婆".endsWith(Thread.currentThread().getName())) {
            synchronized ("密碼") {
                System.out.println("老婆拿到了密碼,等待銀行卡...");
                synchronized ("銀行卡") {
                    System.out.println("老婆拿到了銀行卡...");
                    System.out.println("老婆可以去銀行取錢了!!!");
                }
            }
        }
    }
}

public class Demo3 {

    public static void main(String[] args) {
        DeadLock thread1 = new DeadLock("老公");
        DeadLock thread2 = new DeadLock("老婆");
        thread1.start();
        thread2.start();
    }
}

控制檯輸出如下:

老公拿到了銀行卡,等待密碼...
老婆拿到了密碼,等待銀行卡...

老公進入“銀行卡”的同步代碼塊中,但是此時老婆進入了“密碼”的同步代碼塊中。兩個線程都會陷入無休止的相互等待狀態。儘管這種情況並非經常出現,但一旦碰見,程序的調試就會變得異常艱難。就java語言本身來說,尚未提供防止死鎖的措施,我們需要謹慎設計來避免。

但是上述例子中,只要“老公”線程跑的足夠快,兩者都是可以取到錢的!嘿嘿….
可以考慮將代碼更改爲如下,“老公”和“老婆”就有更大的可能性都能取到錢了!

else if ("老婆".endsWith(Thread.currentThread().getName())) {
            try {
                Thread.sleep(5*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized ("密碼") {
                System.out.println("老婆拿到了密碼,等待銀行卡...");
                synchronized ("銀行卡") {
                    System.out.println("老婆拿到了銀行卡...");
                    System.out.println("老婆可以去銀行取錢了!!!");
                }
            }
        }

7. 線程間通信

wait():等待,線程執行了wait()方法,就會進入等待狀態,等待狀態的線程必須要被其他線程調用notify()方法才能喚醒。
notify():喚醒,喚醒等待的線程。
Note:
- wait()和notify()屬於Object對象的方法
- wait()和notify()必須要在同步代碼塊或同步函數中才能使用
- wait()和notify()必須要由鎖對象調用

當i爲偶數時生產者生產蘋果,i爲奇數是生產者生產香蕉;生產者生產一個產品消費者消費一個產品。使用以下代碼進行模擬:

//產品類
class Product {
    boolean flag = false;// 產品是否已經存在
    String name;
    double price;
}

// 生產者
class Producer extends Thread {

    Product p;

    public Producer(Product p) {
        this.p = p;
    }

    // 不斷地生產
    @Override
    public void run() {
        int i = 0;

        while (true) {
            synchronized (p) {
                if (p.flag == false) {
                    if (i % 2 == 0) {
                        p.name = "蘋果";
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        p.price = 5.0;
                    } else {
                        p.name = "香蕉";
                        p.price = 2.5;
                    }
                    System.out.println("生產者生產了:" + p.name + "價格是:" + p.price);
                    p.flag = true;
                    //生產完畢,喚醒消費者
                    p.notify();
                    i++;
                } else {
                    try {
                        // 生產者已經生產完畢,等待消費者去消費
                        p.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

// 消費者
class Customer extends Thread {

    Product p;

    public Customer(Product p) {
        this.p = p;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (p) {

                if (p.flag == true) {
                    System.out.println("消費者消費了:" + p.name + "價格是:" + p.price);
                    p.flag = false;
                    //消費完了,喚醒生產者
                    p.notify();
                }else{
                    try {
                        //產品沒有被生產,等待生產者生產
                        p.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

public class Demo4 {

    public static void main(String[] args) {

        Product p = new Product();// 產品
        Producer producer = new Producer(p);// 生產者
        Customer customer = new Customer(p);// 消費者
        producer.start();
        customer.start();
    }

}

控制檯輸出:

生產者生產了:蘋果價格是:5.0
消費者消費了:蘋果價格是:5.0
生產者生產了:香蕉價格是:2.5
消費者消費了:香蕉價格是:2.5
...
...

說明:
1. 將p作爲構造函數的參數傳入,以實現p的共享;並且讓p作爲鎖對象,調用wait和notify
2. 如果一個線程執行了wait()方法,那麼該線程就會進入一個以”鎖對象”爲標識的線程池中並處於等待狀態
3. 調用wait()方法會釋放鎖對象
4. 如果一個線程執行了notify()方法,那麼就會喚醒上述線程池中的一個處於等待狀態的線程
5. 調用notify()方法不能指定線程來喚醒,一般來說,先等待的線程先被喚醒

8. 停止線程

使用Thread類的stop()方法來停止一個線程,但此方法已經過時,不推薦使用。


如果需要停止一個處於等待狀態的線程,可以通過布爾變量配合notify()或者interrupt()方法。

public class Demo5 extends Thread {

    boolean falg = true;

    public Demo5(String name) {
        super(name);
    }

    @Override
    public synchronized void run() {
        int i = 0;
        while (falg) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + i++);
        }
    }

    public static void main(String[] args) {

        Demo5 d = new Demo5("線程A");
        d.setPriority(10);
        d.start();

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

            // 主線程i=80是,停止線程A
            if (i == 80) {
                d.falg = false;
                /*同步代碼塊*/
//              synchronized (d) {
//                  d.notify();
//              }

                /*使用interrupt清除等待狀態,中斷線程*/
                d.interrupt();
            }
        }
    }
}

線程A開啓後,flag = true ,線程A會進入等待狀態。然後執行主線程,當i=80,flag賦值爲false,並且喚醒線程A;因爲此時的flag爲false,所以run方法裏的代碼會執行完畢,線程就被停止。


使用interrupt 強制清除處於等待狀態的線程:如果線程在調用Object類的wait()、wait(long)或者wait(long,int)方法,或者Thread類的join()、join(long)、join(long,int)、sleep(long)或者sleep(long,int)方法後,執行interrupt會強制清除這些線程!並返回一個InterruptedException的異常。interrupt還可以指定清除哪個線程。

9. 守護線程和join()

守護線程的作用是在程序運行期間於後臺提供一種“常規”服務,但是它並不屬於程序的一個基本部分。一旦所有的非守護線程完成,程序就會終止,守護線程也會終止。可以調用isDaemon()查看一個線程是否爲守護線程。線程默認不是守護線程,可以使用setDaemon(true)設置一個線程爲守護線程。

以下代碼爲模擬軟件更新包的後臺下載,

public class Demo6 extends Thread {

    public Demo6(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("更新包目前下載" + i + "%....");
            if (i == 100) {
                System.out.println("更新包下載完畢,準備安裝...");
            }
        }
    }

    public static void main(String[] args) {

        Demo6 d = new Demo6("後臺線程");
        //設置爲守護線程
        d.setDaemon(true);
        System.out.println(d.isDaemon());
        d.start();

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

可以發現,當主線程停止時,後臺下載更新的守護線程也會停止。



join():一個線程如果執行了join語句,那麼就有新的線程加入,執行該語句的線程必須要讓步給新加入的線程來完成任務,然後才能繼續執行。

class Mom extends Thread{

    @Override
    public void run() {
        System.out.println("媽媽開始做飯");
        System.out.println("媽媽開始洗菜,切菜,炒菜...");
        System.out.println("媽媽發現沒有醬油了...讓我去打醬油");
        //我去打醬油
        Me me = new Me();
        me.start();
        try {
            /**
             * mom執行了me.join()語句,me線程就加入到mom線程,並且mom線程會等待me線程執行完畢纔會繼續執行
             * */
            me.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("媽媽繼續做飯");
        System.out.println("全家人開心地吃晚飯...");
    }
}

class Me extends Thread{

    @Override
    public void run() {
        System.out.println("我一直往小賣鋪走");
        System.out.println("打完醬油...");
        System.out.println("回家,並把醬油交給媽媽...");
    }
}

public class Demo7 {

    public static void main(String[] args) {

        Mom mom = new Mom();
        mom.start();
    }
}

控制檯輸出如下:

媽媽開始做飯
媽媽開始洗菜,切菜,炒菜...
媽媽發現沒有醬油了...讓我去打醬油
我一直往小賣鋪走
打完醬油...
回家,並把醬油交給媽媽...
媽媽繼續做飯
全家人開心地吃晚飯...



最後,希望這篇線程相關的文章可以對java的初學者有所幫助,上文有不當的地方,請大家提出自己寶貴的意見!

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