1. 程式人生 > >第11章 多執行緒

第11章 多執行緒

開發十年,就只剩下這套架構體系了! >>>   

程式、程序、執行緒

  • 程式(program)是為完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的程式碼,靜態物件。

  • **程序(process)**是程式的一次執行過程或是正在執行的一個程式。動態過程:有它自身的產生、存在和消亡的過程。

如:執行中的QQ,執行中的MP3播放器。 程式是靜態的,程序是動態的。

  • 執行緒(thread)
    ,程序可進一步細化為執行緒,是一個程式內部的一條執行路徑。

若一個程式可同一時間執行多個執行緒,就是支援多執行緒的。

程序與多執行緒

每個Java程式都有一個隱含的主執行緒: main()方法

何時需要多執行緒

  • 程式需要同時執行兩個以上任務
  • 程式需要實現一些需要等待的任務時(阻塞操作)。如使用者輸入、檔案讀寫操作、網路操作、搜尋等。
  • 需要一些後臺執行的程式時。

多執行緒的建立和啟動

  • Java語言的JVM允許程式執行多個執行緒,它通過java.lang.Thread類來實現。
  • Thread類的特性:

每個執行緒都是通過某個特定Thread物件的run()方法來完成操作的,經常把run()

方法的主體稱為執行緒體。 通過該Thread物件的start()方法來呼叫這個執行緒。

子執行緒的建立和啟動過程

public class MyThread extends Thread {

    public MyThread() {
        super();
    }

    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子執行緒:" + i);
        }
    }

    public static void main(String[] args) {
        //1.建立執行緒
        MyThread mt = new MyThread();
        //2.啟動執行緒,並呼叫當前執行緒的run()方法
        mt.start();
    }
}

Thread類

  • 構造方法

Thread():建立新的Thread物件。
Thread(String threadName):建立執行緒並指定執行緒例項名。
Thread(Runnable target):指定建立執行緒的目標物件,它實現了Runnable介面中的run()方法。
Thread(Runnable target, String threadName):建立新的Thread物件。

建立執行緒的兩種方式

  • 繼承Thread

1.定義子類,繼承Thread類。
2.在子類中,重寫Thread類中的run()方法。
3.建立Thread子類物件,即建立了執行緒物件。
4.呼叫執行緒物件start()方法:啟動執行緒,呼叫run()方法。

  • 實現Runnable介面

1.定義子類,實現Runnable介面。
2.子類中重寫Runnable介面中的run()方法。
3.通過Thread類含參構造器建立執行緒物件。
4.將Runnable介面的子類物件作為實際引數傳遞給Thread類的構造器中。
5.呼叫Thread類的start()方法:開啟執行緒,呼叫Runnable子類介面的run()方法。

說明:其實除了以上兩種方式外,還有兩種方式用來建立執行緒: 1.實現Callable介面。(Callable+Future) 2.執行緒池。(Executor)

繼承方式和實現方式的聯絡與區別

public class Thread extends Object implements Runnable 區別:

繼承Thread:執行緒程式碼存放Thread子類run()方法中。 實現Runnable:執行緒程式碼存在介面的子類的run()方法。

實現方法的好處:(推薦實現執行緒的方式)

避免了單繼承的侷限性。 多個執行緒可以共享同一個介面實現類的物件,適合多個相同執行緒來處理同一份資源

練習

建立兩個子執行緒,讓其中一個輸出1-100之間的偶數,另一個輸出1-100之間的奇數。

class Thread1 implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            //偶數
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + "--" + i);
            }
        }
    }
}

class Thread2 implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
             //奇數
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + "--" + i);
            }
        }
    }
}

public class Test1 {
    public static void main(String[] args) {
        Thread1 t1 = new Thread1();
        Thread2 t2 = new Thread2();
        new Thread(t1).start();
        new Thread(t2).start();
    }
}

