1. 程式人生 > >09 - JavaSE之線程

09 - JavaSE之線程

操作系統 run 上下 thread 直接 logs 程序計數器 length 資源

線程

線程的基本概念

線程是一個程序裏面不同的執行路徑。

  • 進程與線程的區別
  1. 每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換開銷大。
  2. 線程可以看作輕量級的進程,同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程的切換開銷小。
  3. 多進程:在操作系統中能同時運行多個程序。
  4. 多線程:在同一應用程序中有多個順序流同時執行。

線程的創建與啟動

  1. Java 的線程是通過 java.lang.Thread 類來實現的。
  2. VM 啟動時,會有一個由主方法 main 所定義的線程。
  3. 可以通過創建 Thread 的實例來創建新的線程。
  4. 每個線程都是通過某個特定的 Thread 對象所對應的 run() 方法來完成這個線程要做的任務,方法 run() 成為線程體。
  5. 通過調用 Thread 類的 start() 方法來啟動一個線程。
  • 有兩種方法創建新的線程:
  1. 第一種(推薦使用):定義線程類實現 Runnable 接口,然後重寫 run 方法, 然後以這個線程類創建 Thread 類,然後調用這個 Thread 類的 start() 方法,就可以開始執行這個線程,這個線程具體要執行的內容在 run 方法裏面。

    public class Test {
    public static void main(String[] args) {
    MyThread mt = new MyThread();
    Thread th = new Thread(mt);
    th.start();

        for(int i=0; i<100; i++) {
            System.out.println("Main: " + i);
        }
    }

    }

    class MyThread implements Runnable {
    @Override
    public void run() {
    for(int i=0; i<100; i++) {
    System.out.println("MyThread:" + i);
    }
    }
    }

PS: 如果我們沒有 new一個 Thread 對象出來,而是直接使用 MyThread 的 run 方法(mt.run()),這就是方法調用,而不是啟動線程了,結果就是先後打印語句,而不是並行打印語句了。

  1. 第二種:可以定義一個 Thread 的子類 myThread,並且重寫 Thread 的 run 方法(Thread 也實現了Runnable 接口),然後生成子類 myThread 的對象,最後調用子類 myThread 對象的 start() 方法即可。

    public class Test {
    public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();

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

    }

    class MyThread extends Thread {
    @Override
    public void run() {
    for(int i=0; i<100; i++) {
    System.out.println("MyThread:" + i);
    }
    }
    }


線程的狀態轉換

技術分享圖片


線程控制基本方法

isAlive() // 判斷線程是否還活著,即線程是否還未終止
getPriority() // 獲得線程的優先級數值
setPriority() // 設置線程的優先級數值
Thread.sleep(...) // 將當前線程指定睡眠時間
join() // 將一個線程合並到某個線程上,成為一個線程執行
yield() // 讓出CPU,當前線程進入就緒隊列等待調度
wait() // 當前線程進入對象的 wait pool
notify()/notifyAll() // 喚醒對象 wait pool中的一個/所有等待線程
  • sleep 方法

可以調用 Thread 的靜態方法 Thread.sleep(long ms) 使得當前線程休眠。(哪個線程調用了Thread.sleep 方法,哪個線程就 sleep)

import java.util.*;
public class Test {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread th = new Thread(mt);
        th.start();

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

        }

        th.interrupt();
    }
}

class MyThread implements Runnable {
    private boolean flag = true;
    @Override
    public void run() {
        while(flag) {
            System.out.println("=== " + new Date() + " ===");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                flag = false;
            }
        }
    }
}
  • join 方法

將一個線程合並到某個線程上,成為一個線程執行。(如下程序就先執行線程 th ,之後才會執行主線程 Main ,而不是並行執行。)

import java.util.*;
public class Test {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread th = new Thread(mt);
        th.start();

        try {
            th.join();
        } catch(InterruptedException e) {

        }

        for(int i=0; i<10; i++) {
            System.out.println("Main Thread......");
        }

        th.interrupt();
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        for(int i=0; i<10; i++) {
            System.out.println("=== " + new Date() + " ===");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
}
  • yield 方法

讓出CPU,當前線程進入就緒隊列等待調度。

public class Test {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("mt1");
        MyThread mt2 = new MyThread("mt2");
        mt1.start();
        mt2.start();
    }
}

