線程的生命週期
1.新建 :當一個Thread類或其子類的對象被生命並創建時,新生的線程對象處於新建狀態。
2.就緒 :處於新建狀態的線程被start()後,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源
3.運行 :當就緒的線程被調度並獲得CPU資源時,便進入運行狀態,run()方法定義了線程的操作和功能。
4.阻塞 :在某種特殊情況下,被人爲掛起或執行輸入輸出操作時,讓出CPU並臨時中止自己運行的狀態,進入阻塞狀態。
5.死亡 :線程完全了它的全部工作或線程被提前強制性地中止或出現異常導致結束。
線程的同步—安全問題
經典例子: 兩個人同時對同一個銀行賬戶進行操作, 賬戶餘額5k,a取3k,b取3k。結果都同時操作完成, 賬戶餘額變成了-1k。
下面對售票系統的訴說。
在添加一個synchronized鎖之後,能解決重票的問題,synchronized給線程加上了一個鎖,其他線程沒拿到就不能執行邏輯代碼, 等待當前拿到鎖的線程處理完邏輯釋放鎖之後被下一個線程拿到,下一個線程的邏輯才能接着運行。
這種方式叫做:同步代碼塊。
synchronized(同步監視器--條件){
//需要被同步的代碼
}
說明:
1.操作共享數據的代碼,即爲需要被同步的代碼。
2.共享數據:多個線程共同操作的變量,例如上面例子的票數。
3.同步監視器。俗稱:鎖,任何一個類的對象,都可以充當鎖。要求:多個線程必須要共用同一把鎖。
第二種方法:同步方法
如果操作共享數據的代碼完整的聲明在一個方法中, 我們就聲明該方法爲同步方法
在方法中加synchronized的修飾詞。
--要觀察好代碼運行的邏輯,決定是否需要將邏輯塊再次單獨封裝成一個方法,並在當前方法前面加synchronized修飾。
解決繼承Thread的線程問題:
private static synchronized void reduce(){//static == 指示當前類 在加synchronized中
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "搶到鎖,進行消耗: " + ticket);
ticket --;
}
}
第三種方法:Lock鎖—JDK5.0開始
eg:
//1.實例化
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
//調用lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票。票號:" + ticket);
ticket --;
}else{
break;
}
}finally {
//3.調用解鎖的方法
lock.unlock();
}
}
}
同步的好處:解決了線程的安全問題, ----好處
操作同步代碼時只能有一個線程參與,其他線程需要等待,–相當於單線程執行,效率低。
synchronized用法— 必須是線程中共享的唯一變量,否則的話 線程安全問題仍然會產生,synchronized也沒用。
synchronized跟Lock的異同?
同:都可以解決線程的安全問題。
不同:synchronized機制在執行完相應的同步代碼後,自動釋放同步監視器。
而Lock在語句結束需要調用unlock方法。
死鎖
- 不同的線程分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就會形成了線程的死鎖
- 出現死鎖後,不會出現異常,不會出現提示,只是所有的線程都處於阻塞狀態。
解決方法:
- 專門的算法、原則
- 儘量減少同步資源的定義
- 儘量減免嵌套同步
線程三個方法:
wait() —一旦執行此方法,線程就會進入到阻塞狀態,並且釋放同步監視器。
notify() — 一旦調用此方法, 就會喚醒調用了wait方法的線程。如果有多個線程被wait,就喚醒優先級高的線程。
notifyAll() —一旦執行此方法,就會喚醒所有被wait的線程。
上述的三個方法,必須使用在同步代碼塊或者同步方法中的同步監視器。
sleep()和wait()的異同:
1.都是使線程進入阻塞狀態。
2.不同:wait()需要等待線程調用notify()或者notifyAll(),
sleep()只需要等待時間過去就會喚醒線程。
兩個方法聲明的位置不同:Thread類中聲明Sleep(),Object類中聲明wait()。
sleep()可以在任何需要調用的地方調用, 而wait()需要在同步塊內。
如果兩個方法都在同步內容內,sleep()不會釋放鎖,wait()會釋放鎖。
JDK5新增:Callable接口
Callable接口功能更強大:
相比Run()方法,Callable可以有返回值。
方法可以拋出異常。
支持泛型的返回值。
需要藉助FutureTask類,比如獲取返回結果。
Future接口:
可以對具體Runnable、Callable任務的執行結果進行取消、查詢是否完成、獲取結果等。
FutureTask是Futrue接口的唯一的實現類。
FutureTask同時實現了Runnable,Future接口。它3既可以作爲Runnable被線程執行,又可以作爲Future得到Callable的返回值。
具體用例:
class NumThread implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1 ; i <= 100 ; i++){
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNewCallable {
public static void main(String[] args) {
NumThread numThread = new NumThread();
FutureTask futureTask = new FutureTask<>(numThread);
new Thread(futureTask).start();
try {
//get()返回值即爲FutureTask構造器參數Callable實現 類重寫的call()的返回值
Object object = futureTask.get();
System.out.println(object);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
上面代碼實現步驟:
1.創建一個實現Callable的實現類
2.實現call方法,將此線程需要執行的操作聲明在call()中,這個call方法是擁有返回值的
3.創建Callable接口實現類的對象
4.將此Callable接口實現類的對象作爲參數傳遞到FutureTask構造器中,創建FutureTask的對象
5.將FutureTask的對象作爲參數傳遞到Thread類的構造器中,創建Thread中並且調用start()
如果要獲取call()的返回值,調用FutureTask.get()就可以了。
如何理解實現Callable接口的方式創建多線程比實現Runnable接口創建多線程方式強大?
1.call()擁有返回值。
2.call()可以拋出異常,根據異常做處理。被外面的操作捕獲,獲取異常信息
3.Callable是支持泛型的
線程池:
背景:經常創建和銷燬、使用量特別大的資源,比如併發情況下的線程,對性能影響很大。
思路:提前創建好多個線程,放入線程池中,使用時直接獲取, 使用完放回池中。可以避免頻繁創建銷燬、實現重複利用。
好處:
1.提高響應速度(減少了創建新線程的時間)
2.降低資源消耗(重複利用線程池中線程,不需要每次都創建)
3.便於線程管理:
corePoolSize:核心池的大小
maximumPoolSize:最大線程數
KeepAliveTime:線程沒有任務時最多保持多長時間後會終止。
JDK5.0提供了線程池相關的API:ExecutorService和Executors
ExecutorService: 真正的線程池接口。常見子類ThreadPoolExecutor
void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行Runnable
Future submit(Callable task):執行任務,有返回值,一般用來執行Callable
void shutdown():關閉連接池。
Executors: 工具類、線程池的工廠類,用於創建並返回不同類型的線程池
Executors.newCachedThreadPool():創建一個可根據需要創建新線程的線程池
Executors.newFixedThreadPool(n):創建一個可重用固定線程數的線程池
Executors.newSingleThreadExecutor():創建一個只有一個線程的線程池
Executors.newScheduledThreadPool(n):創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。
class NumThread implements Runnable{
@Override
public void run() {
for(int i = 0 ; i < 100 ; i ++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
class NumThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int num = 0 ;
for(int i = 0 ; i < 100 ; i ++){
if(i % 2 == 0){
num += i;
}
}
return num;
}
}
public class ThreadPoolDemo {
public static void main(String[] args) {
// Executors.newCachedThreadPool();//創建一個可根據需要創建新線程的線程池
ExecutorService executorService = Executors.newFixedThreadPool(10);//創建一個可重用固定線程數的線程池
// Executors.newSingleThreadExecutor();//創建一個只有一個線程的線程池
// Executors.newScheduledThreadPool(1);//創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。
executorService.execute(new NumThread());//參數只能是runnable---適用於runnable
FutureTask<Integer> futureTask = (FutureTask) executorService.submit(new NumThread2());//適合是用於Callable
try {
Integer integer = futureTask.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
步驟:
1.提供提供線程數量的線程池
2.執行指定的線程的操作,需要指定實現Runnable接口或Callable接口實現類的對象
3.關閉連接池(不用的時候)
線程池的實現類爲:java.util.concurrent.ThreadPoolExecutor
如果要實現對線程池的屬性修改,即需要轉換類型爲ThreadPoolExecutor後對其進行修改: