多執行緒環境下,我們可以通過加鎖(互斥)來同步各個執行緒之間的行為,使得共享資源被正確的訪問!同步的方法有很多種,本章主要介紹Java提供的wait,notify和sleep方法來實現這種協作!附一張執行緒狀態轉換圖
- wait方法是基類Object中的,釋放當前執行緒所佔的物件的鎖,將執行緒由執行狀態轉為等待狀態,即將當前執行緒扔入目標物件的等待池中
- notify方法是基類Object中的,將目標物件的等待池隨即喚醒一個執行緒,將喚醒的執行緒扔進目標物件的鎖池,然後去競爭該物件的鎖!
- notifyAll方法是基類Object中的,將目標物件的等待池喚醒全部執行緒,將喚醒的全部執行緒扔進目標物件的鎖池,然後去競爭該物件的鎖!
ok 通過以上的描述我們先來解決兩個問題:
鎖池和等待池的概念
鎖池:假設執行緒A已經擁有了某個物件(注意:不是類)的鎖,而其它的執行緒想要呼叫這個物件的某個synchronized方法(或者synchronized塊),由於這些執行緒在進入物件的synchronized方法之前必須先獲得該物件的鎖的擁有權,但是該物件的鎖目前正被執行緒A擁有,所以這些執行緒就進入了該物件的鎖池中。
等待池:假設一個執行緒A呼叫了某個物件的wait()方法,執行緒A就會釋放該物件的鎖後,進入到了該物件的等待池中
notify和notifyALL的區別
notify和notifyAll都是將等待池中的執行緒轉移到鎖池中,去競爭物件的鎖!最大的區別在於notify是隨機喚醒一個執行緒,而notifyAll是喚醒全部的等待池中的執行緒!
試想一種情況,有一個生產者A和兩個消費者B與C,某時刻只有消費者B消費資源,而生產者A和消費者C則處於wait狀態,即進入物件的等待池中。假如此時B恰好消費完資源,此時如果執行的是notify的方法,ok,又恰好喚醒了消費者執行緒C,導致C因沒有資源而活活餓死(即進入等待池中,此時鎖池是空的!因為生產者A是在等待池中!)此時如果執行的是notifyAll方法呢?那就不一樣了,就算B消耗沒了資源,在執行notifyAll之後會將A和C一併轉入鎖池中!!!生產者此時是在鎖池中的!
從以上區別及對情況的分析我們可以得出以下結論
在多生產者和多消費者的環境下,不能用notify,因為根據分析可能會導致執行緒餓死。但是在一個生產者和一個消費者的情況下是沒問題的!
這裡主要通過生產者消費者模型來介紹wait,notify和notifyAll的用法
wait和notify以及notifyAll一般都是組合出現的,因為wait之後的執行緒不能自己改變狀態,必須依賴於其他執行緒呼叫notify或者notifyAll方法!
話不多說,上程式碼:
package ddx.多執行緒;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
//生產者消費者問題
public class Bounded_Buffer_roblem {
public static void main(String[] args){
Food food = new Food(3);
ExecutorService pro_executor = Executors.newCachedThreadPool(); //消費者執行緒池
ExecutorService con_executor = Executors.newCachedThreadPool(); //生產者執行緒池
for(int i = 0;i <3;i++){
pro_executor.execute(new producer(food));
con_executor.execute(new consumer(food));
}
try{
TimeUnit.SECONDS.sleep(2);
}catch (Exception e){
e.printStackTrace();
}
con_executor.shutdown();
pro_executor.shutdown();
}
}
//資源
class Food{
private int count; //當前食物總量
private final int MAX_COUNT = 5; //最大食物量
Food(int count){
this.count = count;
}
//減少食物
public void food_down(){
if(!isEmpty()) {
count--;
}
}
//新增食物
public void food_up(){
if(!isFull()) {
count++;
}
}
public int getCount(){
return this.count;
}
//食物滿了嗎
public boolean isFull(){
return count == MAX_COUNT;
}
//食物空著嗎?
public boolean isEmpty(){
return count == 0 ;
}
}
//生產者
class consumer implements Runnable{
private Food food;
int i =20;
consumer(Food food){
this.food = food;
}
public void consume(){
try {
synchronized (food) {
while (food.isEmpty()) {
food.wait();
}
food.food_down();
System.out.println("消費者" + Thread.currentThread().getName() + "正在消費 1個食物,目前食物剩餘量" + food.getCount());
food.notifyAll();
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
while(i-->=0) {
consume();
}
}
}
//消費者
class producer implements Runnable{
private Food food;
int i = 20;
producer(Food food){
this.food = food;
}
public void produce(){
try {
synchronized (food) { //注意此處上鎖的物件!不是this!因為this是給當前這個執行緒物件上鎖!而不是給目標資源food上鎖!如果用的是this,那麼誰來喚醒這個執行緒呢?沒有其他執行緒擁有這個執行緒物件的鎖,因而也就沒執行緒喚醒,最終導致所有執行緒餓死!
while (food.isFull()) {
food.wait();
}
food.food_up();
System.out.println("生產者" + Thread.currentThread().getName() + "正在生產 1個食物,目前食物剩餘量" + food.getCount());
food.notifyAll();
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void run() {
while(i-->=0) {
produce();
}
}
}
當談及wait方法時,一般和sleep方法一起比較。
wait,notify和notifyAll方法是屬於基類Object的,而sleep方法則屬於Thread方法!
為什麼執行緒有關的方法要放在基類中呢?
因為wait,notify和notifyAll方法操作的鎖也是所有物件的一部分。所以你可以將這些方法放在任何一個同步方法或者是同步程式碼塊中,而不必去考慮他是否繼承自Thread類或者實現了Runnable介面!
wait方法必須放在同步程式碼塊或同步控制方法中,並且由指定加鎖物件呼叫!而sleep方法則無所謂,無需操作鎖,所以放在哪都可以
public static void main(String[] args) {
new test1().func();
}
Object obj = new Object();
public void func(){
synchronized (obj){
try {
obj.wait(1100);
//wait(1100);
//報錯! IllegalMonitorStateException!因為這種呼叫方法隱式的指明是由當前這個類的物件呼叫的,即this.wait(100),而非指定obj物件呼叫,所以出錯!
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
wait方法本質上是將當前執行緒持有的鎖釋放,並將自己轉為等待狀態(超時等待)。而sleep方法則不操作鎖(即對鎖的狀態不變),只是將當前執行緒休眠,轉為超時等待狀態
wait方法轉為等待狀態(或者指定時間轉為超時等待狀態)之後,必須通過notify或者notifyAll方法將其喚醒!而sleep方法不需額外的操作,經過指定時間之後就自動返回執行狀態!
2.1 join方法
join方法的主要作用就是同步,它可以使得執行緒之間的並行執行變為序列執行。在A執行緒中呼叫了B執行緒的join()方法時,表示只有當B執行緒執行完畢時,A執行緒才能繼續執行。
先對join進行下理解,以下是join的原始碼
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) { //如果沒有指定等待時間,則預設為0
while (isAlive()) { //如果當前執行緒處於執行狀態,則進入當前執行緒物件的等待池!
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
通過原始碼我們可以發現,其實sleep方法最終是呼叫wait方法的,那麼問題來了,誰來喚醒呢?
good question ! 是Java虛擬機器喚醒的!
// 位於/hotspot/src/share/vm/runtime/thread.cpp中
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
// Notify waiters on thread object. This has to be done after exit() is called
// on the thread (if the thread is the last thread in a daemon ThreadGroup the
// group should have the destroyed bit set before waiters are notified).
ensure_join(this);
}
static void ensure_join(JavaThread* thread) {
// We do not need to grap the Threads_lock, since we are operating on ourself.
Handle threadObj(thread, thread->threadObj());
assert(threadObj.not_null(), "java thread object must exist");
ObjectLocker lock(threadObj, thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
// Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
// Clear the native thread instance - this makes isAlive return false and allows the join()
// to complete once we've done the notify_all below
java_lang_Thread::set_thread(threadObj(), NULL);
//重點在這!當這個thread執行結束之後,將獲得這個threa物件的鎖的執行緒喚醒,也就是主執行緒!!!
lock.notify_all(thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}
理解了這些,我們就可以實現這個問題啦,程式碼如下:
package ddx.多執行緒;
public class wait_main {
public static void main(String[] args){
Thread[] threads = new Thread[10];
for(int i = 0;i<10;i++){
threads[i] = new Thread(new task());
threads[i].start();
}
for(Thread thread : threads){
try {
thread.join(); //重點在這!!!將所有執行緒都加入當前主執行緒!直到所有執行緒完成,主執行緒才能返回執行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//其實我在這是利用了主執行緒來做的,也可以不用再開一個執行緒,直接在主執行緒中做就好
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("最終執行緒執行");
//do something
System.out.println("最終執行緒結束");
}
}).start();
}
}
class task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在執行!");
try {
for(int i = 0;i<20;i++){
Thread.sleep(5);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "執行結束!");
}
}
執行結果如下:
Thread-0正在執行!
Thread-2正在執行!
Thread-3正在執行!
Thread-1正在執行!
Thread-4正在執行!
Thread-5正在執行!
Thread-6正在執行!
Thread-7正在執行!
Thread-8正在執行!
Thread-9正在執行!
Thread-2執行結束!
Thread-3執行結束!
Thread-1執行結束!
Thread-5執行結束!
Thread-4執行結束!
Thread-0執行結束!
Thread-9執行結束!
Thread-8執行結束!
Thread-7執行結束!
Thread-6執行結束!
最終執行緒執行
最終執行緒結束
由上面的執行結果可以看到,雖然開始順序和結束順序不一樣,但是最終的執行緒都是等到上面執行緒全部結束之後執行的!
2.2 wait/notifyAll
我們可以通過等待通知機制(wait和notifyAll)來實現。即維護一個整型值代表當前正在執行的執行緒的數量,每當執行完一個就自減一,直到為0的時候,通過notifyAll去喚醒最終執行緒!!
話不多說上程式碼:
package ddx.多執行緒;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class wait_main_1 {
private static Object obj = new Object();
private static AtomicInteger count = new AtomicInteger(10); //初始執行緒數
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0;i < 10;i++){
executorService.execute(new task11(count,obj));
}
new Thread() {
@Override
public void run() {
synchronized (obj) {
try {
if(count.get() != 0){ //加個判斷!避免所有執行緒執行完才開啟這個執行緒而導致執行緒永遠阻塞!
obj.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("最終執行緒開始執行");
System.out.println("最終執行緒結束執行");
}
}.start();
executorService.shutdown();
}
}
class task11 implements Runnable{
private Object obj;
private AtomicInteger count;
task11(AtomicInteger count, Object obj){
this.count = count;
this.obj = obj;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "開始執行");
for(int i = 0; i< 3;i++){
System.out.println(Thread.currentThread().getName() + "is running!");
}
synchronized (obj) {
if (count.decrementAndGet() == 0) { //自減一判斷!其實這裡也可以用一個整形值來做,畢竟進入物件obj永遠是互斥的!操作都是原子的!這裡用Java包裝好了的AtomicInteger原子類!
obj.notifyAll();
}
}
System.out.println(Thread.currentThread().getName() + "結束執行" + count);
}
}
執行結果:
pool-1-thread-1開始執行
pool-1-thread-2開始執行
pool-1-thread-2is running!
pool-1-thread-2is running!
pool-1-thread-1is running!
pool-1-thread-1is running!
pool-1-thread-1is running!
pool-1-thread-3開始執行
pool-1-thread-3is running!
pool-1-thread-2is running!
pool-1-thread-3is running!
pool-1-thread-1結束執行9
pool-1-thread-4開始執行
pool-1-thread-5開始執行
pool-1-thread-5is running!
pool-1-thread-5is running!
pool-1-thread-5is running!
pool-1-thread-3is running!
pool-1-thread-2結束執行8
pool-1-thread-3結束執行6
pool-1-thread-5結束執行7
pool-1-thread-4is running!
pool-1-thread-6開始執行
pool-1-thread-4is running!
pool-1-thread-6is running!
pool-1-thread-6is running!
pool-1-thread-6is running!
pool-1-thread-4is running!
pool-1-thread-6結束執行5
pool-1-thread-7開始執行
pool-1-thread-7is running!
pool-1-thread-7is running!
pool-1-thread-7is running!
pool-1-thread-8開始執行
pool-1-thread-8is running!
pool-1-thread-8is running!
pool-1-thread-8is running!
pool-1-thread-8結束執行2
pool-1-thread-4結束執行4
pool-1-thread-10開始執行
pool-1-thread-10is running!
pool-1-thread-10is running!
pool-1-thread-9開始執行
pool-1-thread-7結束執行3
pool-1-thread-9is running!
pool-1-thread-9is running!
pool-1-thread-9is running!
pool-1-thread-9結束執行1
pool-1-thread-10is running!
pool-1-thread-10結束執行0
最終執行緒開始執行
最終執行緒結束執行
2.3 CountDownLatch
CountDownLatch是一個非常實用的多執行緒控制工具類。常用的就下面幾個方法:
CountDownLatch(int count) //例項化一個倒計數器,count指定計數個數
countDown() // 計數減一
await() //等待,當計數減到0時,所有執行緒並行執行
話不多說上演示程式碼
public class test1 {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);
ExecutorService executorService = Executors.newFixedThreadPool(3);
for(int i = 0; i< 3;i++) {
executorService.execute(new process(countDownLatch));
}
try{
System.out.println("主執行緒"+Thread.currentThread().getName()+"等待子執行緒執行完成...");
countDownLatch.await();//阻塞當前執行緒,直到計數器的值為0
System.out.println("主執行緒"+Thread.currentThread().getName()+"開始執行...");
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.shutdown();
}
class process implements Runnable {
CountDownLatch countDownLatch;
process(CountDownLatch countDownLatch){
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " 正在執行:" + i);
}
}catch (Exception e){
}finally {
countDownLatch.countDown();
}
}
}
執行結果
主執行緒main等待子執行緒執行完成...
pool-1-thread-1 正在執行:0
pool-1-thread-1 正在執行:1
pool-1-thread-1 正在執行:2
pool-1-thread-1 正在執行:3
pool-1-thread-1 正在執行:4
pool-1-thread-1 正在執行:5
pool-1-thread-1 正在執行:6
pool-1-thread-1 正在執行:7
pool-1-thread-1 正在執行:8
pool-1-thread-1 正在執行:9
pool-1-thread-1 正在執行:10
pool-1-thread-1 正在執行:11
pool-1-thread-1 正在執行:12
pool-1-thread-1 正在執行:13
pool-1-thread-1 正在執行:14
pool-1-thread-1 正在執行:15
pool-1-thread-1 正在執行:16
pool-1-thread-1 正在執行:17
pool-1-thread-1 正在執行:18
pool-1-thread-1 正在執行:19
pool-1-thread-2 正在執行:0
pool-1-thread-2 正在執行:1
pool-1-thread-3 正在執行:0
pool-1-thread-2 正在執行:2
pool-1-thread-3 正在執行:1
pool-1-thread-2 正在執行:3
pool-1-thread-3 正在執行:2
pool-1-thread-2 正在執行:4
pool-1-thread-3 正在執行:3
pool-1-thread-2 正在執行:5
pool-1-thread-2 正在執行:6
pool-1-thread-2 正在執行:7
pool-1-thread-2 正在執行:8
pool-1-thread-2 正在執行:9
pool-1-thread-2 正在執行:10
pool-1-thread-3 正在執行:4
pool-1-thread-2 正在執行:11
pool-1-thread-2 正在執行:12
pool-1-thread-2 正在執行:13
pool-1-thread-3 正在執行:5
pool-1-thread-3 正在執行:6
pool-1-thread-3 正在執行:7
pool-1-thread-3 正在執行:8
pool-1-thread-3 正在執行:9
pool-1-thread-2 正在執行:14
pool-1-thread-2 正在執行:15
pool-1-thread-3 正在執行:10
pool-1-thread-2 正在執行:16
pool-1-thread-2 正在執行:17
pool-1-thread-2 正在執行:18
pool-1-thread-2 正在執行:19
pool-1-thread-3 正在執行:11
pool-1-thread-3 正在執行:12
pool-1-thread-3 正在執行:13
pool-1-thread-3 正在執行:14
pool-1-thread-3 正在執行:15
pool-1-thread-3 正在執行:16
pool-1-thread-3 正在執行:17
pool-1-thread-3 正在執行:18
pool-1-thread-3 正在執行:19
主執行緒main開始執行...
2.4 ExecutorService
不多比比直接上程式碼
package ddx.多執行緒;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class wait_main_2 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(new task3());
}
executorService.shutdown();
new Thread(() -> {
try {
while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)){
/*
這個awaitTermination(100, TimeUnit.MILLISECONDS)的邏輯是,
在100毫秒的時間內executorService的所有執行緒執行結束時返回true
若超過100毫秒還沒有結束則返回false
通過放一個迴圈攔截所有執行緒池中的任務沒有完成的可能!直到全部完成才返回!!!
其實這裡讓這個執行緒處於迴圈並不是一件好事,畢竟空轉是浪費cpu資源的,其實可以稍微控制下等待時間這樣減少迴圈次數!
*/
}
System.out.println("最終執行緒執行!");
System.out.println("最終執行緒結束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
class task3 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "開始執行");
for (int i = 0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "is running!");
}
System.out.println(Thread.currentThread().getName() + "結束執行");
}
}
執行結果
pool-1-thread-1開始執行
pool-1-thread-1is running!
pool-1-thread-1is running!
pool-1-thread-1is running!
pool-1-thread-1is running!
pool-1-thread-1結束執行
pool-1-thread-2開始執行
pool-1-thread-2is running!
pool-1-thread-2is running!
pool-1-thread-2is running!
pool-1-thread-2is running!
pool-1-thread-2結束執行
pool-1-thread-3開始執行
pool-1-thread-3is running!
pool-1-thread-3is running!
pool-1-thread-3is running!
pool-1-thread-3is running!
pool-1-thread-3結束執行
pool-1-thread-4開始執行
pool-1-thread-4is running!
pool-1-thread-4is running!
pool-1-thread-4is running!
pool-1-thread-4is running!
pool-1-thread-4結束執行
pool-1-thread-6開始執行
pool-1-thread-6is running!
pool-1-thread-6is running!
pool-1-thread-6is running!
pool-1-thread-6is running!
pool-1-thread-6結束執行
pool-1-thread-7開始執行
pool-1-thread-7is running!
pool-1-thread-7is running!
pool-1-thread-7is running!
pool-1-thread-7is running!
pool-1-thread-7結束執行
pool-1-thread-9開始執行
pool-1-thread-9is running!
pool-1-thread-9is running!
pool-1-thread-9is running!
pool-1-thread-9is running!
pool-1-thread-9結束執行
pool-1-thread-10開始執行
pool-1-thread-10is running!
pool-1-thread-10is running!
pool-1-thread-10is running!
pool-1-thread-10is running!
pool-1-thread-10結束執行
pool-1-thread-5開始執行
pool-1-thread-8開始執行
pool-1-thread-8is running!
pool-1-thread-8is running!
pool-1-thread-8is running!
pool-1-thread-8is running!
pool-1-thread-8結束執行
pool-1-thread-5is running!
pool-1-thread-5is running!
pool-1-thread-5is running!
pool-1-thread-5is running!
pool-1-thread-5結束執行
最終執行緒執行!
最終執行緒結束
2.5 Semaphore
通過訊號量也可以!其實包括這種方法,本質上都是通過對一個狀態位的原子操作,當所有執行緒執行完畢的時候,這個狀態位達到某種情況,而最終執行緒發現狀態位達到自己想要的狀態,進而可以執行!
話不多說,看看訊號量如何實現的:
package ddx.多執行緒;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class wait_main_3 {
private static final Semaphore semaphore = new Semaphore(10);
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(new task4(semaphore));
}
new Thread(() -> {
try {
semaphore.acquireUninterruptibly(10);
//從這個訊號量獲得給定數量的許可,阻塞直到所有許可都可用。
System.out.println("最終執行緒執行!");
System.out.println("最終執行緒結束!");
semaphore.release();
} catch (Exception e){
e.printStackTrace();
}
}).start();
}
}
class task4 implements Runnable {
private Semaphore semaphore;
task4(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "開始執行");
for (int i = 0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "is running!");
}
System.out.println(Thread.currentThread().getName() + "結束執行");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
執行結果:
pool-1-thread-1開始執行
pool-1-thread-1is running!
pool-1-thread-1is running!
pool-1-thread-1is running!
pool-1-thread-1is running!
pool-1-thread-1結束執行
pool-1-thread-2開始執行
pool-1-thread-2is running!
pool-1-thread-2is running!
pool-1-thread-2is running!
pool-1-thread-2is running!
pool-1-thread-2結束執行
pool-1-thread-3開始執行
pool-1-thread-3is running!
pool-1-thread-3is running!
pool-1-thread-3is running!
pool-1-thread-3is running!
pool-1-thread-3結束執行
pool-1-thread-4開始執行
pool-1-thread-4is running!
pool-1-thread-4is running!
pool-1-thread-4is running!
pool-1-thread-4is running!
pool-1-thread-4結束執行
pool-1-thread-5開始執行
pool-1-thread-5is running!
pool-1-thread-5is running!
pool-1-thread-5is running!
pool-1-thread-5is running!
pool-1-thread-5結束執行
pool-1-thread-6開始執行
pool-1-thread-6is running!
pool-1-thread-6is running!
pool-1-thread-6is running!
pool-1-thread-8開始執行
pool-1-thread-8is running!
pool-1-thread-6is running!
pool-1-thread-8is running!
pool-1-thread-8is running!
pool-1-thread-6結束執行
pool-1-thread-8is running!
pool-1-thread-8結束執行
pool-1-thread-9開始執行
pool-1-thread-9is running!
pool-1-thread-9is running!
pool-1-thread-9is running!
pool-1-thread-9is running!
pool-1-thread-9結束執行
pool-1-thread-10開始執行
pool-1-thread-10is running!
pool-1-thread-10is running!
pool-1-thread-10is running!
pool-1-thread-10is running!
pool-1-thread-10結束執行
pool-1-thread-7開始執行
pool-1-thread-7is running!
pool-1-thread-7is running!
pool-1-thread-7is running!
pool-1-thread-7is running!
pool-1-thread-7結束執行
最終執行緒執行!
最終執行緒結束!
本作品採用《CC 協議》,轉載必須註明作者和本文連結