Thread類的有關方法

  • void start(): 啟動執行緒,並執行物件的run()方法。
  • run():執行緒在被排程時執行的操作。(不是執行執行緒的方法)
  • String getName(): 返回執行緒的名稱
  • void setName(String name):設定該執行緒名稱。
  • static currentThread()返回當前執行緒

執行緒的排程

  • 排程策略

時間片。 搶佔式:高優先順序的執行緒搶佔CPU

  • 排程方法

同優先順序執行緒組成先進先出佇列(先到先服務),使用時間片策略。 對高優先順序,使用優先排程的搶佔式策略。

執行緒的優先順序

  • 執行緒的優先順序控制

MAX_PRIORITY:執行緒可以具有的最高優先順序。
MIN _PRIORITY:執行緒可以具有的最低優先順序。
NORM_PRIORITY:分配給執行緒的預設優先順序。

涉及的方法:

  • getPriority() :返回執行緒優先值。
  • setPriority(int newPriority) :改變執行緒的優先順序。

執行緒建立時繼承父執行緒的優先順序。

  • static native void yield():執行緒禮讓。

暫停當前正在執行的執行緒,把執行機會讓給優先順序相同或更高的執行緒。 若佇列中沒有同優先順序的執行緒,忽略此方法。

  • join():當某個程式執行流中呼叫其他執行緒的join()方法時,呼叫執行緒將被阻塞,直到join()方法加入的join執行緒執行完為止。

低優先順序的執行緒也可以獲得執行。

  • static void sleep(long millis)執行緒暫停 (時間單位:毫秒)

令當前活動執行緒在指定時間段內放棄對CPU的控制,使其他執行緒有機會被執行,時間到後重排隊。 丟擲InterruptedException異常。

  • stop(): 強制執行緒生命期結束。(已過時)
  • boolean isAlive():返回boolean,判斷執行緒是否還活著。

使用多執行緒的優點

背景:只使用單個執行緒完成多個任務(呼叫多個方法),肯定比用多個執行緒來完成用的時間更短,那為何仍還需要多執行緒呢?

多執行緒程式的優點:

  • 提高應用程式的響應。對圖形化介面更有意義,可增強使用者體驗。
  • 提高計算機系統CPU的利用率
  • 改善程式結構。將既長又複雜的程序分為多個執行緒,獨立執行,利於理解和修改。

多執行緒程式的缺點:

  • 用不好,效率會降低。因為,多執行緒的開銷要比單執行緒的開銷大。

執行緒的分類

Java中的執行緒分為兩類:一種是守護執行緒,一種是使用者執行緒

  • 它們在幾乎每個方面都是相同的,唯一的區別是判斷JVM何時離開。
  • 守護執行緒是用來服務使用者執行緒的,通過在start()方法前呼叫thread.setDaemon(true)可以把一個使用者執行緒變成一個守護執行緒。
  • Java垃圾回收就是一個的守護執行緒
  • 若JVM中都是守護執行緒,當前JVM將退出。

執行緒的生命週期(瞭解)

JDK中用Thread.State列舉表示了執行緒的幾種狀態。

要想實現多執行緒,必須在主執行緒中建立新的執行緒物件。Java語言使用Thread類及其子類的物件來表示執行緒,在它的一個完整的生命週期中通常要經歷如下的五種狀態:

  • 新建: 當一個Thread類或其子類的物件被宣告並建立時,新生的執行緒物件處於新建狀態。
  • 就緒:處於新建狀態的執行緒被start()後,將進入執行緒佇列等待CPU時間片,此時它已具備了執行的條件。
  • 執行:當就緒的執行緒被排程並獲得處理器資源時,便進入執行狀態,run()方法定義了執行緒的操作和功能。
  • 阻塞:在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出CPU並臨時中止自己的執行,進入阻塞狀態。
  • 死亡:執行緒完成了它的全部工作或執行緒被提前強制性地中止 。

執行緒的同步(重點)

問題的提出:

  • 多個執行緒執行的不確定性引起執行結果的不穩定。
  • 多個執行緒對賬本的共享,會造成操作的不完整性,會破壞資料。 (多執行緒會對共享資料造成破壞)

例題

模擬火車站售票程式,開啟三個視窗售票。

class Ticket implements Runnable {
    private int tick = 100;
    @Override
    public void run() {
        while (true) {
            if (tick > 0) {
                System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + tick--);
            } else {
                break;
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        new Thread(t, "t1視窗").start();
        new Thread(t, "t2視窗").start();
        new Thread(t, "t3視窗").start();
    }
}

private int tick = 100;
    public void run() {
        while (true) {
            if (tick > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售出車票,tick號為:" + tick--);
            }
        }
    }

多執行緒安全問題

  • 問題的原因:

當多條語句在操作同一個執行緒的共享資料時,一個執行緒對多條語句只執行了一部分,還沒有執行完,另一個執行緒參與進來執行,導致共享資料的錯誤。

  • 解決辦法:

對多條操作共享資料的語句,只能讓一個執行緒都執行完,在執行過程中,其他執行緒不可以參與執行。 1.同步程式碼塊 2.同步方法 3.Lock

Synchronized的使用方法

Java對於多執行緒的安全問題提供了專業的解決方式:同步機制

  • 同步程式碼塊
synchronized(物件){
  // 需要被同步的程式碼
}
  • 同步方法
public synchronized void 方法名 (引數列表){ 
   // 業務程式碼
}

分析同步原理

互斥鎖

在Java語言中,引入了物件互斥鎖的概念,來保證共享資料操作的完整性

