< 筆記 > Java SE - 04 Java SE 多線程

04 Java SE 多線程

By Kevin Song

  • 04-01 多線程概述
  • 04-02 JVM中的多線程
  • 04-03 多線程創建
  • 04-04 多線程同步
  • 04-05 多線程中的單例模式
  • 04-06 多線程死鎖
  • 04-07 多線程通信
  • 04-08 多線程停止

04-01 多線程概述

進程:正在進行中的程序

線程:進程中的一個負責程序執行的控制單元(執行路徑)

  • 一個進程中可以有多個線程
  • 一個進程中至少有一個線程

多線程作用:開啓多個線程是爲了同時運行多部分代碼

多線程的優缺點

  • 優點:可以同時運行多個程序
  • 缺點:內存處理到程序頻率變低,運行速度變慢

04-02 JVM中的多線程

JVM啓動時就啓動了多個線程,至少有兩個線程

  • 執行主方法的線程:該線程的任務代碼都定義在主方法中
  • 負責垃圾回收的線程

Object 類中的 finalize() 方法

當垃圾回收器確定不存在該對象的更多引用時,由對象的垃圾回收器調用此方法

System類中的 System.gc() 方法:運行垃圾回收器,垃圾回收器運行時間隨機

class Demo extends Object {
    public void finalize() {
        System.out.println("Recycle Complete");
    }
}
class ThreadDemo {
    public static void main(String[] args) {
        new Demo();
        new Demo();
        System.gc(); //
        new Demo();
        System.out.println("Hello World!");
    }
}
/*
輸出
Hello World!
Recycle Complete
Recycle Complete
因爲是兩個線程,先運行主線程,JVM關閉前運行垃圾回收線程
*/

04-03 多線程創建

主方法單線程運行

不創建多線程

class Demo {
    private String name;
    Demo (Srting name) {
        this.name = name;
    }
    public void show() {
        for(int x = 0; x < 10; x++) {
            //y的for循環讓每次輸出都有一定的延遲,但是必須d1都輸出完才輸出d2,這時就需要多線程來讓他們同時輸出
           for(int y =-9999999; y < 999999999; y++) {}
           System.out.println(name+"...x="+x);
        }
    }
}
class ThreadDemo {
    public static void main(String[] args) {
        Demo d1 = new Demo("Sequence Initializing");
        Demo d2 = new Demo("序列初始化中");
        d1.show();
        d2.show();
    }
}

創建多線程

目的:開啓一條執行路徑,使得指定的代碼和其他代碼實現同時運行,而此執行路徑運行的指定代碼就是這個執行路徑的任務

任務存放位置
- JVM創建的主線程的任務都定義在了主方法
- 自定義線程的任務定義在Thread類的run方法

創建多線程有兩種方法

  • 繼承Thread
  • 實現Runnable接口

創建線程方式一

繼承Thread

步驟:

  1. 定義一個類,繼承Thread
  2. 重寫Thread類中的 run(); 方法
  3. 直接創建Thread類的子類對象(創建線程)
  4. 調用 start(); 方法開啓線程,並調用線程的任務run(); 方法
class Demo extends Thread{
    private String name;
    Demo (Srting name) {
        this.name = name;
    }
    public void run() {
        for(int x = 0; x < 10; x++) {
           //currentThread()獲取當前運行中線程的引用
           System.out.println(name+"...x="+x+"...Thread Name="+Thread.currentThread().getname());
        }
    }
}
class ThreadDemo {
    public static void main(String[] args) {
        Demo d1 = new Demo("Sequence Initializing");
        Demo d2 = new Demo("序列初始化中");
        d1.start();//開啓線程,調用run方法
        d2.start();//開啓線程,調用run方法
        //CPU在主線程,d1,d2之間隨機高速切換
    }
}

Thread類中的方法&線程名稱

getName()方法

獲取線程的名稱Thread-編號(從0開始)
主線程的名字main

currentThread()方法

獲取當前運行中線程的引用,該方法爲靜態,可以直接被類名調用

多線程異常問題

有異常的線程結束運行,run() 方法出棧,線程之間互不影響。

