現在越來越多的公司,對精通多執行緒的的人才越來越重視,可見多執行緒技術有多熱門。今天,小編結合清華掃地僧級別大佬的分享,爲大家帶來這篇多執行緒的總結,希望大家能夠喜歡。
幾乎所有的操作系統都支援同時執行多個任務,一個任務通常就是一個程式,每個執行中的程式就是一個進程。當一個程式執行時,內部可能包含了多個順序執行流,每個順序執行流就是一個執行緒。
幾乎所有的操作系統都支援進程的概念,所有執行中的任務通常對應一個進程( Process)。當一個程式進入記憶體執行時,即變成一個進程。進程是處於執行過程中的程式,並且具有一定的獨立功能,進程是系統進行資源分配和排程的一個獨立單位。
1、獨立性:進程是系統中獨立存在的實體,它可以擁有自己獨立的資源,每一個進程都擁有自己私有的地址空間。在沒有經過進程本身允許的情況下,一個使用者進程不可以直接存取其他進程的地址空間
2、動態性:進程與程式的區別在於,程式只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念。進程具有自己的生命週期和各種不同的狀態,這些概念在程式中都是不具備的
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方法來結束該執行緒——該方法容易導致死鎖,不推薦使用
執行緒狀態轉化圖
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
有一種執行緒,它是在後台執行的,它的任務是爲其他的執行緒提供服務,這種執行緒被稱爲「後臺執行緒( 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
如果需要讓當前正在執行的執行緒暫停一段時間,並進入阻塞狀態,則可以通過呼叫 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);
}
}
}
每個執行緒執行時都有一定的優先順序,優先順序高的執行緒獲得較多的執行機會,優先順序低的執行緒則獲得較少的執行機會。
每個執行緒預設的優先順序都與建立它的父執行緒的優先順序相同,在預設情況下,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);
}
}
}
}
現有如下程式碼:
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
①、同步程式碼塊:
爲了解決執行緒問題,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物件加鎖,兩個執行緒互相等待對方先釋放,所以就出現了死鎖。
系統啓動一個新執行緒的成本是比較高的,因爲它涉及與操作系統互動。在這種情形下,使用執行緒池可以很好地提高效能,尤其是當程式中需要建立大量生存期很短暫的執行緒時,更應該考慮使用執行緒池。
與數據庫連線池類似的是,執行緒池在系統啓動時即建立大量空閒的執行緒,程式將一個 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
怎麼樣?來自清華掃地僧的功力如此深厚,想瞭解是哪位嗎?那就請多多留言評論,或者關注小編,私信回覆 回復【學習】,即可免費獲贈清華大佬海量學習資料。
心動就趕緊行動起來吧~~~