java之多執行緒
1、執行緒概述
幾乎所有的作業系統都支援同時執行多個任務,一個任務通常就是一個程式,每個執行中的程式就是一個程序。當一個程式執行時,內部可能包含了多個順序執行流,每個順序執行流就是一個執行緒。
2、執行緒與程序
程序概述:
幾乎所有的作業系統都支援程序的概念,所有執行中的任務通常對應一個程序( Process)。當一個程式進入記憶體執行時,即變成一個程序。程序是處於執行過程中的程式,並且具有一定的獨立功能,程序是系統進行資源分配和排程的一個獨立單位。
程序特徵:
1、獨立性:程序是系統中獨立存在的實體,它可以擁有自己獨立的資源,每一個程序都擁有自己私有的地址空間。在沒有經過程序本身允許的情況下,一個使用者程序不可以直接訪問其他程序的地址空間
2、動態性:程序與程式的區別在於,程式只是一個靜態的指令集合,而程序是一個正在系統中活動的指令集合。在程序中加入了時間的概念。程序具有自己的生命週期和各種不同的狀態,這些概念在程式中都是不具備的
3、併發性:多個程序可以在單個處理器上併發執行,多個程序之間不會互相影響。
執行緒:
執行緒與程序相似,但執行緒是一個比程序更小的執行單位。一個程序在其執行的過程中可以產生多個執行緒。與程序不同的是同類的多個執行緒共享同一塊記憶體空間和一組系統資源,所以系統在產生一個執行緒,或是在各個執行緒之間作切換工作時,負擔要比程序小得多,也正因為如此,執行緒也被稱為輕量級程序。
併發和並行:
併發:同一時刻只能有一條指令執行,但多個程序指令被快速輪換執行
並行:同一時刻,有多條指令在多個處理器上同時執行
多執行緒:
概述:
多執行緒就是幾乎同時執行多個執行緒(一個處理器在某一個時間點上永遠都只能是一個執行緒!即使這個處理器是多核的,除非有多個處理器才能實現多個執行緒同時執行。)。幾乎同時是因為實際上多執行緒程式中的多個執行緒實際上是一個執行緒執行一會然後其他的執行緒再執行,並不是很多書籍所謂的同時執行。
多執行緒優點:
1、程序之間不能共享記憶體,但執行緒之間共享記憶體非常容易。
2、系統建立程序時需要為該程序重新分配系統資源,但建立執行緒則代價小得多,因此使用多執行緒來實現多工併發比多程序的效率高
3、Java語言內建了多執行緒功能支援,而不是單純地作為底層作業系統的排程方式,從而簡化了Java的多執行緒程式設計
3、使用多執行緒:
多執行緒的建立:
(1)、繼承Thread類:
第一步:定義Thread類的之類,並重寫run方法,該run方法的方法體就代表了執行緒需要執行的任務
第二步:建立Thread類的例項
第三步:呼叫執行緒的start()方法來啟動執行緒
public class FirstThread extends Thread { private int i; public void run() { for(;i<100;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) { for(int i=0;i<100;i++) { //呼叫Thread的currentThread方法獲取當前執行緒 System.out.println(Thread.currentThread().getName()+" "+i); if(i==20) { new FirstThread().start(); new FirstThread().start(); } } } }
(2)、實現Runnable介面:
第一步:定義Runnable介面的實現類,並重寫該介面的run方法,該run方法同樣是執行緒需要執行的任務
第二步:建立Runnable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件
public class SecondThread implements Runnable { private int i; @Override public void run() { for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { for(int i=0;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); if(i==20) { SecondThread s1=new SecondThread(); new Thread(s1,"新執行緒1").start();; new Thread(s1,"新執行緒2").start(); } } } }
(3)、使用Callable和Future建立執行緒
細心的讀者會發現,上面建立執行緒的兩種方法。繼承Thread和實現Runnable介面中的run都是沒有返回值的。於是從Java5開始,Java提供了Callable介面,該介面是Runnable介面的增強版。Callable介面提供了一個call()方法可以作為執行緒執行體,但call()方法比run()方法功能更強大。
建立並啟動有返回值的執行緒的步驟如下:
第一步:建立 Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,且該call()方法有返回值,再建立 Callable實現類的例項。從Java8開始,可以直接使用 Lambda表示式建立 Callable物件
第二步:使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call方法的返回值
第三步:使用FutureTask物件作為Thread物件的target建立並啟動新執行緒
第四步:通過FutureTask的get()方法獲得子執行緒執行結束後的返回值
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class ThirdThread { public static void main(String[] args) { //ThirdThread rt=new ThirdThread(); FutureTask<Integer> task=new FutureTask<Integer>((Callable<Integer>)()->{ int i=0; for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+"的迴圈變數i"+i); } return i; }) ; for(int i=0;i<100;i++) { System.out.println(Thread.currentThread().getName()+"的迴圈變數i為"+i); if(i==20) { new Thread(task,"有返回值的執行緒").start();; } } try { System.out.println("子執行緒的返回值"+task.get()); }catch(Exception e) { e.printStackTrace(); } } }
建立執行緒的三種方式的對比:
採用Runnable、Callable介面的方式建立多執行緒的優缺點:
優點:
1、執行緒類只是實現了 Runnable介面或 Callable介面,還可以繼承其他類
2、在這種方式下,多個執行緒可以共享同一個 target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、程式碼和資料分開,形成清晰的模型,較好地體現了面向物件的思想。
缺點:
程式設計稍稍複雜,如果需要訪問當前執行緒,則必須使用Thread.currentThread()方法。
採用繼承 Thread類的方式建立多執行緒的優缺點:
優點:
編寫簡單,如果需要訪問當前執行緒,則無須使用 Thread.current Thread()方法,直接使用this即可獲得當前執行緒
缺點:
因為執行緒已經繼承了Thread類,所以不能再繼承其他類
執行緒的生命週期:
新建和就緒狀態:
當程式使用new關鍵字建立一個執行緒後,該執行緒就處於新建狀態。
當執行緒物件呼叫了start()方法後,該執行緒就處於就緒狀態。
執行和阻塞狀態:
如果處於就緒狀態的執行緒獲取了CPU,開始執行run()方法的執行緒執行體,則該執行緒處於執行狀態。
當執行緒呼叫sleep(),呼叫一個阻塞式IO方法,執行緒會被阻塞
死亡狀態:
1、run()或者call()方法執行完成,執行緒正常結束
2、執行緒丟擲一個未捕獲的Exception或Error
3、直接呼叫該執行緒的stop方法來結束該執行緒——該方法容易導致死鎖,不推薦使用
執行緒狀態轉化圖
4、控制執行緒:
(1)、join執行緒
Thread提供了讓一個執行緒等待另一個執行緒完成的方法——join方法。當在某個程式執行流中呼叫其直到被 join方法加入的join執行緒執行完為止
public class JoinThread extends Thread { //提供一個有引數的構造器,用於設定該執行緒的名字 public JoinThread(String name) { super(name); } //重寫run方法,定義執行緒體 public void run() { for(int i=0;i<10;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) throws InterruptedException { //啟動子執行緒 new JoinThread("新執行緒").start(); for(int i=0;i<10;i++) { if(i==5) { JoinThread jt=new JoinThread("被join的執行緒"); jt.start(); //main執行緒呼叫了jt執行緒的join方法,main執行緒 //必須等jt執行結束才會向下執行 jt.join(); } System.out.println(Thread.currentThread().getName()+" "+i); } } }
執行結果:
main 0 main 1 main 2 main 3 main 4 新執行緒 0 新執行緒 1 新執行緒 2 新執行緒 3 被join的執行緒 0 新執行緒 4 被join的執行緒 1 新執行緒 5 被join的執行緒 2 新執行緒 6 被join的執行緒 3 新執行緒 7 被join的執行緒 4 新執行緒 8 被join的執行緒 5 新執行緒 9 被join的執行緒 6 被join的執行緒 7 被join的執行緒 8 被join的執行緒 9 main 5 main 6 main 7 main 8 main 9
(2)、後臺執行緒:
有一種執行緒,它是在後臺執行的,它的任務是為其他的執行緒提供服務,這種執行緒被稱為“後臺執行緒( Daemon Thread)”,又稱為“守護執行緒”或“精靈執行緒”。JVM的垃圾回收執行緒就是典型的後臺執行緒。
後臺執行緒有個特徵:如果所有的前臺執行緒都死亡,後臺執行緒會自動死亡。
呼叫 Thread物件的 setDaemon(true)方法可將指定執行緒設定成後臺執行緒。下面程式將執行執行緒設定成後臺執行緒,可以看到當所有的前臺執行緒死亡時,後臺執行緒隨之死亡。當整個虛擬機器中只剩下後臺執行緒時,程式就沒有繼續執行的必要了,所以虛擬機器也就退出了。
public class DaemonThread extends Thread { //定義後臺執行緒的執行緒體與普通執行緒沒有什麼區別 public void run() { for(int i=0;i<1000;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) { DaemonThread t=new DaemonThread(); //將此執行緒設定為後臺執行緒 t.setDaemon(true); t.start(); for(int i=0;i<10;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } //程式到此執行結束,前臺執行緒(main)結束,後臺執行緒也隨之結束 } }
執行結果:
main 0 Thread-0 0 main 1 Thread-0 1 Thread-0 2 main 2 Thread-0 3 Thread-0 4 Thread-0 5 main 3 main 4 Thread-0 6 main 5 Thread-0 7 Thread-0 8 main 6 main 7 main 8 Thread-0 9 main 9 Thread-0 10 Thread-0 11 Thread-0 12 Thread-0 13 Thread-0 14 Thread-0 15 Thread-0 16 Thread-0 17 Thread-0 18 Thread-0 19 Thread-0 20 Thread-0 21
(3)、執行緒睡眠:
如果需要讓當前正在執行的執行緒暫停一段時間,並進入阻塞狀態,則可以通過呼叫 Thread類的靜態 sleep方法來實現。 sleep方法有兩種過載形式
static void sleep(long millis):讓當前正在執行的執行緒暫停millis毫秒,並進入阻塞狀態
static void sleep(long millis,int nanos):讓當前正在執行的執行緒暫停millis毫秒加上nanos毫微秒,並進入阻塞狀態,通常我們不會精確到毫微秒,所以該方法不常用
import java.util.Date; public class SleepTest { public static void main(String[] args) throws InterruptedException { for(int i=0;i<10;i++) { System.out.println("當前時間"+new Date()); Thread.sleep(1000); } } }
(4)、改變執行緒優先順序:
每個執行緒執行時都有一定的優先順序,優先順序高的執行緒獲得較多的執行機會,優先順序低的執行緒則獲得較少的執行機會。
每個執行緒預設的優先順序都與建立它的父執行緒的優先順序相同,在預設情況下,main執行緒具有普通優先順序,由main執行緒建立的子執行緒也具有普通優先順序。
Thread類提供了 setPriority(int newPriority)、 getPriority()方法來設定和返回指定執行緒的優先順序,其中 setPriority()方法的引數可以是一個整數,範圍是1-10之間,也可以使用 Thread類的如下三個靜態常量
MAX_PRIORITY:其值是10
MIN_PRIORITY:其值時1
NORM_PRIPRITY:其值是5
public class PriorityTest extends Thread { //定義一個構造器,用於建立執行緒時傳入執行緒的名稱 public PriorityTest(String name) { super(name); } public void run() { for(int i=0;i<50;i++) { System.out.println(getName()+",其優先順序是:"+getPriority()+"迴圈變數的值:"+i); } } public static void main(String[] args) { //改變主執行緒的優先順序 Thread.currentThread().setPriority(6); for(int i=0;i<30;i++) { if(i==10) { PriorityTest low=new PriorityTest("低階"); low.start(); System.out.println("建立之初的優先順序:"+low.getPriority()); //設定該執行緒為最低優先順序 low.setPriority(Thread.MIN_PRIORITY); } if(i==20) { PriorityTest high=new PriorityTest("高階"); high.start(); System.out.println("建立之初的優先順序"+high.getPriority()); high.setPriority(Thread.MAX_PRIORITY); } } } }
5、執行緒同步:
(1)、執行緒安全問題:
現有如下程式碼:
public class Account { private String accountNo; private double balance; public Account() {} public Account(String accountNo, double balance) { super(); this.accountNo = accountNo; this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public int hashCode() { return accountNo.hashCode(); } public boolean equals(Object obj) { if(this==obj) { return true; } if(obj!=null&&obj.getClass()==Account.class) { Account target=(Account)obj; return target.getAccountNo().equals(accountNo); } return false; } }
import com.alibaba.util.Account; public class DrawThread extends Thread{ //模擬使用者賬戶 private Account account; //當前取錢執行緒所希望的錢數 private double drawAmount; public DrawThread(String name,Account account,double drawAmount) { super(name); this.account=account; this.drawAmount=drawAmount; } //多個執行緒修改同一個共享資料,可能發生執行緒安全問題 @Override public void run() { if(account.getBalance()>drawAmount) { System.out.println(getName()+"取錢成功"+" "+drawAmount); try { Thread.sleep(1); }catch(Exception e) { e.printStackTrace(); } account.setBalance(account.getBalance()-drawAmount); System.out.println("\t餘額為"+" "+account.getBalance()); }else { System.out.println("餘額不足,取錢失敗"); } } }
import com.alibaba.util.Account; public class DrawTest { public static void main(String[] args) { Account account=new Account("1234567",1000); //模擬兩個執行緒同時操作賬號 new DrawThread("甲", account, 800).start();; new DrawThread("乙", account, 800).start();; } }
現在我們來分析一下以上程式碼:
我們現在希望實現的操作是模擬多個使用者同時從銀行賬戶裡面取錢,如果使用者取錢數小於等於當前賬戶餘額,則提示取款成功,並將餘額減去取款錢數,如果餘額不足,則提示餘額不足,取款失敗。
Account 類:銀行賬戶類,裡面有一些賬戶的基本資訊,以及操作賬戶資訊的方法
DrawThread類:繼承了Thread,是一個多執行緒類,用於模擬多個使用者操作同一個賬戶的資訊
DrawTest:測試類
這時我們執行程式可能會看到如下執行結果:
甲取錢成功 800.0 乙取錢成功 800.0 餘額為 200.0 餘額為 -600.0
餘額竟然為-600,餘額不足也能取出錢來,這就是執行緒安全問題。因為執行緒排程的不確定性,出現了偶然的錯誤。
(2)、如何解決執行緒安全問題:
①、同步程式碼塊:
為了解決執行緒問題,Java的多執行緒支援引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步程式碼塊。同步程式碼塊的語法格式如下:
synchronized(obj){
//此處的程式碼就是同步程式碼塊
}
我們將上面銀行中DrawThread類作如下修改:
import com.alibaba.util.Account; public class DrawThread extends Thread{ //模擬使用者賬戶 private Account account; //當前取錢執行緒所希望的錢數 private double drawAmount; public DrawThread(String name,Account account,double drawAmount) { super(name); this.account=account; this.drawAmount=drawAmount; } //多個執行緒修改同一個共享資料,可能發生執行緒安全問題 @Override public void run() { //使用account作為同步監視器,任何執行緒在進入下面同步程式碼塊之前 //必須先獲得account賬戶的鎖定,其他執行緒無法獲得鎖,也就無法修改它 //這種做法符合:"加鎖-修改-釋放鎖"的邏輯 synchronized(account) { if(account.getBalance()>drawAmount) { System.out.println(getName()+"取錢成功"+" "+drawAmount); try { Thread.sleep(1); }catch(Exception e) { e.printStackTrace(); } account.setBalance(account.getBalance()-drawAmount); System.out.println("\t餘額為"+" "+account.getBalance()); }else { System.out.println("餘額不足,取錢失敗"); } } } }
我們來看這次的執行結果:
甲取錢成功 800.0 餘額為 200.0 餘額不足,取錢失敗
我們發現結果變了,是我們希望看到的結果。因為我們在可能發生執行緒安全問題的地方加上了synchronized程式碼塊
②:同步方法:
與同步程式碼塊對應,Java的多執行緒安全支援還提供了同步方法,同步方法就是使用 synchronized關鍵字來修飾某個方法,則該方法稱為同步方法。對於 synchronized修飾的例項方法(非 static方法)而言,無須顯式指定同步監視器,同步方法的同步監視器是this,也就是呼叫該方法的物件。同步方法語法格式如下:
public synchronized void 方法名(){
//具體程式碼
}
③、同步鎖:
從Java5開始,Java提供了一種功能更強大的執行緒同步機制—一通過顯式定義同步鎖物件來實現同步,在這種機制下,同步鎖由Lock物件充當。
Lock提供了比 synchronized方法和 synchronized程式碼塊更廣泛的鎖定操作,Lock允許實現更靈活的結構,可以具有差別很大的屬性,並且支援多個相關的 Condition物件。
在實現執行緒安全的控制中,比較常用的是 ReentrantLock(可重入鎖)。使用該Lock物件可以顯式加鎖、釋放鎖,通常使用ReentrantLock的程式碼格式如下:
class X{ //定義鎖物件 private final ReentrantLock lock=new ReentrantLock(); //... //定義需要保護執行緒安全的方法 public void m() { //加鎖 lock.lock(); try { //需要保證執行緒安全的程式碼 //...method body }finally { //釋放鎖 lock.unlock(); } } }
死鎖:
當兩個執行緒相互等待對方釋放同步監視器時就會發生死鎖,Java虛擬機器沒有監測,也沒有采取措施來處理死鎖情況,所以多執行緒程式設計時應該採取措施避免死鎖岀現。一旦岀現死鎖,整個程式既不會發生任何異常,也不會給出任何提示,只是所有執行緒處於阻塞狀態,無法繼續。
死鎖是很容易發生的,尤其在系統中出現多個同步監視器的情況下,如下程式將會出現死鎖
class A{ public synchronized void foo(B b) { System.out.println("當前執行緒名:"+Thread.currentThread().getName()+"進入A例項的foo方法");//① try { Thread.sleep(200); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println("當前執行緒名:"+Thread.currentThread().getName()+"企圖呼叫B的方法");//③ 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 e) { e.printStackTrace(); } System.out.println("當前執行緒名:"+Thread.currentThread().getName()+"企圖呼叫A的方法");//④ 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(b); System.out.println("進入了主執行緒之後"); } @Override public void run() { Thread.currentThread().setName("副執行緒"); b.bar(a); System.out.println("進入副執行緒之後"); } public static void main(String[] args) { DeadLock d=new DeadLock(); new Thread(d).start(); d.init(); } }
執行結果:
從圖中可以看出,程式既無法向下執行,也不會丟擲任何異常,就一直“僵持”著。究其原因,是因為:上面程式中A物件和B物件的方法都是同步方法,也就是A物件和B物件都是同步鎖。程式中兩個執行緒執行,副執行緒的執行緒執行體是 DeadLock類的run()方法,主執行緒的執行緒執行體是 Deadlock的main()方法(主執行緒呼叫了init()方法)。其中run()方法中讓B物件呼叫b進入foo()方法之前,該執行緒對A物件加鎖—當程式執行到①號程式碼時,主執行緒暫停200ms:CPU切換到執行另一個執行緒,讓B物件執行bar()方法,所以看到副執行緒開始執行B例項的bar()方法,進入bar()方法之前,該執行緒對B物件加鎖——當程式執行到②號程式碼時,副執行緒也暫停200ms:接下來主執行緒會先醒過來,繼續向下執行,直到③號程式碼處希望呼叫B物件的last()方法——執行該方法之前必須先對B物件加鎖,但此時副執行緒正保持著B物件的鎖,所以主執行緒阻塞;接下來副執行緒應該也醒過來了,繼續向下執行,直到④號程式碼處希望呼叫A物件的 last()方法——執行該方法之前必須先對A物件加鎖,但此時主執行緒沒有釋放對A物件的鎖——至此,就出現了主執行緒保持著A物件的鎖,等待對B物件加鎖,而副執行緒保持著B物件的鎖,等待對A物件加鎖,兩個執行緒互相等待對方先釋放,所以就出現了死鎖。
6、執行緒池:
系統啟動一個新執行緒的成本是比較高的,因為它涉及與作業系統互動。在這種情形下,使用執行緒池可以很好地提高效能,尤其是當程式中需要建立大量生存期很短暫的執行緒時,更應該考慮使用執行緒池。
與資料庫連線池類似的是,執行緒池在系統啟動時即建立大量空閒的執行緒,程式將一個 Runnable物件或 Callable物件傳給執行緒池,執行緒池就會啟動一個空閒的執行緒來執行它們的run()或call()方法,當run()或call()方法執行結束後,該執行緒並不會死亡,而是再次返回執行緒池中成為空閒狀態,等待執行下一個Runnable物件的run()或call()方法。
建立執行緒池的幾個常用的方法:
1.newSingleThreadExecutor
建立一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。
2.newFixedThreadPool
建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。
3.newCachedThreadPool
建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,
那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。
4.newScheduledThreadPool
建立一個大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolTest { public static void main(String[] args) { ExecutorService pool=Executors.newFixedThreadPool(6); Runnable target=()->{ for(int i=0;i<10;i++) { System.out.println(Thread.currentThread().getName()+"的i的值"+i); } }; pool.submit(target); pool.submit(target); pool.submit(target); //關閉執行緒池 pool.shutdown(); } }
執行結果:
pool-1-thread-1的i的值0 pool-1-thread-2的i的值0 pool-1-thread-3的i的值0 pool-1-thread-2的i的值1 pool-1-thread-1的i的值1 pool-1-thread-2的i的值2 pool-1-thread-3的i的值1 pool-1-thread-2的i的值3 pool-1-thread-1的i的值2 pool-1-thread-2的i的值4 pool-1-thread-3的i的值2 pool-1-thread-2的i的值5 pool-1-thread-1的i的值3 pool-1-thread-2的i的值6 pool-1-thread-3的i的值3 pool-1-thread-2的i的值7 pool-1-thread-1的i的值4 pool-1-thread-2的i的值8 pool-1-thread-3的i的值4 pool-1-thread-2的i的值9 pool-1-thread-1的i的值5 pool-1-thread-3的i的值5 pool-1-thread-1的i的值6 pool-1-thread-1的i的值7 pool-1-thread-1的i的值8 pool-1-thread-1的i的值9 pool-1-thread-3的i的值6 pool-1-thread-3的i的值7 pool-1-thread-3的i的值8 pool-1-thread-3的i的值9
&n