線程的狀態

  • new被創建
    • start() 運行(具備執行資格,具備執行權)
      • 凍結(釋放執行權,釋放執行資格)
        • sleep(time) 凍結 時間到自動喚醒
        • wait() 凍結 notify 喚醒
      • 阻塞(具備執行資格,不具備執行權,正在等待執行權)
      • 消亡
        • run() 方法結束
        • stop() 停止線程

CPU的執行資格:可以被CPU處理,在處理隊列中排隊
CPU的執行權:正在被CPU處理

創建線程方式二

實現Runnable接口

步驟:

  1. 定義一個類,實現Runnable接口
  2. 重寫接口中的run方法,將線程的任務代碼封裝到run方法中
  3. 通過Thread類創建線程對象,將Runnable接口的子類對象作爲構造方法的參數進行傳遞
  4. 調用線程對象的start(); 方法開啓線程
class Demo extends Fu implements Runable{ //無法繼承Thread但是需要多線程,通過接口形式完成
    public void run() {
        show();
    }
    public void show() {
        for(int x = 0; x < 20; x++) {
            System.out.println(Thread.currentThread().getName()+"..."+x);
        }
    }
}
class ThreadDemo {
    public static void main(String[] args) {
        Demo d = new Demo();
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        t1.start;
        t2.start;
    }
}

第二種方法的好處

  • 將線程的任務從線程的子類中分離出來,進行了單獨的封裝,按照面向對象的思想將任務封裝成對象
  • 避免了java單繼承的侷限性

04-04 多線程同步

賣票示例

四個線程,每個線程都賣100張票

class Ticket extends Thread{
    private int num = 100;
    public void run() {
        sale();
    }
    public void sale() {
        while(true) {
            if(num > 0) {
                System.out.println(Thread.currentThread().getName()+“...sale...”num--);
            }
        }
    }
}
class TicketDemo {
    public static void main(String[] args) {
        //四個對象,每個對象都有100張票
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        Ticket t4 = new Ticket();

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

四個線程一起賣100張票

class Ticket implements Runnable {
    private int num = 100;
    public void run() {
        sale();
    }
    public void sale() {
        while(true) {
            if(num > 0) {
                System.out.println(Thread.currentThread().getName()+“...sale...”num--);
            }
        }
    }
}
class TicketDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket(); //一個對象,所有一共只有100張票
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

線程安全問題的現象

會出現0,-1,-2張票的情況,因爲線程1還沒運行到num–的時候,線程2,線程3有可能就已經加載進來,當num–運行完之後,線程還會繼續運行,這就導致0,-1,-2的出現。

安全問題產生的原因

  • 多個線程在操作共享的數據
  • 操作共享數據的線程代碼有多條

當一個線程在執行操作共享數據的多條代碼過程中,其他線程參與了運算

多線程同步

將多條操作共享的數據的線程代碼封裝起來,當有線程在執行這些代碼的時候,其他線程不可以參與運算。

同步代碼塊

格式

synchronized(對象) {
    需要被同步的代碼;
}
class Ticket implements Runnable {
    private int num = 100;
    Object obj = new Object; //此對象爲了傳給synchronized作爲鎖
    public void run() {
        sale();
    }
    public void sale() {
        while(true) {
            synchronized(obj) { //synchronized修飾的代碼塊只能同一時間被一個線程調用
                if(num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {}
                    System.out.println(Thread.currentThread().getName()+“...sale...”num--);
                }
            }
        }
    }
}
class TicketDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket(); //一個對象,所有一共只有100張票
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        Ticket t4 = new Ticket();

        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

同步的前提

  • 同步中必須有多個線程並使用同一個鎖
  • 當作鎖的對象必須在 run() 方法外創建

同步的優缺點

  • 優點:解決了線程的安全問題
  • 缺點:相對降低了效率,因爲同步外的線程都會判斷同步鎖

同步方法

/*
兩個客戶每次去銀行存100,存三次
*/
class Bank {
    private int sum;
    private Object obj = new Object();
    public synchronized void add(int num) {//同步方法
//        synchronized(obj) {
            sum = sum + num;
            System.out.println("sum="+sum);
//        }
    }
}
class Cus implements Runnable {
    private Bank b = new Bank();
    public void run() {
        for(int x = 0; x < 3; x++) {
            b.add(100);
        }
    }
}
class BankDemo {
    public static void main(String[] args) {
        Cus c = new Cus;
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}
/*
輸出:
sum=100
sum=200
sum=300
sum=400
sum=500
sum=600
*/

同步代碼塊和同步方法同時使用

class Ticket implements Runnable {
    private int num = 100;
    Object obj = new Object; //此對象爲了傳給synchronized
    boolean flag = true;
    public void run() {
        if(flag)//flag爲true則運行同步代碼快
            while(true) {
                synchronized(this) {
                    if(num>0) {
                        try {
                            Thread.sleep(10);
                        } catch(InterruptedException e) {}
                        System.out.println(Thread.currentThread().getName()+"...obj..."+num--);
                    }
                }
            }
        else //flag爲false則運行同步方法
            while(true)
                this.show();
    }
    public synchronized void show() {
        if(num > 0) {
            try {
                Thread.sleep(10);
            } catch ()
            System.out.println(Thread.currentThread().getName()+“...syn...”num--);
        }
    }
}
class TicketDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket(); //一個對象,所有一共只有100張票
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();

        t1.start();

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

        t.flag = false;
        t2.start();
    }
}

同步方法和同步代碼快的區別

