夜光序言:
努力是會上癮的 尤其是嚐到甜頭以後
正文:
以道御術 / 以術識道
九、Java 的多線程和併發庫
對於 Java 程序員來說,多線程在工作中的使用場景還是比較常見的,而僅僅掌握了 Java 中的傳統多線程機制,還是不夠的。
在 JDK5.0 之後,Java 增加的併發庫中提供了很多優秀的 API,在實際開發中用的比較多。
因此我們有必要對這部分知識做一個全面的瞭解。
( 1 ) 傳統使用類 Thread 和接口 Runnable 實現
1.在 Thread 子類覆蓋的 run 方法中編寫運行代碼
方式一
package com.hy.多線程高併發;
public class Test {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
2.在傳遞給 Thread 對象的 Runnable 對象的 run 方法中編寫代碼
3.總結
查看 Thread 類的 run()方法的源代碼,可以看到其實這兩種方式都是在調用 Thread 對象的 run 方法
如果 Thread類的 run 方法沒有被覆蓋,並且爲該 Thread 對象設置了一個 Runnable 對象,該 run 方法會調用 Runnable 對象的 run 方法
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
( 2 ) 定實現時器 Timer 和 TimerTask
Timer 在實際開發中應用場景不多,一般來說都會用其他第三方庫來實現。
下面我們就針對來使用 Timer 定時類。
//要求:使用定時器,間隔 4 秒執行一次,再間隔 2 秒執行一次,以此類推執行。
package com.hy.多線程高併發;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class Test2 {
private static volatile int count = 0;
static class TimerTastCus extends TimerTask {
// private static volatile int count = 0;
@Override
public void run() {
count = (count +1)%2;
System.err.println("Boob boom ");
new Timer().schedule(new TimerTastCus(), 2000+2000*count);
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTastCus(), 2000+2000*count);
while (true) {
System.out.println(new Date().getSeconds());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} }
//PS:下面的代碼中的 count 變量中
//此參數要使用在你匿名內部類中,使用 final 修飾就無法對其值進行修改,
//只能改爲靜態變量
}
}
( 3 ) 線程互斥與同步
在引入多線程後,由於線程執行的異步性,會給系統造成混亂,特別是在急用臨界資源時,如多個線程急用同一臺打印機,會使打印結果交織在一起,難於區分。
當多個線程急用共享變量,表格,鏈表時,可能會導致數據處理出錯,
因此線程同步的主要任務是使併發執行的各線程之間能夠有效的共享資源和相互合作,從而使程序的執行具有可再現性。
當線程併發執行時,由於資源共享和線程協作,使用線程之間會存在以下兩種制約關係。
1. 間接相互制約。一個系統中的多個線程必然要共享某種系統資源,如共享 CPU,共享 I/O 設備,所謂間接相互制約即源於這種資源共享,打印機就是最好的例子,線程 A 在使用打印機時,其它線程都要等待。
2. 直接相互制約。這種制約主要是因爲線程之間的合作,如有線程 A 將計算結果提供給線程 B 作進一步處理,那麼線程 B 在線程 A 將數據送達之前都將處於阻塞狀態。
間接相互制約可以稱爲互斥,直接相互制約可以稱爲同步,對於互斥可以這樣理解,線程 A 和線程 B 互斥訪問某個資源則它們之間就會產個順序問題——要麼線程 A 等待線程 B 操作完畢,要麼線程 B 等待線程操作完畢,這其實就是線程的同步了。
因此同步包括互斥,互斥其實是一種特殊的同步。
下面我們通過一道題來體會線程的交互。
要求:子線程運行執行 10 次後,主線程再運行 5 次。這樣交替執行三遍
package com.hy.多線程高併發;
/**
* @Description: 要求:子線程運行執行 10 次後,主線程再運行 5 次。這樣交替執行三遍
* @Param:
* @return:
* @Author: Hy
* @Date: 2019
*/
public class Test4 {
public static void main(String[] args) {
final Bussiness bussiness = new Bussiness();
//子線程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {//三遍
bussiness.subMethod(); //調用一下這個方法
} }
}).start();
//主線程
for (int i = 0; i < 3; i++) { //三遍
bussiness.mainMethod();
}
}
}
//定義一個類
class Bussiness {
private boolean subFlag = true;
//寫一個方法 mainMethod
public synchronized void mainMethod() {
while (subFlag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
} }
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()
+ " : main thread running loop count -- " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} }
subFlag = true;
notify();
}
//寫一個方法 subMethod
public synchronized void subMethod() {
while (!subFlag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
} }
for (int i = 0; i < 10; i++) {
System.err.println(Thread.currentThread().getName()
+ " : sub thread running loop count -- " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} }
subFlag = false;
notify();
}
}
( 4 ) 線程局部變量 ThreadLocal
ThreadLocal 的作用和目的:用於實現線程內的數據共享,即對於相同的程序代碼,多個模塊在同一個線程中運行時要共享一份數據,而在另外線程中運行時又共享另外一份數據。
每個線程調用全局 ThreadLocal 對象的 set 方法,在 set 方法中,首先根據當前線程獲取當前線程的 ThreadLocalMap 對象,然後往這個 map 中插入一條記錄,key 其實是 ThreadLocal 對象,value 是各自的 set 方法傳進去的值。
也就是每個線程其實都有一份自己獨享的 ThreadLocalMap對象,該對象的 Key 是 ThreadLocal 對象,值是用戶設置的具體值。
在線程結束時可以調用 ThreadLocal.remove()方法,這樣會更快釋放內存,不調用也可以,因爲線程結束後也可以自動釋放相關的 ThreadLocal 變量。
ThreadLocal 的應用場景:
➢ 訂單處理包含一系列操作:減少庫存量、增加一條流水臺賬、修改總賬,這幾個操作要在同一個事務中完成,通常也即同一個線程中進行處理,如果累加公司應收款的操作失敗了,則應該把前面的操作回滾,否則,提交所有操作,這要求這些操作使用相同的數據庫連接對象,而這些操作的代碼分別位於不同的模塊類中。
➢ 銀行轉賬包含一系列操作: 把轉出帳戶的餘額減少,把轉入帳戶的餘額增加,這兩個操作要在同一個事務中完成,它們必須使用相同的數據庫連接對象,轉入和轉出操作的代碼分別是兩個不同的帳戶對象的方法。
➢ 例如 Strut2 的 ActionContext,同一段代碼被不同的線程調用運行時,該代碼操作的數據是每個線程各自的狀態和數據,對於不同的線程來說,getContext 方法拿到的對象都不相同,對同一個線程來說,不管調用 getContext 方法多少次和在哪個模塊中 getContext 方法,拿到的都是同一個。