線程安全
線程的生命週期
Thread.State
類中定義了:
- 新建:
Thread
類的物件被創建 - 就緒:
start()
後等待分配CPU資源的階段,可能是獲取了同步鎖、被notify()
- 運行:拿到實際資源、開始執行
run()
方法 - 阻塞:被暫時掛起,可能是
sleep()
或是被join()
、或等待同步鎖、wait()
- 死亡:跑完或提前
stop()
、出錯了
同步代碼塊
-
解決多線程安全問題
-
格式:
synchronized (同步器) { // 需要同步的代碼 }
-
同步器可以是任何物件,只需要滿足"它是多個線程共用的",比如同類中的一個變量
- 若是靠實現
Runnable
接口方法的多線程,可以用this
,因為只有一個該類,當前對象是同一個 - 如果是繼承類實現的多線程,則可以用
static變量
,或是考慮"類名.class
"(這玩意也是唯一的)
- 若是靠實現
-
但這樣做實質等於單線程了,效率不高
同步方法
-
解決多線程安全問題
-
舉例:
public class SynTest implements Runnable { int ticket = 100; @Override public void run() { show(); } private synchronized void show() { for (; ticket > 0; ) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "賣票" + ticket); ticket--; } } } class Test { public static void main(String[] args) { SynTest s1 = new SynTest(); Thread thread0 = new Thread(s1); Thread thread1 = new Thread(s1); Thread thread2 = new Thread(s1); thread0.start(); thread1.start(); thread2.start(); } }
-
把要鎖的地方提取出來,寫成一個
synchronized
修飾的方法,切記不是寫在run()
-
非靜態的同步方法,此時的同步器=
this
。- 同理,若是由繼承類實現的多線程,它必然是一個靜態方法(加上static的),此時的同步器是
類.class
(類本身)
- 同理,若是由繼承類實現的多線程,它必然是一個靜態方法(加上static的),此時的同步器是
懶漢單例模式改進
雙重判斷是為了增加效率,同時也線程安全了
public static Bank getInstance(){
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance=new Bank();
}
}
}
return instance;
}
}
死鎖deadlock
- 不同的線程分別占用對方需要的同步資源不放棄,都在等待對方釋出資源,就形成死鎖
- 形成死鎖後不會報錯,只會互相阻塞無法繼續
- 解法:專門的算法、原則,盡量減少定義同步資源,盡量避免嵌套同步
可重入鎖ReentrantLock
解決線程安全問題
-
造一個
ReentrantLock
類的物件 (Reentrant=可重入) -
上鎖,然後用
try
把要保護的地方包起來 -
finally
區塊寫上解鎖 -
範例:
import java.util.concurrent.locks.ReentrantLock; public class SynTest implements Runnable { int ticket = 100; private ReentrantLock lock = new ReentrantLock(true); @Override public void run() { while (true) { lock.lock(); // 這裡加鎖 try { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "賣票" + ticket); ticket--; } else { break; } } finally { lock.unlock(); // 這裡解鎖,切記 } } } } class Test { public static void main(String[] args) { SynTest s1 = new SynTest(); Thread thread0 = new Thread(s1); Thread thread1 = new Thread(s1); Thread thread2 = new Thread(s1); thread0.start(); thread1.start(); thread2.start(); } }
-
new ReentrantLock(true);
後面的true表示是一個公平鎖,先入先出;預設沒寫則=false -
同樣的,若是由繼承類實現的多線程,new lock時還要加上static
-
優勢在於Lock可以靈活手動控制,而synchronized只能自動
練習Lock
同時存錢
import java.util.concurrent.locks.ReentrantLock;
class Account {
private int balance = 0;
ReentrantLock lock = new ReentrantLock(true);
public void deposit(int amt) {
lock.lock();
try {
if (amt > 0) {
balance = balance + amt;
System.out.println(Thread.currentThread().getName() + "存錢成功");
System.out.println("餘額" + balance);
}
} finally {
lock.unlock();
}
}
}
class Customer extends Thread {
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
public class BankTest {
public static void main(String[] args) {
Account acct = new Account();
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.start();
c2.start();
}
}
線程的通信
方法
wait()
:使當前線程進入阻塞狀態,並釋放同步器notifly()
:喚醒被wait的一個線程,若有多個則喚醒優先極高的那個notifyAll()
:全叫醒
限制
- 三個方法必須位於同步代碼塊或同步方法中
- 三個方法的調用者必須是同一個的計數器,例如都是
this
;否則會IllegalMonitorStateException
- 從任何物件都能成為計數器可以得知,這些方法都是位於
Object
類中
範例
class Number implements Runnable {
private int num = 1;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (this) {
this.notify(); // this可省略
if (num <= 100) {
try {
Thread.sleep(1);
System.out.println(Thread.currentThread().getName() + "-" + num);
num++;
this.wait(); // this可省略
// 注意要先執行完才wait,否則會多一次
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class ComTest {
public static void main(String[] args) {
Number number = new Number();
Thread thread1 = new Thread(number);
Thread thread2 = new Thread(number);
thread1.setName("t1");
thread2.setName("t2");
thread1.start();
thread2.start();
}
}
面試題-sleep與wait
相同:都能使當前線程進入阻塞
相異:
sleep() | wait() | |
---|---|---|
調用的主體 | Thread類 | Object類 |
使用的位置 | 任意場景 | 同步代碼塊或方法中 |
重新就緒條件 | 設定的時間 | 等到被notify為止 |
是否釋放同步器 | 否 | 是 |
經典例題-生產者與消費者
可能同時有複數的生產者跟消費者都在使用同一個資源(產品),它們共同的屬性:經過店員當作中轉
所以造一個店員類,店員擁有這個資源(產品)作為屬性
而生產者跟消費者都繼承Thread
類,並且以同一個店員作為構造器生成物件(生出來的實例都有同一個"店員"屬性,所以可以拿這個店員當共同的計數器,實現線程間的溝通
class Clerk { // 店員
private int Product = 0;
public synchronized void launchProduct() {
if (Product < 20) {
Product++;
System.out.println(Thread.currentThread().getName() +
":上架第" + Product + "個產品");
notify(); // 上架了就能喚醒消費者
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void sellProduct() {
if (Product > 0) {
System.out.println(Thread.currentThread().getName() +
":消費了第" + Product + "個產品");
Product--;
notify(); // 消費了就能喚醒生產者
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread {
private Clerk clerk;
@Override
public void run() {
System.out.println(getName() + "開始生產商品");
while (true) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.launchProduct();
}
}
public Producer(Clerk clerk) {
this.clerk = clerk;
}
}
class Customer extends Thread {
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + "開始消費商品");
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.sellProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk c1 = new Clerk();
Customer cus = new Customer(c1);
Customer cus2 = new Customer(c1);
Producer pro = new Producer(c1);
cus.setName("消費者");
cus2.setName("消費者2");
pro.setName("生產者");
cus.start();
cus2.start();
pro.start();
}
}
JDK5.0新增的線程特性
Callable接口
與Runnable
相比,Callable
有以下優勢:
- 可以有返回值,並支持泛型的返回值
- 方法可以拋出異常
- 需要藉助
FutureTask
類,例如獲取返回結果
Future接口
- 可以對具體
Runnable
、Callable
任務的執行結果進行取消、查詢等等 FutureTask
類是Future
接口的唯一實現類FutureTask
同時實現了Runnable
與Future
接口,他既可以做為Runnable
被線程執行,也可以作為Future
獲取Callable
的返回值
方法三-實現Callable接口
其實跟
Runnable
沒差多少,就是多套了一層FutureTask
當中轉來解決返回值跟拋異常的需求
-
創建一個自訂類實現
Callable
接口 -
在自訂類實現
call
方法,裡面放進想多線程的代碼(類似於run
) -
創建此自訂類的實例物件
-
將剛剛創立的物件作為形參傳遞到
FutureTask
構造器中,創立FutureTask
類的物件 -
將剛剛創立的
FutureTask
物件作為形參傳遞到Thread
構造器,創立Thread
類的物件 -
以此
Thread
類物件調用start()
方法 -
若想獲取返回值,使用
FutureTask類的物件.get()
,並且此方法可以拋出異常 -
舉例:
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; class NumThread implements Callable { @Override public Object call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) { System.out.println(i); sum += i; } return sum; } } public class CallTest { public static void main(String[] args) { NumThread numThread = new NumThread(); FutureTask futureTask = new FutureTask(numThread); new Thread(futureTask).start(); try { Object sum = futureTask.get(); System.out.println("總合為" + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
方法四-使用線程池
實際開發中使用的
-
使用
ExecutorService 接口名 = Executors.newFixedThreadPool();
創造指定線程數量的線程池接口 -
設置線程池的各種屬性,他們的關係其實是這樣:
ThreadPoolExecutor extends AbstractExecutorService
AbstractExecutorService implements ExecutorService
- 所以可以向下強轉接口得到方便設置屬性的物件
ThreadPoolExecutor 物件名= (ThreadPoolExecutor) 接口名;
-
線程池接口.execute
調用Runnable
方法 -
線程池接口.submit
調用Callable
方法,或再加.get()
獲取返回值 -
線程池接口.shutdown()
關閉線程池 -
舉例:
import java.util.concurrent.*; class NumThread1 implements Runnable { @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "-i=" + i); } } } class NumThread2 implements Callable { @Override public Object call() throws Exception { int sum = 0; for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "-i=" + i); sum += i; } return sum; } } public class ThreadPool { public static void main(String[] args) { // 1. 使用ExecutorService 接口名 = Executors.newFixedThreadPool();創造指定線程數量的線程池接口 ExecutorService service = Executors.newFixedThreadPool(5); // 2. 設置線程池的各種屬性 ThreadPoolExecutor serviceObj = (ThreadPoolExecutor) service; System.out.println(service.getClass()); serviceObj.setCorePoolSize(15); serviceObj.setKeepAliveTime(100, TimeUnit.MILLISECONDS); // 3. 線程池接口.execute調用Runnable方法 service.execute(new NumThread1()); // 4. 線程池接口.submit調用Callable方法,或再加.get()獲取返回值 try { Object obj = service.submit(new NumThread2()).get(); System.out.println("返回值=" + obj); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } // 5. 線程池接口.shutdown()關閉線程池 service.shutdown(); } }
小結
- 線程的就緒:被
notify
、獲取了同步器 - 線程的阻塞:
sleep
、wait
、被join
、等待同步器 - 同步器:必須是多個線程間共用的物件,要嘛是
this
、要嘛static
的則是類.class
本身 - 確保線程安全時,鎖的性能考慮:
ReentranLock
> 同步代碼塊 > 同步方法 - 創建多線程的4種方式與關鍵點
- 繼承
Thread
類:用static
- 實現
Runnable
接口:用this
- 實現
Callable
接口:可返回,用FutureTask
中轉 - 線程池:便於開發使用,記得關
- 繼承
上次修改於 2021-12-04