  • 同步方法的鎖是固定this(當前對象)
  • 同步代碼塊的鎖是任意的對象

靜態同步方法使用的鎖:
該方法所屬字節碼文件對象,可以用如下方式獲取

  • this.getClass() 僅限非靜態方法中使用
  • Ticket.class

04-05 多線程中的單例模式

使用同步雖然可以避免安全問題,但是會導致程序運行效率變低,因爲每個線程都需要判斷是否可以拿鎖

//餓漢式(單例設計模式)
class Single {
    private static final Single s = new Single();
    private Single() {}
    public static Single getInstance() {
        return s;
    }
}
//懶漢式(延遲加載單例設計模式)
class Single {
    private static Single s = null;
    private Single() {}
    public static Single getInstance() {
        if(s == null) {//解決效率問題:多加一句判斷,後續線程不會判斷是否可以拿鎖
            //不用this.getClass()是因爲getClass()是非靜態方法
            synchronized(Single.class) {//解決安全問題
                if(s == null)
                    s = new Single();
            }
        }
        return s;
    }
}
class SingleDemo {
    public static void main(String[] args) {
        System.out.println();
    }
}

04-06 多線程死鎖

同步代碼塊嵌套

class Test implements Runnable{
    private boolean flag;
    Test(boolean flag) {
        this.flag = flag;
    }
    public void run() {
        if(flag) {
            synchronized(MyLock.locka) {
                System.out.println("if...locka...");
                synchronized(MyLock.lockb) {
                    System.out.println("if...lockb...");
                }
            }
        } else {
            synchronized(MyLock.lockb) {
                System.out.println("if...lockb...");
                synchronized (MyLock.locka) {
                    System.out.println("if...locka...");
                }
            }
        }
    }
}
class MyLock {
    public static final Object locka = new Object();
    public static final Object lockb = new Object();
}
class DeadLockTest {
    public static void main(String[] args) {
        Test a = new Test(true);
        Test b = new Test(false);

        Thread t1 = new Thread(a);
        Thread t2 = new Thread(b);

        t1.start;
        t2.start;
    }
}

04-07 多線程通信

定義:多個線程在處理同一資源,任務卻不同

等待喚醒機制

涉及的方法

