第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();
}
}
拓展問題:可否實現兩個儲戶交替存錢的操作。需要使用執行緒通訊!
小結:釋放鎖的操作
- 當前執行緒的同步方法、同步程式碼塊執行結束。
- 當前執行緒在同步程式碼塊、同步方法中遇到
break
、return
終止了該程式碼塊、該方法的繼續執行。 - 當前執行緒在同步程式碼塊、同步方法中出現了未處理的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