多線程相關總結:
萬字圖解Java多線程
Java(Android)線程池
面試官:說說多線程并發問題
一、概述:
1、線程是什么呢?
我們先來說一說比較熟悉的進程吧,之后就比較容易理解線程了。所謂進程,就是一個正在執行(進行)中的程序。每一個進程的執行都有一個執行順序,或者說是一個控制單元。簡單來說,就是你做一件事所要進行的一套流程。線程,就是進程中的一個獨立的控制單元;也就是說,線程是愛控制著進程的執行。一個進程至少有一個線程,并且線程的出現使得程序要有效率。打個比方說,在倉庫搬運貨物,一個人搬運和五個人搬運效率是不一樣的,搬運貨物的整個程序,就是進程;每一個人搬運貨物的過程,就是線程。
2、java中的線程:
在java中,JVM虛擬機啟動時,會有一個進程為java.exe,該程序中至少有一個線程負責java程序的執行;而且該程序運行的代碼存在于main方法中,該線程稱之為主線程。其實,JVM啟動時不止有一個線程(主線程),由于java是具有垃圾回收機制的,所以,在進程中,還有負責垃圾回收機制的線程。
3、多線程的意義:
透過上面的例子,可以看出,多線程有兩方面的意義:
1)提高效率。【運行更快,CPU資源利用更充分】
2)清除垃圾,解決內存不足的問題。
二、自定義線程:
線程有如此的好處,那要如何才能通過代碼自定義一個線程呢?其實,線程是通過系統創建和分配的,java是不能獨立創建線程的;但是,java是可以通過調用系統,來實現對進程的創建和分配的。java作為一種面向對象的編程語言,是可以將任何事物描述為對象,從而進行操作的,進程也不例外。我們通過查閱API文檔,知道java提供了對線程這類事物的描述,即Thread類。創建新執行線程有兩種方法:
1、創建線程方式
方式一:繼承Thread類
- 局限性:
單繼承的局限性
任務中的成員變量不共享,加入static才能共享
1、步驟:
第一、定義類繼承Thread。
第二、復寫Thread類中的run方法。
第三、調用線程的start方法。分配并啟動該子類的實例。
start方法的作用:啟動線程,并調用run方法。
class Demo extends Thread
{
public void run()
{
for (int i=0;i<60;i++)
System.out.println(Thread.currentThread().getName() + "demo run---" + i);
}
}
class Test2
{
public static void main(String[] args)
{
Demo d1 = new Demo();//創建一個對象就創建好了一個線程
Demo d2 = new Demo();
d1.start();//開啟線程并執行run方法
d2.start();
for (int i=0;i<60;i++)
System.out.println("Hello World!---" + i);
}
}
2、運行特點:
A.并發性:
我們看到的程序(或線程)并發執行,其實是一種假象。有一點需要明確:;在某一時刻,只有一個程序在運行(多核除外),此時cpu是在進行快速的切換,以達到看上去是同時運行的效果。由于切換時間是非常短的,所以我們可以認為是在并發進行。
B.隨機性:
在運行時,每次的結果不同。由于多個線程都在獲取cpu的執行權,cpu執行到哪個線程,哪個線程就會執行。可以將多線程運行的行為形象的稱為互相搶奪cpu的執行權。這就是多線程的特點,隨機性。執行到哪個程序并不確定。
3、覆蓋run方法的原因:
1)Thread類用于描述線程。該類定義了一個功能:用于存儲線程要運行的代碼,該存儲功能即為run方法。也就是說,Thread類中的run方法用于存儲線程要運行的代碼,就如同main方法存放的代碼一樣。
2)復寫run的目的:將自定義代碼存儲在run方法中,讓線程運行要執行的代碼。直接調用run,就是對象在調用方法。調用start(),開啟線程并執行該線程的run方法。如果直接調用run方法,只是將線程創建了,但未運行。
方式二:實現Runnable接口
- 局限性:
沒有返回值
任務無法拋異常給調用者
1、步驟:
第一、定義類實現Runnable接口。
第二、覆蓋Runnable接口中的run方法。
第三、通過Thread類建立線程對象。要運行幾個線程,就創建幾個對象。
第四、將Runnable接口的子類對象作為參數傳遞給Thread類的構造函數。
第五、調用Thread類的start方法開啟線程,并調用Runnable接口子類的run方法。
//多個窗口同時賣票
class Ticket implements Runnable
{
private int tic = 20;
public void run()
{
while(true)
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "sale:" + tic--);
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
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();
}
}
2、說明:
A.步驟2覆蓋run方法:將線程要運行的代碼存放在該run方法中。
B.步驟4:為何將Runnable接口的子類對象傳給Thread構造函數。因為自定義的run方法所屬對象為Runnable接口的子類對象,所以讓線程指定對象的run方法,就必須明確該run方法所屬的對象。
方式三:實現Callable接口
利用
FutureTask
執行任務
// 實現接口
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
log.info("我是實現Callable的任務");
return "success";
}
}
// 執行
FutureTask<String> target = new FutureTask<>(new MyCallable());
new Thread(target).start();
log.info(target.get());
需要注意的是:局部變量在每一個線程中都獨有一份。
2、Thread類中的一些方法簡介:
在這簡單介紹幾個Thread中的方法:
1、線程名稱
- 獲取線程名稱:
getName()
每個線程都有自己默認的名稱,
也就是說,線程一為:Thread-0,線程二為:Thread-1。
也可以獲取當前線程對象的名稱,通過currentThread().getName()
。
// 調用
對象.getName();
// 結果
Thread-編號(從0開始)
- 設置線程名稱:
setName()
或構造函數
可以通過setName()設置線程名稱,或者通過含有參數的構造函數直接顯式初始化線程的名稱。如:Test(String name)
2、線程的禮讓:
- 優先級:
setPriority()
在Thread中,存在著1~10這十個執行級別;但是并不是優先級越高,就會一直執行這個線程,只是說會優先執行到這個線程,此后還是有其他線程會和此線程搶奪cpu執行權的。
cpu比較忙時,優先級高的線程獲取更多的時間片
cpu比較閑時,優先級設置基本沒用
優先級是可以設定的,可通過setPriority()設定
//最低
public final static int MIN_PRIORITY = 1;
//默認
public final static int NORM_PRIORITY = 5;
// 最高
public final static int MAX_PRIORITY = 10;
// 方法的定義
public final void setPriority(int newPriority) {
}
yield()
此方法可暫停當前線程,而執行其他線程。通過這個方法,可稍微減少線程執行頻率,達到線程都有機會平均被執行的效果。
即讓運行中的線程切換到就緒狀態,重新爭搶cpu的時間片,爭搶時是否獲取到時間片看cpu的分配。
如下示例:t2線程每次執行時進行了yield(),線程1執行的機會明顯比線程2要多。
// 方法的定義
public static native void yield();
Runnable r1 = () -> {
int count = 0;
for (;;){
log.info("---- 1>" + count++);
}
};
Runnable r2 = () -> {
int count = 0;
for (;;){
Thread.yield();
log.info(" ---- 2>" + count++);
}
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();
// 運行結果
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
11:49:15.798 [t2] INFO thread.TestYield - ---- 2>293
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518
3、守護線程:setDaemon()
- 默認情況下,java進程需要等待所有線程都運行結束,才會結束;
有一種特殊線程叫守護線程,當所有的非守護線程都結束后,即使它沒有執行完,也會強制結束。
默認的線程都是非守護線程。- 垃圾回收線程就是典型的守護線程
- 可將一個線程標記為守護線程,直接調用
setDaemon()
方法。
此方法需要在啟動前調用守護線程在這個線程結束后,會自動結束,則Jvm虛擬機也結束運行。
........
//守護線程(后臺線程),在啟動前調用。后臺線程自動結束
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
.........
4、線程的阻塞:
- 阻塞方式:
BIO阻塞,即使用了阻塞式的IO流
sleep(long time)
讓線程休眠進入阻塞狀態
a.join()
調用該方法的線程進入阻塞,等待a線程執行完恢復運行
sychronized
或ReentrantLock
造成線程未獲得鎖進入阻塞狀態 (同步鎖章節細說)
獲得鎖之后調用wait()
方法 也會讓線程進入阻塞狀態 (同步鎖章節細說)
LockSupport.park()
讓線程進入阻塞狀態 (同步鎖章節細說)sleep()
使線程休眠,會將運行中的線程進入阻塞狀態。當休眠時間結束后,重新爭搶cpu的時間片繼續運行
// 方法的定義 native方法
public static native void sleep(long millis) throws InterruptedException;
try {
// 休眠2秒
// 該方法會拋出 InterruptedException異常 即休眠過程中可被中斷,被中斷后拋出異常
Thread.sleep(2000);
} catch (InterruptedException異常 e) {
}
try {
// 使用TimeUnit的api可替代 Thread.sleep
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
join()
【臨時加入線程】
特點:當A線程執行到B線程方法時,A線程就會等待,B線程都執行完,A才會執行。join可用來臨時加入線程執行。
class Demo implements Runnable{
public void run(){
for(int x=0;x<90;x++){
System.out.println(Thread.currentThread().getName() + "----run" + x);
}
}
}
class JoinDemo{
public static void main(String[] args)throws Exception{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
t1.join();//等t1執行完了,主線程才從凍結狀態恢復,和t2搶執行權。t2執不執行完都無所謂。
int n = 0;
for(int x=0;x<80;x++){
System.out.println(Thread.currentThread().getName() + "----main" + x);
}
System.out.println("Over");
}
}
5、停止線程:
stop
【過時】
在Java1.5之后,就不再使用stop
方法停止線程了。那么該如何停止線程呢?只有一種方法,就是讓run方法結束。
開啟多線程運行,運行代碼通常為循環結構,只要控制住循環,就可以讓run方法結束,也就可以使線程結束。
注: 特殊情況:當線程處于凍結狀態,就不會讀取標記,那么線程就不會結束。如下:
class StopThread implements Runnable{
private boolean flag = true;
public synchronized void run(){
while (flag){
try{
wait();
}catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "----Exception");
flag = false;
}
System.out.println(Thread.currentThread().getName() + "----run");
}
}
public void changeFlag(){
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 n = 0;
while (true){
if (n++ == 60){
st.changeFlag();
break;
}
System.out.println("Hello World!");
}
}
}
這時,當沒有指定的方式讓凍結的線程回復打破運行狀態時,就需要對凍結進行清除。強制讓線程回復到運行狀態來,這樣就可以操作標記讓線程結束。
interrupt()
1、此方法是為了讓線程中斷,但是并沒有結束運行,讓線程恢復到運行狀態,再判斷標記從而停止循環,run方法結束,線程結束。
2、可以打斷sleep,wait,join等顯式的拋出InterruptedException方法的線程,但是打斷后,線程的打斷標記還是false
isInterrupted()
獲取線程的打斷標記 ,調用后不會修改線程的打斷標記
interrupted()
獲取線程的打斷標記,調用后清空打斷標記 即如果獲取為true 調用后打斷標記為false (不常用)
class StopThread implements Runnable{
private boolean flag = true;
public synchronized void run(){
while (flag){
try{
wait();
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName() + "----Exception");
flag = false;
}
System.out.println(Thread.currentThread().getName() + "----run");
}
}
}
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 n = 0;
while (true){
if (n++ == 60){
t1.interrupt();
t2.interrupt();
break;
}
System.out.println("Hello World!");
}
}
}
三、線程的運行狀態
1、系統線程的狀態
- 初始狀態:創建線程對象時的狀態
- 可運行狀態(就緒狀態):調用start()方法后進入就緒狀態,也就是準備好被cpu調度執行
- 運行狀態:線程獲取到cpu的時間片,執行run()方法的邏輯
- 阻塞狀態: 線程被阻塞,放棄cpu的時間片,等待解除阻塞重新回到就緒狀態爭搶時間片
- 終止狀態: 線程執行完成或拋出異常后的狀態
需要說明的是:
- 阻塞狀態:具備運行資格,但是沒有執行權,必須等到cpu的執行權,才轉到運行狀態。
- 凍結狀態:放棄了cpu的執行資格,cpu不會將執行權分配給這個狀態下的線程,必須被喚醒后,此線程要先轉換到阻塞狀態,等待cpu的執行權后,才有機會被執行到。
2、Thread類定義的線程狀態
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
-
NEW
線程對象被創建 -
RUNNALE
線程調用了start()方法后進入該狀態,該狀態包含了三種情況- 就緒狀態 :等待cpu分配時間片
- 運行狀態:進入Runnable方法執行任務
- 阻塞狀態:BIO 執行阻塞式io流時的狀態
-
BLOCKED
沒獲取到鎖時的阻塞狀態(同步鎖章節會細說) -
WAITING
調用wait()、join()等方法后的狀態 -
TIMED_WAITING
調用 sleep(time)、wait(time)、join(time)等方法后的狀態 -
TERMINATED
線程執行完成或拋出異常后的狀態
四、上下文切換
CPU在多個線程間進行調度,運行時會進行上下文切換
發生切換場景:
- 線程的cpu時間片用完
- 垃圾回收
- 線程自己調用了
sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法
當發生上下文切換時,操作系統會保存當前線程的狀態,并恢復另一個線程的狀態,jvm中有塊內存地址叫程序計數器,用于記錄線程執行到哪一行代碼,是線程私有的。
五、多線程的安全問題:
在那個簡單的賣票小程序中,發現打印出了0、-1、-2等錯票,也就是說這樣的多線程在運行的時候是存在一定的安全問題的。
1、為什么會出現這種安全問題呢?
原因是當多條語句在操作同一線程共享數據時,一個線程對多條語句只執行了一部分,還未執行完,另一線程就參與進來執行了,導致共享數據發生錯誤。
以也就是說,由于cpu的快速切換,當執行線程一時,tic為1了,執行到if (tic > 0)的時候,cpu就可能將執行權給了線程二,那么線程一就停在這條語句了,tic還沒減1,仍為1;線程二也判斷if (tic > 0)是符合的,也停在這,以此類推。
當cpu再次執行線程一的時候,打印的是1號,執行線程二的時候,是2號票,以此類推,就出現了錯票的結果。其實就是多條語句被共享了,如果是一條語句,是不會出現此種情況的。
-
問題根源
:一行代碼編譯成字節碼的時候可能為多行,在多個線程上下文切換時就可能交錯執行。
2、線程安全
-
線程安全
:多線程調用同一個對象的臨界區的方法時,對象的屬性值一定不會發生錯誤,這就保證了線程安全。
線程安全的類一定所有的操作都線程安全嗎?
開發中經常會說到一些線程安全的類,如ConcurrentHashMap,線程安全指的是類里每一個獨立的方法是線程安全的,但是方法的組合就不一定是線程安全的。
成員變量和靜態變量是否線程安全?
- 如果沒有多線程共享,則線程安全
- 如果存在多線程共享
- 多線程只有讀操作,則線程安全
- 多線程存在寫操作,寫操作的代碼又是臨界區,則線程不安全
局部變量是否線程安全?
- 局部變量是線程安全的
- 局部變量引用的對象未必是線程安全的
- 如果該對象沒有逃離該方法的作用范圍,則線程安全
- 如果該對象逃離了該方法的作用范圍,比如:方法的返回值,需要考慮線程安全
3、那么該如何解決呢?(synchronized
)
對于多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可參與執行,就不會出現問題了。Java對于多線程的安全問題,提供了專業的解決方式,即同步代碼塊,可操作共享數據。
1、同步代碼塊
synchronized(對象)//對象稱為鎖旗標
{
需要被同步的代碼
}
其中的對象如同鎖,持有鎖的線程可在同步中執行,沒有鎖的線程,即使獲得cpu的執行權,也進不去,因為沒有獲取鎖,是進不去代碼塊中執行共享數據語句的。
同步的前提:
- A.必須有兩個或兩個以上的線程
- B.必須保證同步的線程使用同一個鎖。必須保證同步中只能有一個線程在運行。
好處與弊端:
- 解決了多線程的安全問題(阻塞式的解決方案)。
- 多個線程需要判斷鎖,較為消耗資源。
class Ticket implements Runnable
{
private int tic = 100;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)//任意的一個對象
{
//此兩句為共享語句
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "sale:" + tic--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t,"1");//創建第一個線程
Thread t2 = new Thread(t,"2");//創建第二個線程
//開啟線程
t1.start();
t2.start();
}
}
2、同步函數
同步函數就是將修飾符synchronized放在返回類型的前面,下面通過同步函數給出多線程安全問題的具體解決方案:
1)目的:判斷程序中是否有安全問題,若有,該如何解決。
2)解決:
第一、明確哪些代碼是多線程的運行代碼
第二、明確共享數據
第三、明確多線程運行代碼中,哪些語句是操作共享數據的。
示例:
class Bank
{
private int sum;//共享數據
//run中調用了add,所以其也為多線程運行代碼
public synchronized void add(int n)//同步函數,用synchronized修飾
{
//這有兩句操作,是操作共享數據的
sum += n;
System.out.println("sum" + sum);
}
}
class Cus implements Runnable
{
private Bank b = new Bank();//共享數據
//多線程運行代碼run
public void run()
{
for (int i=0;i<3;i++)
{
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();
}
}
六、同步函數中的鎖:
1、非靜態同步函數中的鎖:this
函數需被對象調用,那么函數都有一個所屬的對象引用,就是this,因此同步函數使用的鎖為this。
測驗如下:
class Ticket implements Runnable
{
private int tic = 100;
boolean flog = true;
public void run()
{
if (flog)
{
//線程一執行
while(true)
{
//如果對象為obj,則是兩個鎖,是不安全的;換成this,為一個鎖,會安全很多
synchronized(this)
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "--cobe--:" + tic--);
}
}
}
//線程二執行
else
while(true)
show();
}
public synchronized void show()
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);
}
}
class ThisLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//創建一個線程
Thread t2 = new Thread(t);//創建一個線程
t1.start();
t.flog = false;//開啟線程一,即關閉if,讓線程二執行else中語句
t2.start();
}
}
讓線程一執行打印cobe的語句,讓線程二執行打印show的語句。如果對象換位另一個對象obj,那將是兩個鎖,因為在主函數中創建了一個對象即Ticket t = new Ticket();,線程會共享這個對象調用的run方法中的數據,所以都是這個t對象在調用,那么,其中的對象應為this;否則就破壞了同步的前提,就會出現安全問題。
2、靜態同步函數中的鎖:
如果同步函數被靜態修飾后,經驗證,使用的鎖不是this了,因為靜態方法中不可定義this,所以,這個鎖不再是this了。靜態進內存時,內存中沒有本類對象,但是一定有該類對應的字節碼文件對象:類名.class;該對象的類型是Class。
所以靜態的同步方法使用的鎖是該方法所在類的字節碼文件對象,即類名.class。
示例:
>class Ticket implements Runnable
{
//私有變量,共享數據
private static int tic = 100;
boolean flog = true;
public void run()
{
//線程一執行
if (flog)
{
while(true)
{
synchronized(Ticket.class)//不再是this了,是Ticket.class
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "--obj--:" + tic--);
}
}
}
//線程二執行
else
while(true)
show();
}
public static synchronized void show()
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);
}
}
class StaticLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//創建第一個線程
Thread t2 = new Thread(t);//創建第二個線程
t1.start();
t.flog = false;
t2.start();
}
}
在之前,也提到過關于多線程的安全問題的相關知識,就是在單例設計模式中的懶漢式中,用到了鎖的機制。
七、多線程間的通信:
多線程間通信是線程之間進行交互的方式,簡單說就是存儲資源 和 獲取資源。比如說倉庫中的貨物,有進貨的,有出貨的。還比如生產者和消費者的例子。這些都可以作為線程通信的實例。那么如何更好地實現通信呢?
先看下面的代碼:
/*
線程間通信:
等待喚醒機制:升級版
生產者消費者 多個
*/
import java.util.concurrent.locks.*;
class ProducerConsumerDemo{
public static void main(String[] args){
Resouse r = new Resouse();
Producer p = new Producer(r);
Consumer c = new Consumer(r);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
Thread t3 = new Thread(p);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Resouse{
private String name;
private int count = 1;
private boolean flag = false;
private Lock lock = new ReentrantLock();
private Condition condition_P = lock.newCondition();
private Condition condition_C = lock.newCondition();
//要喚醒全部,否則都可能處于凍結狀態,那么程序就會停止。這和死鎖有區別的。
public void set(String name)throws InterruptedException{
lock.lock();
try{
while(flag)//循環判斷,防止都凍結狀態
condition_P.await();
this.name = name + "--" + count++;
System.out.println(Thread.currentThread().getName() + "..生成者--" + this.name);
flag = true;
condition_C.signal();
}finally{
lock.unlock();//釋放鎖的機制一定要執行
}
}
public void out()throws InterruptedException{
lock.lock();
try{
while(!flag)//循環判斷,防止都凍結狀態
condition_C.await();
System.out.println(Thread.currentThread().getName() + "..消費者." + this.name);
flag = false;
condition_P.signal();//喚醒全部
}finally{
lock.unlock();
}
}
}
class Producer implements Runnable{
private Resouse r;
Producer(Resouse r){
this.r = r;
}
public void run(){
while(true){
try{
r.set("--商品--");
}catch (InterruptedException e){}
}
}
}
class Consumer implements Runnable{
private Resouse r;
Consumer(Resouse r){
this.r = r;
}
public void run(){
while(true){
try{
r.out();
}catch (InterruptedException e){}
}
}
}
1、等待喚醒機制:
1、顯式鎖機制和等待喚醒機制:
在JDK 1.5中,提供了改進synchronized的升級解決方案。將同步synchronized替換為顯式的Lock操作,將Object中的wait,notify,notifyAll替換成Condition對象,該對象可對Lock鎖進行獲取。這就實現了本方喚醒對方的操作。
在這里說明幾點:
1)、對于wait,notify和notifyAll這些方法都是用在同步中,也就是等待喚醒機制,這是因為要對持有監視器(鎖)的線程操作。所以要使用在同步中,因為只有同步才具有鎖。
2)、而這些方法都定義在Object中,是因為這些方法操作同步中的線程時,都必須表示自己所操作的線程的鎖,就是說,等待和喚醒的必須是同一把鎖。不可對不同鎖中的線程進行喚醒。所以這就使得程序是不良的,因此,通過對鎖機制的改良,使得程序得到優化。
3)、等待喚醒機制中,等待的線程處于凍結狀態,是被放在線程池中,線程池中的線程已經放棄了執行資格,需要被喚醒后,才有被執行的資格。
2、對于上面的程序,有兩點要說明:
1)、為何定義while判斷標記:
原因是讓被喚醒的線程再判斷一次。
避免未經判斷,線程不知是否應該執行,就執行本方的上一個已經執行的語句。如果用if,消費者在等著,兩個生成著一起判斷完flag后,cpu切換到其中一個如t1,另一個t3在wait,當t1喚醒凍結中的一個,是t3(因為它先被凍結的,就會先被喚醒),所以t3未經判斷,又生產了一個。而沒消費。
2)這里使用的是signal方法,而不是signalAll方法。是因為通過Condition的兩個對象,分別喚醒對方,這就體現了Lock鎖機制的靈活性。可以通過Contidition對象調用Lock接口中的方法,就可以保證多線程間通信的流暢性了。
對于多線程的知識,還需要慢慢積累,畢竟線程通信可以提高程序運行的效率,這樣就可以讓程序得到很大的優化。期待新知識······
八、線程池
1、簡述:
預先創建好一些線程,任務提交時直接執行,既可以節約創建線程的時間,又可以控制線程的數量。
2、線程池的好處
- 降低資源消耗,通過池化思想,減少創建線程和銷毀線程的消耗,控制資源
- 提高響應速度,任務到達時,無需創建線程即可運行
- 提供更多更強大的功能,可擴展性高
3、線程池的主要流程
流程包括:線程池創建、接收任務、執行任務、回收線程的步驟
線程池的構造函數:
public ThreadPoolExecutor(int corePoolSize, //核心線程數
int maximumPoolSize, //最大線程數
long keepAliveTime, //救急線程的空閑時間
TimeUnit unit, //救急線程的空閑時間單位
BlockingQueue<Runnable> workQueue, //阻塞隊列
ThreadFactory threadFactory, //創建線程的工廠,主要定義線程名
RejectedExecutionHandler handler //拒絕策略
) {
//......
}
- 流程:
1、創建線程池后,線程池的狀態是RUNNABLE,該狀態下才能有下面的步驟
2、提交任務時,線程池會創建線程去處理任務
3、當線程池的工作線程數達到corePoolSize時,繼續提交任務會進入阻塞隊列
4、當阻塞隊列裝滿時,繼續提交任務,會創建救急線程來處理
5、當線程池中的工作線程數達到maximumPoolSize時,會執行拒絕策略
6、當線程取任務的時間達到keepAliveTime還沒有取到任務,工作線程數大于corePoolSize時,會回收該線程
- 注意: 不是剛創建的線程是核心線程,后面創建的線程是非核心線程;線程是沒有核心非核心的概念的。
- 拒絕策略
1、調用者拋出RejectedExecutionException (默認策略)
2、讓調用者運行任務
3、丟棄此次任務
4、丟棄阻塞隊列中最早的任務,加入該任務
- 提交任務的方法
// 執行Runnable
public void execute(Runnable command) {
}
// 提交Callable
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
// 內部構建FutureTask
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
// 提交Runnable,指定返回值
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 內部構建FutureTask
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
// 提交Runnable,指定返回值
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
// 內部構建FutureTask
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
4、Execetors創建線程池
-
newFixedThreadPool
(定長線程池)
核心線程數 = 最大線程數 沒有救急線程
阻塞隊列無界 可能導致oom
可控制線程最大并發數,超出的線程會在隊列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
newCachedThreadPool
(可緩存線程池)
核心線程數是0,最大線程數無限制 ,救急線程60秒回收
隊列采用 SynchronousQueue 實現,沒有容量,即放入隊列后沒有線程來取就放不進去
可能導致線程數過多,cpu負擔太大
如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
-
newSingleThreadExecutor
(單線程化的線程池)
核心線程數和最大線程數都是1,沒有救急線程,無界隊列 可以不停的接收任務
將任務串行化 一個個執行, 使用包裝類是為了屏蔽修改線程池的一些參數 比如 corePoolSize
如果某線程拋出異常了,會重新創建一個線程繼續執行
可能造成oom
用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行
現行大多數GUI程序都是單線程的。Android中單線程可用于數據庫操作,文件操作,應用批量安裝,應用批量刪除等不適合并發但可能IO阻塞性及影響UI線程響應的操作。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
-
newScheduledThreadPool
(定長線程池)
任務調度的線程池。可以指定延遲時間調用,可以指定隔一段時間調用
支持定時及周期性任務執行
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
5、線程池的關閉
shutdown()
會讓線程池狀態為shutdown,不能接收任務,但是會將工作線程和阻塞隊列里的任務執行完 相當于優雅關閉shutdownNow()
會讓線程池狀態為stop, 不能接收任務,會立即中斷執行中的工作線程,并且不會執行阻塞隊列里的任務, 會返回阻塞隊列的任務列表
6、線程池的使用
- 配置參數:
- cpu密集型 : 指的是程序主要發生cpu的運算
核心線程數 = CPU核心數+1- IO密集型:遠程調用RPC,操作數據庫等,不需要使用cpu進行大量的運算。 大多數應用的場景
核心線程數 = 核數cpu期望利用率總時間 / cpu運算時間