  • wait(); 讓線程處於凍結狀態,釋放執行權和執行資格,存儲到線程池中
  • notify(); 喚醒線程池中一個線程
  • notifyAll(); 喚醒線程池中所有線程,防止多生產多消費的死鎖問題

這些方法都必須定義在同步中,因爲這些方法都是用於線程狀態的方法,必須要明確到底操作的是哪個鎖上的線程。

等待喚醒機制中的方法(wait(); notify(); notifyAll();)定義在Object類中,因爲這些方法是監視器的方法,監視器其實就是鎖

//資源
class Resource {
    private String name;
    private String sex;
    privateboolean flag = false; //用來判斷是否可以輸入輸出
    public synchronized void set(String name) {
        if(flag) //如果flag爲false,則不wait
            try {
                this.wait();
            } catch (InterruptException e) {}
        this.name = name;
        this.sex = sex;
        flag = true;
        this.notify();
    }
    public synchronized void out() {
        if(!flag)
            try {
                this.wait();
            } catch (InterruptException e) {}
        System.out.println(name+"..."+sex);
        flag = false;
        notify();
    }
}
//輸入
class Input implements Runnable {
    Resource r;
    Input(Resource r) {
        this.r = r;
    }
    public void run() {
        int x = 0;
        while(true) {
            if(x == 0) { //控制兩種數據輸入
                r.set("Kevin","Male")
            } else {
                r.set("Lily","Female");
            }
            x = (x+1)%2;
        }
    }
}
//輸出
class Output implements Runnable {
    Resource r;
    Output(Resource r) {
        this.r = r;
    }
    public void run() {
        while(true) { 
            r.out();
            }
        }
    }
}
class ResourceDemo {
    public static void mian(String[] args) {
        //創建資源
        Resource r = new Resource();
        //創建任務
        Input in  = new Input(r);
        Output out = new Output(r);
        //創建線程
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
        //開啓線程
        t1.start();
        t2.start();
    }
}

多生產者多消費者問題

多生產多消費與單生產單消費的區別

flag判斷語句區別

  • if只判斷標記一次,會導致不該運行的線程開始運行,出現數據錯誤
  • while可以循環判斷標記,解決了線程獲取執行權後,是否要運行的問題

喚醒語句區別

  • notify()只能喚醒一個線程,如果喚醒了本方,沒有意義,並且會導致死鎖,所有線程都進線程池
  • notifyAll()解決了,本方線程一定會喚醒對方線程
//定義資源
class Resource {
    private String name;//資源名稱
    private int count = 1;//計數器
    private boolean flag = false;
    public synchronized void set(String name) {
        while(flag)//flag爲false時不wait,直接運行下面內容。
            try {
                this.wait();//當flag爲true時進入wait狀態
            } catch (InterruptException e) {}
        this.name = name + count;
        count++;
        System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
        flag = true; //flag改成true
        notify(); //隨機喚醒一個線程,如果是多生產多消費者則用notifyAll();
    }
    public synchronized void out() {
        while(!flag)
            try {
                this.wait();
            } catch (InterruptException e) {}
        System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
        flag = false;
        notify();//隨機喚醒一個線程,如果是多生產多消費者則用notifyAll();
    }
}
class Producer implements Runnable {
    private Resource r;
    Producer(Resource r) {
        this.r = r;
    }
    public void run() {
        while(true) {
            r.set("烤鴨");
        }
    }
}
class Producer implements Runnable {
    private Resource r;
    Producer(Resource r) {
        this.r = r;
    }
    public void run() {
        while(true) {
            r.out();
        }
    }
}
class ProducerConsumerDemo {
    public static void main(String[] args) {
        Resource r = new Resource();
        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(con);
        t0.start();
        t1.start();
    }
}

JDK1.5新特性

Lock
Lock接口:代替了同步代碼快或者同步方法,將同步的隱式鎖操作變成顯式鎖操作,更爲靈活,可以一個鎖上加多組監視器。

  • lock(); 獲取鎖
  • unlock(); 釋放鎖,通常需要定義在finally代碼塊中
Lock lock = new ReentrantLock();
public void run() {
    lock.lock();
    try {
        code;
    } finally {
        lock.unlock();
    }
}

Condition

Condition接口:代替了Object中的wait notify notifyAll方法。這些監視器被封裝,變成Condition監視器對象,可以任意鎖進行組合

  • await(); 功能同wait()
  • signal(); 同notify()
  • signalAll(); 同notifyAll()

1.5版本的多生產者多消費者代碼