class MyThread extends Thread {
    MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        try {
            for (int i = 0; i < 50; i++) {
                System.out.println(getName() + " --- " + i);
                Thread.sleep(100);

                if (i % 10 == 0) {
                    Thread.yield();
                }
            }
        } catch (InterruptedException e) {

        }
    }
}

線程的優先級

  • Java 提供一個線程調度器來監控程序中啟動後進入就緒狀態的所有線程。線程調度器按照線程的優先級決定應調度哪個線程來執行。
  • 線程的優先級用數字表示,範圍從 1 到 10,一個線程的缺省優先級是 5。

    Thread.MIN_PRIORITY = 1
    Thread.MAX_PRIORITY = 10
    Thread.NORM_PRIORITY = 5

  • 使用下面方法獲得或設置線程對象的優先級:

    int getPriority();
    void setPriority(int newPriority);


Java 中的線程臨界區

  • 系統中每次只允許一個線程訪問的資源叫做臨界資源。
  • 對臨界資源進行訪問的程序代碼區域叫做臨界區。
  • Java 中通過 synchronized 關鍵字和對象鎖機制對臨界區進行管理。
  • Java 中的每個對象都可以作為對象鎖使用。

線程同步

我們先看這樣一個程序:

public class Test implements Runnable {
    Timer time = new Timer();
    public static void main(String args[]){
        Test test = new Test();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        time.add(Thread.currentThread().getName());
    }
}

class Timer {
    private static int num = 0;
    public void add(String name) {
        num ++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(name + " 是第 " + num + " 個使用timer的線程");
    }
}

我們兩個線程同時執行,而且調用同一個方法,相當於訪問同一個共享資源 num,執行的結果為:

t2 是第 2 個使用timer的線程
t1 是第 2 個使用timer的線程

這是怎麽回事呢?按照猜想,雖然 t1 t2 線程並行執行,但是先開啟的 t1 進程,num = 1;在開啟的 t2 線程,num = 2,所以,應該是:t1 是第 1 個使用timer的線程;t2 是第 2 個使用timer的線程 才對啊?

其實,這就涉及到線程同步的問題,如果在一個線程訪問一個共享對象的時候沒有給這個共享資源上鎖的話,那麽這個線程操作的共享資源可能就是錯誤的,因為可能別的進程也在訪問這個共享資源。

那麽,我們就需要在進程訪問這個共享資源的時候,將其上鎖,上鎖的方式有兩種:(還是以上面的程序為例:)

// 方式一
class Timer {
    private static int num = 0;
    public void add(String name) {
        synchronized (this) {} {  // 資源上鎖
            num ++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {

            }
            System.out.println(name + " 是第 " + num + " 個使用timer的線程");
        }
    }
}

synchronized (this) {

// 需要鎖定的內容

}

synchronized(this)表示: 鎖定當前對象。括號中的語句在一個線程執行的過程中,不會被另一個線程打斷。

// 方式二
class Timer {
    private static int num = 0;
    synchronized public void add(String name) { // 資源上鎖
        num ++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(name + " 是第 " + num + " 個使用timer的線程");
    }
}

在 add 函數加上 synchronized 關鍵字,就表示哪個進程調用了add 函數,那麽這個進程在執行這個方法的時候,鎖定當前對象。

  • 在 Java 語言中引入了對象互斥鎖的概念,保證共享數據操作的完整性。每個對象都對應於一個可稱為“互斥鎖”的標記,這個標記保證在任意時刻,智能有一個線程訪問該對象。
  • 關鍵字 synchronized 來與對象的互斥鎖聯系。當某個對象 synchronized 修飾時,表明該對象在任一時刻只能由一個線程訪問。

線程死鎖

什麽是線程死鎖?

如果兩個或多個線程分別擁有不同的資源, 而同時又需要對方釋放資源才能繼續運行時,就會發生死鎖。簡單來說:死鎖就是當一個或多個進程都在等待系統資源,而資源本身又被占用時,所產生的一種狀態。

public class Test implements Runnable {
    public int flag = 0;

    static Object o1 = new Object();
    static Object o2 = new Object();
    
    public static void main(String args[]){
        Test test1 = new Test();
        Test test2 = new Test();

        test1.flag = 1;
        test2.flag = 2;

        Thread t1 = new Thread(test1);
        Thread t2 = new Thread(test2);

        t1.start();
        t2.start();
    }

    public void run() {
        System.out.println("flag=" + flag);
        if(1 == flag) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                synchronized (o2) {
                    System.out.println("1");
                }
            }
        }

        if(2 == flag) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                synchronized (o1) {
                    System.out.println("2");
                }

            }
        }
    }
}