  • 每個物件都對應於一個可稱為“互斥鎖”的標記,這個標記用來保證在任一時刻,只能有一個執行緒訪問該物件
  • 關鍵字synchronized來與物件的互斥鎖聯絡。 當某個物件用synchronized修飾時,表明該物件在任一時刻只能由一個執行緒訪問。
  • 同步的侷限性:導致程式的執行效率降低
  • 同步方法(非靜態的)的鎖為this
  • 同步方法(靜態的)的鎖為當前類本身。(類名.class)

單例設計模式之懶漢式

class Singleton {
    private static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            //同步程式碼塊
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

public class TestSingleton {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

練習

銀行有一個賬戶。 有兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。每次存完列印賬戶餘額。 問題:該程式是否有安全問題,如果有,如何解決? 提示: 1、明確哪些程式碼是多執行緒執行程式碼,須寫入run()方法。 2、明確什麼是共享資料。 3、明確多執行緒執行程式碼中哪些語句是操作共享資料的。

class Account {
    private int balance; //餘額
    //存錢
    public synchronized void deposit(int amt) {
        balance += amt;
        try {
            //必須有。體現“每次”。
            Thread.currentThread().sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":" + balance);
    }
}

class Customer implements Runnable {
    private Account account; //每個儲戶都有一個賬戶
    public Customer(Account account) {
        this.account = account;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            try {
                //必須有。體現“每次”。
                Thread.currentThread().sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.deposit(1000);
        }
    }
}

public class BankTest {
    public static void main(String[] args) {
        Account a = new Account();
        Customer c1 = new Customer(a);
        Customer c2 = new Customer(a);

        Thread t1 = new Thread(c1,"張三");
        Thread t2 = new Thread(c2,"李四");

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

拓展問題:可否實現兩個儲戶交替存錢的操作。需要使用執行緒通訊!

小結:釋放鎖的操作

  • 當前執行緒的同步方法、同步程式碼塊執行結束。
  • 當前執行緒在同步程式碼塊、同步方法中遇到breakreturn終止了該程式碼塊、該方法的繼續執行。
  • 當前執行緒在同步程式碼塊、同步方法中出現了未處理的Error或Exception,導致異常結束。
  • 當前執行緒在同步程式碼塊、同步方法中執行了執行緒物件的wait()方法,當前執行緒暫停,並釋放鎖。

小結:不會釋放鎖的操作

  • 執行緒執行同步程式碼塊或同步方法時,程式呼叫Thread.sleep()Thread.yield()方法暫停當前執行緒的執行。
  • 執行緒執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()方法將該執行緒掛起,該執行緒不會釋放鎖(同步監視器)。

應儘量避免使用suspend()resume()來控制執行緒。 這兩個方法已過時。

執行緒的死鎖問題

  • 死鎖

不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖

  • 解決方法

專門的演算法、原則。 儘量減少同步資源的定義

例:

class A {
	public synchronized void foo(B b) {
		System.out.println("當前執行緒名: " + Thread.currentThread().getName()
				+ " 進入了A例項的foo方法"); // ①
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("當前執行緒名: " + Thread.currentThread().getName()
				+ " 企圖呼叫B例項的last方法"); // ③
		b.last();
	}
	public synchronized void last() {
		System.out.println("進入了A類的last方法內部");
	}
}

class B {
	public synchronized void bar(A a) {
		System.out.println("當前執行緒名: " + Thread.currentThread().getName()
				+ " 進入了B例項的bar方法"); // ②
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("當前執行緒名: " + Thread.currentThread().getName()
				+ " 企圖呼叫A例項的last方法"); // ④
		a.last();
	}
	public synchronized void last() {
		System.out.println("進入了B類的last方法內部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();
	public void init() {
		Thread.currentThread().setName("主執行緒");
		// 呼叫a物件的foo方法
		a.foo(b);
		System.out.println("進入了主執行緒之後");
	}
	public void run() {
		Thread.currentThread().setName("副執行緒");
		// 呼叫b物件的bar方法
		b.bar(a);
		System.out.println("進入了副執行緒之後");
	}
	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();
		dl.init();
	}
}
public class TestDeadLock {

    static StringBuffer s1 = new StringBuffer();
    static StringBuffer s2 = new StringBuffer();

    public static void main(String[] args) {
        
        new Thread() {
            public void run() {
                synchronized (s1) {
                    s2.append("A");
                    synchronized (s2) {
                        s2.append("B");
                        System.out.print(s1);
                        System.out.print(s2);
                    }
                }
            }
        }.start();
        
        new Thread() {
            public void run() {
                synchronized (s2) {
                    s2.append("C");
                    synchronized (s1) {
                        s1.append("D");
                        System.out.print(s2);
                        System.out.print(s1);
                    }
                }
            }
        }.start();
    }
}

執行緒通訊

  • wait():令當前執行緒掛起並放棄CPU、同步資源,使別的執行緒可訪問並修改共享資源,而當前執行緒排隊等候再次對資源的訪問。
  • notify()喚醒正在排隊等待同步資源的執行緒中優先順序最高者結束等待。
  • notifyAll()喚醒正在排隊等待資源的所有執行緒結束等待。

java.lang.Object提供的這三個方法只有在同步方法同步程式碼塊中才能使用,否則會報java.lang.IllegalMonitorStateException異常。

wait() 方法

  • 在當前執行緒中呼叫方法:物件名.wait()
  • 功能:使當前執行緒進入等待/掛起(某物件)狀態 ,直到另一執行緒對該物件發出notify()(或notifyAll()) 為止。
  • 呼叫方法的必要條件:當前執行緒必須具有對該物件的監控權(加鎖)。
  • 呼叫此方法後,當前執行緒將釋放物件監控權 ,然後進入等待。
  • 在當前執行緒被notify後,要重新獲得監控權,然後從斷點處繼續程式碼的執行。

notify()、notifyAll()

  • 在當前執行緒中呼叫方法:物件名.notify()
  • 功能:喚醒等待該物件監控權的一個執行緒。
  • 呼叫方法的必要條件:當前執行緒必須具有對該物件的監控權(加鎖)。

例題

使用兩個執行緒列印 1-100。執行緒1,執行緒2 交替列印。

public class Communication implements Runnable {

    private int i = 1;
    
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + i++);
                } else {
                   break;
                }
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Communication c = new Communication();
        new Thread(c).start();
        new Thread(c).start();
    }
}

經典例題:生產者/消費者問題

生產者(Productor)將產品交給店員(Clerk),而消費者(Consumer)從店員處取走產品。 店員一次只能持有固定數量的產品(比如:20)。 1.如果生產者試圖生產更多的產品,店員會叫生產者停一下,當店中有空位放產品了,再通知生產者繼續生產; 2.如果店中沒有產品了,店員會告訴消費者等一下,當店中有產品了再通知消費者來取走產品。

這裡可能出現兩個問題:

  • 生產者比消費者快時,消費者會漏掉一些資料沒有取到。
  • 消費者比生產者快時,消費者會取相同的資料。
class Clerk {  //售貨員
    private int product = 0;

    public synchronized void addProduct() {
        if (product >= 20) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            product++;
            System.out.println("生產者生產了第" + product + "個產品");
            notifyAll();
        }
    }

    public synchronized void getProduct() {
        if (this.product <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("消費者取走了第" + product + "個產品");
            product--;
            notifyAll();
        }
    }
}

class Productor implements Runnable {  //生產者
    Clerk clerk;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }

    public void run() {
        System.out.println("生產者開始生產產品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
            }
            clerk.addProduct();
        }
    }
}

class Consumer implements Runnable {  //消費者
    Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    public void run() {
        System.out.println("消費者開始取走產品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
            }
            clerk.getProduct();
        }
    }
}

public class TestProduct {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Thread productorThread = new Thread(new Productor(clerk));
        Thread consumerThread = new Thread(new Consumer(clerk));
        productorThread.start();
        consumerThread.start();
    }
}

練習

模擬銀行取錢的問題 1.定義一個Account類 1)該Account類封裝了賬戶編號(String)和餘額(double)兩個屬性 2)設定相應屬性的getter和setter方法 3)提供無參和有兩個引數的構造器 4)系統根據賬號判斷與使用者是否匹配,需提供hashCode()和equals()方法的重寫 2.提供一個取錢的執行緒類 1)提供了Account類的account屬性和double類的取款額的屬性 2)提供帶執行緒名的構造方法 3)run()方法中提供取錢的操作 3.在主類中建立執行緒進行測試。考慮執行緒安全問題。

class Account {
    private String accountId;
    private double balance;

    public Account() {
    }

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

    public String getAccountId() {
        return accountId;
    }

    public void setAccountId(String accountId) {
        this.accountId = accountId;
    }

    public double getBalance() {
        return balance;
    }

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

    public String toString() {
        return "Account [accountId=" + accountId + ", balance=" + balance + "]";
    }

    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((accountId == null) ? 0 : accountId.hashCode());
        long temp;
        temp = Double.doubleToLongBits(balance);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Account other = (Account) obj;
        if (accountId == null) {
            if (other.accountId != null) return false;
        } else if (!accountId.equals(other.accountId))
            return false;
        if (Double.doubleToLongBits(balance) != Double
                .doubleToLongBits(other.balance))
            return false;
        return true;
    }
}

class WithDrawThread extends Thread {
    Account account;
    //要取款的額度
    double withDraw;

    public WithDrawThread(String name, Account account, double amt) {
        super(name);
        this.account = account;
        this.withDraw = amt;
    }

    public void run() {
        synchronized (account) {
            if (account.getBalance() > withDraw) {
                System.out.println(Thread.currentThread().getName()
                        + ":取款成功,取現的金額為:" + withDraw);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.setBalance(account.getBalance() - withDraw);
            } else {
                System.out.println("取現額度超過賬戶餘額,取款失敗");
            }
            System.out.println("現在賬戶的餘額為:" + account.getBalance());
        }
    }
}

public class TestWithDrawThread {
    public static void main(String[] args) {
        Account account = new Account("1234567", 10000);
        Thread t1 = new WithDrawThread("小明", account, 8000);
        Thread t2 = new WithDrawThread("小明's wife", account, 2800);
        t1.start();
        t2.start