//定義資源
class Resource {
    private String name;//資源名稱
    private int count = 1;//計數器
    private boolean flag = false;
    //創建一個鎖對象
    Lock lock = new ReentrantLock();

    //通過已有的鎖獲取該鎖上的監視器對象
    //Condition con = lock.newCondition();

    Condition Producer_con = lock.newCondition();
    Condition Consumer_con = lock.newCondition();

    public void set(String name) {
        lock.lock();
        try {
            while(flag)//flag爲false時不wait,直接運行下面內容。
                try {
                    Producer_con.await();//當flag爲true時進入wait狀態
                } catch (InterruptException e) {}
            this.name = name + count;
            count++;
            System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
            flag = true; //flag改成true
            //notifyAll();
            //con.signalAll();
            consumer_con.signal();
        } finally {
            lock.unlock();
        }
    }
    public synchronized void out() {
        lock.lock();
        try {
            while(!flag)
                try {
                    consumer_con.await();
                } catch (InterruptException e) {}
            System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
            flag = false;
            //notifyAll();
            //con.signalAll();
            //Producer_Con.signal();
        } finally {
            lock.unlock();
        }
    }
}
class Producer implements Runnable {
    private Resource r;
    Producer(Resource r) {
        this.r = r;
    }
    public void run() {
        while(true) {
            r.set("烤鴨");
        }
    }
}
class Consumer implements Runnable {
    private Resource r;
    Producer(Resource r) {
        this.r = r;
    }
    public void run() {
        while(true) {
            r.out();
        }
    }
}
class ProducerConsumerDemo {
    public static void main(String[] args) {
        Resource r = new Resource();
        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(con);
        t0.start();
        t1.start();
    }
}

wait和sleep區別

  • 時間指定不同
    • wait可以指定時間也可以不指定
    • sleep**必須**指定時間
  • 在同步中時,對CPU的執行權的處理不同
    • wait:釋放執行權,釋放鎖
    • sleep:釋放執行權,不釋放鎖

04-08 多線程停止

停止線程

  • stop方法,已經過時
  • 定義標記,使run方法結束

定義標記

定義一個標記flag,當flag爲true時線程可以運行,當flag爲負時線程停止運行。

侷限性:當有wait() 的時候會導致線程無法繼續判斷flag而進程凍結

class StopThread implements Runnable {
    boolean flag = true; //定義flag標記
    public void run() {
        while(flag) { //flag標記爲true時運行run方法
            System.out.println(Thread.currentThread().getName()+"...");
        }
    }
    public void setFlag() { //定義設置flag爲false的方法
        flag = false;
    }
}
class StopThreadDemo {
    public static void main(String[] args) {
        StopThread st = new StopThread();

        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);

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

        int num = 1;
        for(;;) {
            if(++num==50) {
                st.setFlag();
                break;
            }
            System.out.println("main..."+num);
        }
        System.out.println("over");
    }
}

interrupt()方法

Interrput() 方法可以把線程從凍結狀態強制恢復到運行狀態中來

class StopThread implements Runnable {
    boolean flag = true; //定義flag標記
    public synchronized void run() {
        while(flag) { //flag標記爲true時運行run方法
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getNmae()+"..."+e);
                flag = false;
            }
            System.out.println(Thread.currentThread().getName()+"...");
        }
    }
    public void setFlag() { //定義設置flag爲false的方法
        flag = false;
    }
}
class StopThreadDemo {
    public static void main(String[] args) {
        StopThread st = new StopThread();

        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);

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

        int num = 1;
        for(;;) {
            if(++num==50) {
                //st.setFlag();
                t1.interrupt();//強制恢復運行
                t2.interrupt();//強制恢復運行
                break;
            }
            System.out.println("main..."+num);
        }
        System.out.println("over");
    }
}

setDaemon()方法

使線程變成守護線程(後臺線程),前臺線程結束之後後臺線程自動結束

join()方法
執行權讓給t1

t1.join();

setPriority()方法
設置線程優先級

t1.setPriority(Thread.MAX_PRIORITY);
  • MAX_PRIORITY
  • MIN_PRIORITY
  • NORM_PRIORITY
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章