註意:以上代碼中 Object 對象 O1 O2 一定是 static 的,否則不能得到進程死鎖。還有一定要有 Thread.sleep 語句。

舉例:

public class Test implements Runnable {
    public int  b = 100;

    public synchronized void m1() throws Exception{
        b = 1000;
        Thread.sleep(3000);
        System.out.println("m1:b = " + b);
    }

    public void m2 () {
        System.out.println("m2:b = " + b);
        b = 2;
        System.out.println("m2:b = " + b);
    }

    public void run() {
        try {
            m1();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        Test tt = new Test();
        Thread t = new Thread(tt);
        t.start();

        Thread.sleep(1000);

        tt.m2();

        System.out.println("b = " + tt.b);
    }
}

提問:各個打印 b 的值為多少?

m2:b = 1000

m2:b = 2

b = 2

m1:b = 2

為什麽 b 的值可以被修改呢?

因為,如果一個方法加了 synchronized ,而且有一個進程正在訪問這個方法,那麽只能說明別的進程不可以同時訪問這個方法,但是並不妨礙別的進程訪問其他的方法,如果其他的方法中有對你需要保護的對象(這裏是 b)進行操作的話,也是允許的。

所以,如果要保護一個需要同步的對象的話,對訪問這個對象的所有方法考慮加不加 synchronized 。因為一個方法加了鎖,只是另一個線程不能訪問加了鎖的方法,但是可以訪問其他的方法,其他的方法可能修改了你需要同步的對象。synchronized 修飾的方法可以防止多個線程同時訪問這個對象的synchronized方法(如果一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法。(所以,上面的 m2 方法也需要加 synchronized 修飾。)


生產者和消費者問題:

/**
 * 這裏模擬的是生產者和消費者的問題:這裏生產者生產櫻花膏,消費者吃櫻花膏。然後
 * 還有一個裝櫻花膏的籃子 basket. 我們需要做的生產者生產的櫻花膏按照順序放入籃子,
 * 吃貨消費者按照順序從頂部依次拿出來吃,所以 basket 的數據結構是棧。
 *
 * 註意:生產者生產的櫻花膏要和吃貨需要吃的櫻花膏數量一致,否則程序會wait
 */

public class Test {
    public final int NUM = 10;
    public static void main(String[] args) {
        BasketStack bs = new BasketStack();
        Producer p = new Producer(bs);
        Consumer c = new Consumer(bs);

        new Thread(p).start();
        new Thread(p).start();
        new Thread(p).start();

        new Thread(c).start();
    }
}

// 櫻花膏
class Sakura {
    private int id = 0;

    Sakura (int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return ((Integer)id).toString();
    }
}

// 籃子
class BasketStack {
    int index = 0;
    Sakura[] sakuras = new Sakura[6]; // 一個籃子只能裝6個櫻花膏

    public synchronized void push(Sakura sa) {
        while(index == (sakuras.length - 1)) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll();
        sakuras[index] = sa;
        index ++;
    }

    public synchronized Sakura pop() {
        while(index == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.notifyAll();
        index--;
        return sakuras[index];
    }
}

// 生產者
class Producer implements Runnable {
    BasketStack bs = null;

    Producer(BasketStack bs) {
        this.bs = bs;
    }

    @Override
    public void run() {
        for(int i=0; i<10; i++) {
            Sakura sa = new Sakura(i);
            bs.push(sa);
            System.out.println("生產者:" + sa.toString());
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 吃貨
class Consumer implements Runnable {
    BasketStack bs = null;

    Consumer(BasketStack bs) {
        this.bs = bs;
    }

    @Override
    public void run() {
        for(int i=0; i<30; i++) {
            System.out.println("吃貨:" + bs.pop().toString());

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

wait和sleep的區別:

1.wait之後 鎖就不歸我所有,別的線程可以訪問鎖定對象;sleep後 鎖還是我的,別的線程不可以訪問鎖定對象。

2.調用 wait方法的時候必須先鎖定對象,否則談何wait。

2.wait是Object的方法,sleep是Thread的方法。


總結(關鍵字聯想)

  • 線程/進程的概念
  • 創建和啟動線程的方式
  • sleep
  • join
  • yield
  • synchronized
  • wait
  • notify/notifyAll

09 - JavaSE之線程