併發編程解惑之線程
一、線程與進程
進程是資源分配的最小單位,每個進程都有獨立的代碼和數據空間,一個進程包含 1 到 n 個線程。線程是 CPU 調度的最小單位,每個線程有獨立的運行棧和程序計數器,線程切換開銷小。
Java 程序總是從主類的 main 方法開始執行,main 方法就是 Java 程序默認的主線程,而在 main 方法中再創建的線程就是其他線程。在 Java 中,每次程序啓動至少啓動 2 個線程。一個是 main 線程,一個是垃圾收集線程。每次使用 Java 命令啓動一個 Java 程序,就相當於啓動一個 JVM 實例,而每個 JVM 實例就是在操作系統中啓動的一個進程。
二、線程的創建方式
多線程可以通過繼承或實現接口的方式創建。
2.1 直接繼承 Thread 類實現多線程
Thread 類是 JDK 中定義的用於控制線程對象的類,該類中封裝了線程執行體 run() 方法。需要強調的一點是,線程執行先後與創建順序無關。
/\*\* \* 類MyThread \*/
public class MyThread extends Thread {
int nTime;
String strThread;
public MyThread(int nTime, String strThread) {
this.nTime = nTime;
this.strThread = strThread;
}
//線程執行體
public void run() {
while (true) {
try {
System.out.println("Thread name:" + strThread + " ");
//線程睡眠,睡眠完成後繼續執行
Thread.sleep(nTime);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//main方法是主線程
static public void main(String args[]) {
//額外創建了三個MyThread線程
MyThread aThread = new MyThread(1000, "aThread");
aThread.start();
MyThread bThread = new MyThread(2000, "bThread");
bThread.start();
MyThread cThread = new MyThread(3000, "cThread ");
//調用線程對象的start方法,線程會以多線程的方式併發執行,
//如果直接調用run方法,線程是直接執行普通方法,並不是併發運行
cThread.start();
}
}
2.2 通過 Runnable 接口實現多線程
/\*\* \* 類MyRunnable \*/
public class MyRunnable implements Runnable{
int nTime;
String strThread;
public MyRunnable(int nTime, String strThread) {
this.nTime = nTime;
this.strThread = strThread;
}
@Override
public void run() {
while (true) {
try {
System.out.println("Thread name:" + strThread + " ");
//線程睡眠,睡眠完成後繼續執行
Thread.sleep(nTime);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//main方法是主線程
static public void main(String args[]) {
//對象實例的運行線程
Thread aRunnable = new Thread(new MyRunnable(1000, "aRunnable"));
aRunnable.start();
Thread bRunnable = new Thread(new MyRunnable(2000, "bRunnable"));
bRunnable.start();
Thread cRunnable = new Thread(new MyRunnable(3000, "cRunnable"));
cRunnable.start();
}
}
通過 Runnable 方式創建線程相比通過繼承 Thread 類創建線程的優勢是避免了單繼承的侷限性。若一個 boy 類繼承了 person 類,boy 類就無法通過繼承 Thread 類的方式來實現多線程。
使用 Runnable 接口創建線程的過程:先是創建對象實例 MyRunnable,然後將對象 My Runnable 作爲 Thread 構造方法的入參,來構造出線程。對於 new Thread(Runnable target)
創建的使用同一入參目標對象的線程,可以共享該入參目標對象 MyRunnable 的成員變量和方法,但 run() 方法中的局部變量相互獨立,互不干擾。
上面代碼是 new 了三個不同的 My Runnable 對象,如果只想使用同一個對象,可以只 new 一個 MyRunnable 對象給三個 new Thread 使用。
實現 Runnable 接口比繼承 Thread 類所具有的優勢:
- 適合多個相同的程序代碼的線程去處理同一個資源(使用同一個目標對象),一份代碼,多份數據,代碼和數據分離
- 可以避免 Java 中的單繼承的限制
- 線程池只能放入實現 Runable 或 callable 類線程,不能直接放入繼承 Thread 類的線程
三、線程的狀態
線程有新建、可運行、阻塞、等待、定時等待、死亡 6 種狀態。一個具有生命的線程,總是處於這 6 種狀態之一。 每個線程可以獨立於其他線程運行,也可和其他線程協同運行。線程被創建後,調用 start() 方法啓動線程,該線程便從新建態進入就緒狀態。
3.1 新建狀態
NEW 狀態(新建狀態) 實例化一個線程之後,並且這個線程沒有開始執行,這個時候的狀態就是 NEW 狀態:
Thread thread = new Thread();
System.out.println(thread.getState());
// NEW 狀態(thread.getState()獲取線程狀態)。
3.2 就緒狀態
RUNNABLE 狀態(就緒狀態):
static public void main(String args[]) {
Thread thread = new Thread(
// 創建runnable入參對象
new Runnable() {
@Override public void run() {
System.out.println(thread.getState());
}
},
// 線程名稱
"RUNNABLE-Thread");
// 線程啓動後就是runnable狀態,正在運行的狀態
thread.start();
}
3.3 阻塞狀態
阻塞狀態有 3 種:
- 等待阻塞:運行的線程執行了 wait() 方法,釋放持有的鎖,JVM 會把該線程放入等待隊列(等待池)中。
- 同步阻塞:運行線程獲取同步鎖時,而同步鎖被其他線程持有,則 JVM 會把線程放入同步隊列(鎖池)中。
- 其他阻塞:運行的線程執行 sleep() 或 join() 方法,JVM 會把該線程置爲阻塞狀態。當 sleep() 狀態超時、join() 等待的線程結束了或者超時,線程會重新轉入就緒狀態。當運行線程發出了 I/O 請求時,一樣會變成阻塞狀態,直到 I/O 處理完畢,才變成就緒狀態。
3.4 等待狀態
如果一個線程調用了一個對象的 wait 方法, 那麼這個線程就會處於等待狀態(waiting 狀態)直到另外一個線程調用這個對象的 notify 或者 notifyAll 方法後纔會解除這個狀態。
static public void main(String args[]) {
final Object lock = new Object();
Thread threadA = new Thread(new Runnable() {
@Override public void run() {
synchronized (lock) {
try {
//當前線程釋放持有的lock鎖,並等待lock鎖的分配
lock.wait();
//等待結束
System.out.println("wait over");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "WAITING-Thread-A");
Thread threadB = new Thread(new Runnable() {
@Override public void run() {
synchronized (lock) {
//當前線程喚醒等待隊列裏的線程,讓線程進入就緒隊列申請lock鎖
//但是本線程仍然繼續擁有lock這個同步鎖,本線程仍然繼續執行
lock.notifyAll();
try {
// 當前線程睡眠2000毫秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "WAITING-Thread-B");
threadA.start();
threadB.start();
}
3.5 終結狀態
run() 裏的代碼執行完畢後,線程進入終結狀態(TERMINATED 狀態)。
3.6 小結
線程狀態有 6 種:新建、可運行、阻塞、等待、定時等待、死亡。
四、線程常用的 API
- Thread.currentThread():獲取當前線程
- thread.isAlive():某個線程實例是否存活。
- Thread.sleep():sleep 方法是 static 方法,線程類和線程實例調用,效果一樣
- thread.interrupt():將某個線程的中斷標誌位設置爲 true,並沒有中斷線程,它只是向線程發送一箇中斷信號。
- Thread.interrupted():判斷當前線程是否中斷,如果發現是 true,表明線程是中斷,返回 true,返回前將標誌位設置爲 false
- thread.isInterrupted():判斷線程是否中斷,不改變標誌位
- Object.wait():讓獲得 Object 鎖的 thread 線程等待
- Object.notify():喚醒獲得 Object 鎖的 thread 線程
- Object.wait() 與 Object.notify() 必須要與 synchronized (Object) 一起使用
- thread.join():等待 thread 線程終止
我們看下 join 方法的使用:
public class JoinTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "主線程運行開始!");
MyThreads m1 = new MyThreads("A");
MyThreads m2 = new MyThreads("B");
m1.start();
m2.start();
try {
//等待A線程運行結束後,main線程和B線程才能繼續執行
m1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
//等待B線程運行結束後,main線程才能繼續執行
m2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "主線程運行結束!");
}
}
class MyThreads extends Thread {
private String name;
public MyThreads(String name) {
super(name);
this.name = name;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " 子線程運行開始!");
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 子線程運行結束!");
}
}
運行結果:
main主線程運行開始!
A 子線程運行開始!
B 子線程運行開始!
A 子線程運行結束!
B 子線程運行結束!
main主線程運行結束!
- thread.yield(): yield() 是讓當前運行中的線程回到就緒狀態(可運行狀態),以允許具相同優先級的其他線程獲得運行機會。使得相同優先級的線程之間能適當的輪流執行。但實際中無法保證 yield() 達到讓步目的,讓步的線程還是有可能被調度到繼續執行。
我們來看下 yield 方法的使用:
public class YieldTest {
public static void main(String[] args) {
ThreadInstance y1 = new ThreadInstance("A");
ThreadInstance y2 = new ThreadInstance("B");
y1.start();
y2.start();
}
}
class ThreadInstance extends Thread {
public ThreadInstance(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 當i等於10時,就把對CPU的佔用釋放,讓自己或者同優先級的其他線程運行,誰搶到CPU時間片誰就執行
if (i == 3) {
this.yield();
}
}
}
}
運行結果:
B-----1
B-----2
B-----3
B-----4
B-----5
A-----1
A-----2
A-----3
A-----4
A-----5
五、線程間的通信
線程與線程之間是無法直接通信的,A 線程無法直接通知 B 線程,Java 中線程之間交換信息是通過共享的內存來實現的,控制共享資源的讀寫的訪問,使得多個線程輪流執行對共享數據的操作,線程之間通信是通過對共享資源上鎖或釋放鎖來實現的。線程排隊輪流執行共享資源,這稱爲線程的同步。
5.1 線程的通信方式
Java 提供了很多同步操作(也就是線程間的通信方式),同步可使用 synchronized 關鍵字、Object 類的 wait/notifyAll 方法、ReentrantLock 鎖、無鎖同步 CAS 等方式來實現。
5.2 ReentrantLock 鎖
ReentrantLock 是 JDK 內置的一個鎖對象,用於線程同步(線程通信),需要用戶手動釋放鎖。
public class ReentrantLockTest {
// 創建鎖對象
private ReentrantLock lock = new ReentrantLock();
public void work() {
lock.lock();//對下面的操作上鎖,只有拿到鎖,才能繼續執行
try {
System.out.println(Thread.currentThread().getName() );
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
}
} finally {
// 就算出現異常,也確保能釋放鎖
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
Thread thread1 = new Thread(new Runnable() {
@Override public void run() {
// 創建線程1執行同步方法work,需要拿到鎖才能執行try裏的代碼
reentrantLockTest.work();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override public void run() {
// 創建線程2執行同步方法work,需要拿到鎖才能執行try裏的代碼
reentrantLockTest.work();
}
});
//啓動兩個線程
thread1.start();
thread2.start();
}
}
運行結果:
Thread-0
// 隔了6秒鐘 輸入下面
Thread-1
這表明同一時間段只能有 1 個線程執行 work 方法,因爲 work 方法裏的代碼需要獲取到鎖才能執行,這就實現了多個線程間的通信,線程 0 獲取鎖,先執行,線程 1 等待,線程 0 釋放鎖,線程 1 繼續執行。
5.3 synchronized 內置鎖
synchronized 是一種語法級別的同步方式,稱爲內置鎖。該鎖會在代碼執行完畢後由 JVM 釋放。
public class SynchronizedTest {
//synchronized放在返回值後,對work方法上鎖,鎖住的是SynchronizedTest該類的對象實例
public synchronized void work() {
try {
nextLock();
Thread.sleep(6000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) {
SynchronizedTest reentrantLockTest = new SynchronizedTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
reentrantLockTest.work();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
reentrantLockTest.work();
}
});
//啓動兩個線程,這兩個線程都調用同一個對象實例reentrantLockTest的work方法
//這兩次調用需要競爭的是同一個鎖對象reentrantLockTest,故能實現線程間的同步
thread1.start();
thread2.start();
}
}
輸出結果跟 ReentrantLock 一樣。
Thread-0
等待了6秒後
Thread-1
5.4 wait/notifyAll 方式
Java 中的 Object 類默認是所有類的父類,該類擁有 wait、 notify、notifyAll 方法,其他對象會自動繼承 Object 類,可調用 Object 類的這些方法實現線程間的通信。
public class WaitNotifyAllTest {
public synchronized void doWait() {
//進入到方法內部,表明線程獲取到了鎖,鎖就是WaitNotifyAllTest對應的對象實例,this代表該實例
try {
// 此時釋放了鎖,其他線程可獲得鎖並執行
this.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public synchronized void doNotify() {
//進入到方法,表明獲取到了鎖
try {
Thread.sleep(6000);
//通知其他線程競爭鎖,此時鎖還未被釋放
this.notifyAll();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
//方法結束,鎖被自動釋放,其他線程終於可以申請鎖了
}
public static void main(String[] args) {
WaitNotifyAllTest waitNotifyAllTest = new WaitNotifyAllTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
waitNotifyAllTest.doWait();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
waitNotifyAllTest.doNotify();
}
});
thread1.start();
thread2.start();
}
}
5.5 無鎖同步 CAS
除了可以通過鎖的方式來實現通信,還可通過無鎖的方式來實現,無鎖同 CAS(Compare-and-Swap,比較和交換)的實現,需要有 3 個操作數:內存地址 V,舊的預期值 A,即將要更新的目標值 B,當且僅當內存地址 V 的值與預期值 A 相等時,將內存地址 V 的值修改爲目標值 B,否則就什麼都不做。
我們通過計算器的案例來演示無鎖同步 CAS 的實現方式,非線程安全的計數方式如下:
/\*\* \* 非線程安全計數器 \*/
private void count() {
i++;
}
線程安全的計數方式如下:
//基於CAS實現線程安全的計數器方法safeCount
public class CountAtomic {
private AtomicInteger atomic = new AtomicInteger(0);
private int i = 0;
/\*\* \* 使用CAS實現線程安全計數器 \*/
private void safeCount() {
for (;;) {
//獲取原子類實例值0(初始值爲0)
int i = atomic.get();
//實例值0和預期值0相同,設置實例值爲1
boolean sum = atomic.compareAndSet(i, ++i);
if (sum) {
//無限循環,直到sum值爲true,設置成功,退出循環,設置失敗,不斷循環判斷,不斷重試,類似自旋鎖
break;
}
}
}
/\*\* \* 非線程安全計數器 \*/
private void count() {
i++;
}
//調用main方法,測試兩種計數方法
public static void main(String[] args) {
final CountAtomic cas = new CountAtomic();
List<Thread> ts = new ArrayList<Thread>();//初始值爲600
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override public void run() {
for (int i = 0; i < 1000; i++) {
cas.count();
cas.safeCount();
}
}
});
//將線程加入隊列
ts.add(t);
}
//啓動所有線程
for (Thread t : ts) {
t.start();
}
// 等待所有線程執行完成
for (Thread t : ts) {
try {
//在線程A中調用了線程B的Join()方法,是A線程進入wait狀態,直到線程B執行完畢後,纔會繼續執行線程A
//這是讓main線程進入等待,直到所有t線程執行完,才繼續執行main線程
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("非線程安全累加結果"+cas.i);
System.out.println("線程安全累加結果"+cas.atomic.get());
}
}
運行結果:
非線程安全累加結果98636
線程安全累加結果100000
線程安全累加的結果纔是正確的,非線程安全會出現少計算值的情況。JDK 1.5 開始,併發包裏提供了原子操作的類,AtomicBoolean 用原子方式更新的 boolean 值,AtomicInteger 用原子方式更新 int 值,AtomicLong 用原子方式更新 long 值。 AtomicInteger 和 AtomicLong 還提供了用原子方式將當前值自增 1 或自減 1 的方法,在多線程程序中,諸如 ++i 或 i++ 等運算不具有原子性,是不安全的線程操作之一。 通常我們使用 synchronized 將該操作變成一個原子操作,但 JVM 爲此種操作提供了原子操作的同步類 Atomic,使用 AtomicInteger 做自增運算的性能是 ReentantLock 的好幾倍。
六、J.U.C 包
上面我們都是使用底層的方式實現線程間的通信的,但在實際的開發中,我們應該儘量遠離底層結構,使用封裝好的 API,例如 J.U.C 包(java.util.concurrent,又稱併發包)下的工具類 CountDownLath、CyclicBarrier、Semaphore,來實現線程通信,協調線程執行。
6.1 閉鎖 CountDownLatch
CountDownLatch 能夠實現線程之間的等待,CountDownLatch 用於某一個線程等待若干個其他線程執行完任務之後,它纔開始執行。
CountDownLatch 類只提供了一個構造器:
public CountDownLatch(int count) { }; //參數count爲計數值
CountDownLatch 類中常用的 3 個方法:
- await() 方法:調用 await() 方法的線程會被掛起,它會等待直到 CountDownLatch 的 count 值爲 0 才繼續執行。
public void await() throws InterruptedException { };
- 帶時間的 await() 方法,等待一定的時間後 CountDownLatch 的 count 值還沒變爲 0 的話就會繼續執行。
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
- 將 CountDownLatch 的 count 值減 1。
public void countDown() { };
public class CountDownLatchTest {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(2);
new Thread() {
public void run() {
try {
System.out.println("子線程" + Thread.currentThread().getName() + "正在執行");
Thread.sleep(3000);
System.out.println("子線程" + Thread.currentThread().getName() + "執行完畢");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
;
}.start();
new Thread() {
public void run() {
try {
System.out.println("子線程" + Thread.currentThread().getName() + "正在執行");
Thread.sleep(2000);
System.out.println("子線程" + Thread.currentThread().getName() + "執行完畢");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
;
}.start();
try {
System.out.println("main線程等待2個子線程執行完畢");
latch.await();
System.out.println("2個子線程已經執行完畢");
System.out.println("main線程繼續執行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運行結果:
main線程等待2個子線程執行完畢
子線程Thread-1正在執行
子線程Thread-0正在執行
子線程Thread-1執行完畢
子線程Thread-0執行完畢
2個子線程已經執行完畢
main線程繼續執行
6.2 循環柵欄 CyclicBarrier
CyclicBarrier 字面意思循環柵欄,通過它可以讓一組線程等待至某個狀態之後再全部同時執行。當所有等待線程都被釋放以後,CyclicBarrier 可以被重複使用,所以有循環之意。
相比 CountDownLatch,CyclicBarrier 可以被循環使用,而且如果遇到線程中斷等情況時,可以利用 reset() 方法,重置計數器,CyclicBarrier 會比 CountDownLatch 更加靈活。
CyclicBarrier 提供 2 個構造器:
public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}
上面的方法中,參數 parties 指讓多少個線程或者任務等待至 barrier 狀態;參數 barrierAction 爲當這些線程都達到 barrier 狀態時會執行的內容。
CyclicBarrier 中最重要的方法 await 方法,它有 2 個重載版本。下面方法用來掛起當前線程,直至所有線程都到達 barrier 狀態再同時執行後續任務。
public int await() throws InterruptedException, BrokenBarrierException {
};
而下面的方法則是讓這些線程等待至一定的時間,如果還有線程沒有到達 barrier 狀態就直接讓到達 barrier 的線程執行任務。
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
};
public class CyclicBarrierTest {
public static void main(String[] args) {
Random random = new Random();
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
//啓動5個線程,對應cyclicBarrier設置的讓5個線程等待至barrier狀態
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override public void run() {
int secs = random.nextInt(5);
System.out.println("線程" + Thread.currentThread().getName() + "正在寫入數據");
try {
//以睡眠來模擬寫入數據操作
Thread.sleep(secs * 1000);
System.out.println("線程" + Thread.currentThread().getName() + "寫入數據完畢,等待其他線程寫入完畢");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("所有線程寫入完畢,繼續處理其他任務");
}
}).start();
}
}
}
運行結果:
線程Thread-0正在寫入數據
線程Thread-3正在寫入數據
線程Thread-4正在寫入數據
線程Thread-1正在寫入數據
線程Thread-2正在寫入數據
線程Thread-3寫入數據完畢,等待其他線程寫入完畢
線程Thread-2寫入數據完畢,等待其他線程寫入完畢
線程Thread-0寫入數據完畢,等待其他線程寫入完畢
線程Thread-1寫入數據完畢,等待其他線程寫入完畢
線程Thread-4寫入數據完畢,等待其他線程寫入完畢
所有線程寫入完畢,繼續處理其他任務
所有線程寫入完畢,繼續處理其他任務
所有線程寫入完畢,繼續處理其他任務
所有線程寫入完畢,繼續處理其他任務
所有線程寫入完畢,繼續處理其他任務
CyclicBarrier 用於一組線程互相等待至某個狀態,然後這一組線程再同時執行,CountDownLatch 是不能重用的,而 CyclicBarrier 可以重用。
6.3 信號量 Semaphore
Semaphore 類是一個計數信號量,它可以設定一個閾值,多個線程競爭獲取許可信號,執行完任務後歸還,超過閾值後,線程申請許可信號時將會被阻塞。Semaphore 可以用來 構建對象池,資源池,比如數據庫連接池。
假如在服務器上運行着若干個客戶端請求的線程。這些線程需要連接到同一數據庫,但任一時刻只能獲得一定數目的數據庫連接。要怎樣才能夠有效地將這些固定數目的數據庫連接分配給大量的線程呢?
給方法加同步鎖,保證同一時刻只能有一個線程去調用此方法,其他所有線程排隊等待,但若有 10 個數據庫連接,也只有一個能被使用,效率太低。另外一種方法,使用信號量,讓信號量許可與數據庫可用連接數爲相同數量,10 個數據庫連接都能被使用,大大提高性能。
public class SemaphoreDemo {
// 請求總數(客戶端請求)
public static int clientTotal = 10;
// 同時併發執行的線程數(服務端連接數)
public static int threadTotal = 2;
public static void main(String[] args) throws Exception {
// 創建緩存線程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 創建許可數和線程數一樣,表示最多2個線程同時運行
final Semaphore semaphore = new Semaphore(threadTotal);
// 閉鎖countDownLatch的計數設置爲10
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
// 申請許可
semaphore.acquire();
// 睡眠1秒(實際中爲具體業務代碼)
resolve(count);
// 釋放許可。使得每隔一秒就有兩個線程執行
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
// for循環每執行一次,計數器就減1
countDownLatch.countDown();
});
}
// main線程一直阻塞,直到countDownLatch計數爲0
countDownLatch.await();
// 關閉線程池
executorService.shutdown();
}
private static void resolve(int i) throws InterruptedException {
Thread.sleep(1000);
}
}
上面三個工具類是 J.U.C 包的核心類,J.U.C 包的全景圖就比較複雜了:
七、異步線程和線程封閉
7.1 異步線程
Future 是一種異步執行的設計模式,類似 ajax 異步請求,不需要同步等待返回結果,可繼續執行代碼。使 Runnable(無返回值不支持上報異常)或 Callable(有返回值支持上報異常)均可開啓線程執行任務。但是如果需要異步獲取線程的返回結果,就需要通過 Future 來實現了。
Future 是位於 java.util.concurrent 包下的一個接口,Future 接口封裝了取消任務,獲取任務結果的方法。
public interface Future\<V\> {
//如果取消任務成功則返回true,如果取消任務失敗則返回false。
//參數mayInterruptIfRunning表示是否允許取消正在執行的任務,true爲允許
boolean cancel(boolean mayInterruptIfRunning);
//任務是否被取消成功,若是,返回true
boolean isCancelled();
//任務是否已經完成,若是,返回true
boolean isDone();
//阻塞獲取任務執行結果,一直去獲取,直到任務執行完畢返回
V get() throws InterruptedException, ExecutionException;
//獲取執行結果,超時後,未獲取到結果,就返回null
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
在 Java 中,一般是通過繼承 Thread 類或者實現 Runnable 接口來創建多線程, Runnable 接口不能返回結果,JDK 1.5 之後,Java 提供了 Callable 接口來封裝子任務,Callable 接口可以獲取返回結果。我們使用線程池提交 Callable 接口任務,將返回 Future 接口添加進 ArrayList 數組,最後遍歷 FutureList,實現異步獲取返回值。
public class FutureDesign {
public static void main(String[] args) {
Long start = System.currentTimeMillis();
ExecutorService exs = Executors.newFixedThreadPool(6);
try {
//存儲返回結果
List<Integer> list = new ArrayList<Integer>();
List<Future<Integer>> futureList = new ArrayList<Future<Integer>>();
//提交6個任務,每個任務返回一個Future對象,再加入futureList
for (int i = 0; i < 6; i++) {
futureList.add(exs.submit(new CallableTask(i + 1)));
}
Long getResultStart = System.currentTimeMillis();
//2.結果歸集,用迭代器遍歷futureList,高速輪詢(模擬併發訪問),任務完成就移除
while (futureList.size() > 0) {
Iterator<Future<Integer>> iterable = futureList.iterator();
//如果下一個元素存在
while (iterable.hasNext()) {
//獲取下一個元素,future對象
Future<Integer> future = iterable.next();
//任務完成後或者被取消後
if (future.isDone() && !future.isCancelled()) {
//獲取結果
Integer i = future.get();
list.add(i);
//任務完成,可移除任務
iterable.remove();
} else {
Thread.sleep(1);//避免CPU高速運轉,休息1毫秒,CPU運行速度是納秒級別
}
}
}
System.out.println("list=" + list);
System.out.println("總耗時=" + (System.currentTimeMillis() - start) + "毫秒" + ",取結果歸集耗時=" + (System.currentTimeMillis() - getResultStart) + "毫秒");
} catch (Exception e) {
e.printStackTrace();
} finally {
exs.shutdown();
}
}
//回調方法
static class CallableTask implements Callable\<Integer\> {
Integer i;
public CallableTask(Integer i) {
super();
this.i = i;
}
@Override
public Integer call() throws Exception {
Thread.sleep(3000);//任務耗時3秒
System.out.println("task線程:" + Thread.currentThread().getName() + "任務i=" + i + ",完成!");
return i;
}
}
}
運行結果:
task線程:pool-1-thread-5任務i=5,完成!
task線程:pool-1-thread-2任務i=2,完成!
task線程:pool-1-thread-4任務i=4,完成!
task線程:pool-1-thread-6任務i=6,完成!
task線程:pool-1-thread-3任務i=3,完成!
task線程:pool-1-thread-1任務i=1,完成!
list=[2, 4, 5, 6, 1, 3]
總耗時=3013毫秒,取結果歸集耗時=3003毫秒
上面就是異步線程執行的調用過程,實際開發中用得更多的是使用現成的異步框架來實現異步編程,如 RxJava,有興趣的可以繼續去了解,通常異步框架都是結合遠程 HTTP 調用 Retrofit 框架來使用的,兩者結合起來用,可以避免調用遠程接口時,花費過多的時間在等待接口返回上。
7.2 線程封閉
線程封閉是通過本地線程 ThreadLocal 來實現的,ThreadLocal 是線程局部變量(local vari able),它爲每個線程都提供一個變量值的副本,每個線程對該變量副本的修改相互不影響。
在 JVM 虛擬機中,堆內存用於存儲共享的數據(實例對象),也就是主內存。Thread Local .set()、ThreadLocal.get() 方法直接在本地內存(工作內存)中寫和讀共享變量的副本,而不需要同步數據,不用像 synchronized 那樣保證數據可見性,修改主內存數據後還要同步更新到工作內存。
Myabatis、hibernate 是通過 threadlocal 來存儲 session 的,每一個線程都維護着一個 session,對線程獨享的資源操作很方便,也避免了線程阻塞。
ThreadLocal 類位於 Thread 線程類內部,我們分析下它的源碼:
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
public T get() {
// 得到當前線程
Thread t = Thread.currentThread();
// 獲取當前線程的ThreadLocalMap容器
ThreadLocalMap map = getMap(t);
if (map != null) {
//容器存在,找到當前線程對應的鍵值對Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//獲取要返回的值
T result = (T)e.value;
return result;
}
}
// 不存在ThreadLocalMap容器,就初始化一個ThreadLocalMap
return setInitialValue();
}
private T setInitialValue() {
// 初始化ThreadLocalMap
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 實例化ThreadLocalMap之後,將key爲當前線程對象,value爲null的初始值設置到Map中
map.set(this, value);
else
createMap(t, value);
return value;
}
public void set(T value) {
// 找到當前線程的ThreadLocalMap,設置對應的值,
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else//找不到ThreadLocalMap,創建ThreadLocalMap容器
createMap(t, value);
}
static class ThreadLocalMap {
//ThreadLocalMap容器中存放的就是鍵值對Entry,Entry的KEY就是ThreadLocal(當前線程),VALUE就是值(共享變量值)。
static class Entry extends WeakReference\<ThreadLocal\<?\>\> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocal 和 Synchonized 都用於解決多線程併發訪問的問題,訪問多線程共享的資源時,Synchronized 同步機制採用了以時間換空間的方式,提供一份變量讓多個線程排隊訪問,而 ThreadLocal 採用了以空間換時間的方式,提供每個線程一個變量,實現數據隔離。
ThreadLocal 可用於數據庫連接 Connection 對象的隔離,使得每個請求線程都可以複用連接而又相互不影響。
public class ConnectionManager {
private static ThreadLocal<Connection> connections = new ThreadLocal<Connection>() {
@Override protected Connection initialValue() {
Connection conn = null;
try {
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/shop", "name","password");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
};
public static Connection getConnection() {
return connections.get();
}
public static void setConnection(Connection conn) {
connections.set(conn);
}
}
7.3 Java 的引用和內存泄漏
在 Java 裏面,存在強引用、弱引用、軟引用、虛引用。我們主要來了解下強引用和弱引用:
A a = new A();
B b = new B();
上面 a、b 對實例 A、B 都是強引用
C c = new C(b);
b = null;
而上面這種情況就不一樣了,即使 b 被置爲 null,但是 c 仍然持有對 C 對象實例的引用,而間接的保持着對 b 的強引用,所以 GC 不會回收分配給 b 的空間,導致 b 無法回收也沒有被使用,造成了內存泄漏。這時可以通過 c = null;
來使得 c 被回收,但也可以通過弱引用來達到同樣目的:
WeakReference c = new WeakReference(b);
從源碼中可以看出 Entry 裏的 key 對 ThreadLocal 實例是弱引用:
static class Entry extends WeakReference\<ThreadLocal\<?\>\> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry 裏的 key 對 ThreadLocal 實例是弱引用,將 key 值置爲 null,堆中的 ThreadLocal 實例是可以被垃圾收集器(GC)回收的。但是 value 卻存在一條從 Current Tnsparent;text-size-adjust:none;-webkit-font-smoothing:antialiased;box-sizing:border-box;margin:0px 0px 1.1em;outline:0px;">總結來說,利用 ThreadLocal 來訪問共享數據時,JVM 通過設置 ThreadLocalMap 的 Key 爲弱引用,來避免內存泄露,同時通過調用 remove、get、set 方法的時候,回收弱引用(Key 爲 null 的 Entry)。當使用 static ThreadLocal 的時候(如上面的 Spring 多數據源),static 變量在類未加載的時候,它就已經加載,當線程結束的時候,static 變量不一定會被回收,比起普通成員變量使用的時候才加載,static 的生命週期變長了,若沒有及時回收,容易產生內存泄漏。
八、線程池
8.1 線程池的核心參數和作用順序
使用線程池,可以重用存在的線程,減少對象創建、消亡的開銷,可控制最大併發線程數,避免資源競爭過度,還能實現線程定時執行、單線程執行、固定線程數執行等功能。
Java 把線程的調用封裝成了一個 Executor 接口,Executor 接口中定義了一個 execute 方法,用來提交線程的執行。Executor 接口的子接口是 ExecutorService,負責管理線程的執行。通過 Executors 類的靜態方法可以初始化
ExecutorService 線程池。Executors 類的靜態方法可創建不同類型的線程池:
newFixedThreadPool 固定線程線程池
newSingleThreadExecutor 單線程線程池
newCachedThreadPool 緩存線程線程池
newScheduledThreadPool 調度線程線程池
但是,不建議使用 Executors 去創建線程池,而是通過 ThreadPoolExecutor 的方式,明確給出線程池的參數去創建,規避資源耗盡的風險。
如果使用 Executors 去創建線程池:
- 對於 Executors.newFixedThreadPool() 和 Executors.newSingleThreadExecutor() 線程池: 隊列用的是 LinkedBlockingQueue,默認大小爲 Integer.MAX_VALUE ,可能會堆積大量的請求(可以無限的添加任務),從而導致內存溢出。
- 對於 Executors.newCachedThreadPool() 和 Executors.ScheduledThreadPool 線程池: 允許的創建的最大線程數量爲 Integer.MAX_VALUE,即 2147483647,大量線程的創建會導致嚴重的性能問題(線程上下文切換帶來的開銷),線程創建佔用堆外內存,任務對象佔用堆內內存,大量線程執行任務會導致堆外內存或堆內內存任意一個首先內存溢出。
最佳的實踐是通過 ThreadPoolExecutor 手動地去創建線程池,選取合適的隊列存儲任務,並指定線程池線程大小。通過線程池實現類 ThreadPoolExecutor 可構造出線程池的,構造函數有下面幾個重要的參數:
public ThreadPoolExecutor(int corePoolSize, i nt maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue\<Runnable\> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;}
參數 1:corePoolSize
線程池核心線程數。
參數 2:workQueue
阻塞隊列,用於保存執行任務的線程,有 4 種阻塞隊列可選:
- 基於數組的有界阻塞隊列,按 FIFO 先進先出任務的 ArrayBlockingQueue();
- 基於鏈表的阻塞隊列(可支持有界或無界),按 FIFO 先進先出任務的 LinkedBlockingQueue();
- 不存儲元素的阻塞隊列,一個線程插入元素後會被阻塞,直到被其他線程取出元素纔會喚醒的 Synchronous Queue(吞吐量高於 LinkedBlockingQueue,是一個無界阻塞隊列,理論上可存儲無限個元素);
- 具有優先級的可以針對任務排序的無界阻塞隊列 PriorityBlockingQueue()。
關於隊列的其他內容,會在併發編程解惑之隊列這篇裏做詳細的介紹。
參數 3:maximunPoolSize
線程池最大線程數。如果阻塞隊列滿了(有界的阻塞隊列),來了一個新的任務,若線程池當前線程數小於最大線程數,則創建新的線程執行任務,否則交給飽和策略處理。如果是無界隊列就不存在這種情況,任務都在無界隊列裏存儲着。
參數 4:RejectedExecutionHandler
拒絕策略,當隊列滿了,而且線程達到了最大線程數後,對新任務採取的處理策略。
有 4 種策略可選:
- 丟棄新任務並拋出 Rejected Execution Exception 異常的 AbortPolicy 策略
- 直接丟棄新任務不拋出異常的 DiscardPolicy 策略
- 由調用線程處理該任務的 CallerRunsPolicy() 策略
- 丟棄隊列最前面的任務,然後重新嘗試執行任務(不斷重複該過程)的 DiscardOldestPolicy 策略
最後,還可以自定義處理策略。
參數 5:ThreadFactory
創建線程的工廠。
參數 6:keeyAliveTime
線程沒有任務執行時最多保持多久時間終止。當線程池中的線程數大於 corePoolSize 時,線程池中所有線程中的某一個線程的空閒時間若達到 keepAliveTime,則會終止,直到線程池中的線程數不超過 corePoolSize。但如果調用了 allowCoreThread TimeOut(boolean value) 方法,線程池中的線程數就算不超過 corePoolSize,keepAlive Time 參數也會起作用,直到線程池中的線程數量變爲 0。
參數 7:TimeUnit
配合第 6 個參數使用,表示存活時間的時間單位最佳的實踐是通過 ThreadPoolExecutor 手動地去創建線程池,選取合適的隊列存儲任務,並指定線程池線程大小。
public class FixedThreadPoolTest {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2, Executors.defaultThreadFactory());
final LinkedBlockingDeque<String> que = new LinkedBlockingDeque<String>();
for(int i = 1; i <= 10; i ++) {
//將數字轉換成字符串
que.add(i + "");
}
Future<String> result = es.submit(new Callable<String>() {
@Override public String call() throws Exception {
while (!que.isEmpty()) {
System.out.println(que.poll());
}
return "運行完畢";
}
});
System.out.println(result.isDone());
// get方法會阻塞,直到拿到返回值
try {
System.out.println(result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
運行結果:
false 1 2 3 4 5 6 7 8 9 10 運行完畢
8.2 線程池源碼
線程池創建線程時,會將線程封裝成工作線程 Worker,Worker 在執行完任務後,還會不斷的去獲取隊列裏的任務來執行。Worker 的加鎖解鎖機制是繼承 AQS 實現的。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
private static final long serialVersionUID = 6138294804551838833L;
//被封裝的線程,就是Worker自己
final Thread thread;
//Worker要執行的第一個任務
Runnable firstTask;
//記錄執行完成的任務數量
volatile long completedTasks;
//Worker類構造器
Worker(Runnable firstTask) {
setState(-1); // 在worker線程沒有啓動前是-1狀態,無法加鎖
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
// state爲0代表沒加鎖
// state爲1代表加鎖了
//通過CAS嘗試加鎖,將狀態從0設置爲1
//該方法重寫了父類AQS的同名方法
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//嘗試釋放鎖,直接將state置爲0
//該方法重寫了父類AQS的同名方法
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//需要注意的是:tryAcquire與tryRelease是重寫了父類AQS的方法,並且不可以直接調用,它們被下面的方法調用,實現加鎖和解除
//加鎖
//acquire方法是它父類AQS類的方法,方法裏會調用tryAcquire方法加鎖
public void lock() {
acquire(1);
}
//嘗試加鎖
public boolean tryLock() {
return tryAcquire(1);
}
//解鎖
//release方法是它父類AQS類的方法,方法裏會調用tryRelease方法釋放鎖
public void unlock() {
release(1);
}
//返回鎖的狀態
public boolean isLocked() {
return isHeldExclusively();
}
}
我們來看下 Worker 線程的運行過程:
//Worker執行任務時會調用run方法
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;// 得到Worker中的任務task(通過addWorker方法提交的任務)
w.firstTask = null;
w.unlock(); //允許中斷
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();// 拿到了任務,給Worker上鎖,表示當前Worker開始執行任務了
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
//先判斷線程池狀態是否允許繼續執行任務:
//如果是stop、tidying、terminated狀態(這種狀態是不接受任務,且不執行任務的),並且線程是非中斷狀態
//又或者是shutingdown、runing狀態 ,並且處於中斷狀態
//這個時候則中斷線程
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();// 調用task的run方法執行任務,而不是start方法。線程池調用shutdownNow方法可以中斷run的運行
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();// 執行完任務後,解鎖Worker,當前Worker線程變成閒置Worker線程
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);// 回收Worker線程
}
}
總結來說,如果當前運行的線程數小於 corePoolSize 線程數,則獲取全局鎖,然後創建新的線程來執行任務如果運行的線程數大於等於 corePoolSize 線程數,則將任務加入阻塞隊列 BlockingQueue 如果阻塞隊列已滿,無法將任務加入 BlockingQueue,則獲取全局所,再創建新的線程來執行任務
如果新創建線程後使得線程數超過了 maximumPoolSize 線程數,則調用 Rejected ExecutionHandler.rejectedExecution() 方法根據對應的拒絕策略處理任務。
CPU 密集型任務,線程執行任務佔用 CPU 時間會比較長,應該配置相對少的線程數,避免過度爭搶資源,可配置 N 個 CPU+1 個線程的線程池;但 IO 密集型任務則由於需要等待 IO 操作,線程經常處於等待狀態,應該配置相對多的線程如 2*N 個 CPU 個線程,A 線程阻塞後,B 線程能馬上執行,線程多競爭激烈,能飽和的執行任務。線程提交 SQL 後等待數據庫返回結果時間較長的情況,CPU 空閒會較多,線程數應設置大些,讓更多線程爭取 CPU 的調度。
參考資料
- Java 高級 - 多線程機制詳解
- Java 併發編程序列之線程狀態
- Java 併發編程:CountDownLatch、CyclicBarrier 和 Semaphore
- Java 內存模型與線程
- 原子操作的實現原理
- ThreadLocal 原理及其實際應用
- Spring 動態切換多數據源解決方案
- Java 線程池 ThreadPoolExecutor 源碼分析
- Java 多線程編程(三)-Future、FutureTask、CompletableFuture
歡迎關注我的公衆號,回覆關鍵字“Java” ,將會有大禮相送!!! 祝各位面試成功!!!