Java 線程相關(guān)
- 如何創(chuàng)建線程(兩種方式,區(qū)別,使用場景)
- 線程狀態(tài)調(diào)度
- 多線程數(shù)據(jù)共享(會有什么問題,如何實現(xiàn)共享,多線程操作同一個變量會有什么問題,如果不希望有問題怎么做)
- 數(shù)據(jù)傳遞
- 線程池相關(guān)(如何創(chuàng)建線程池,要注意什么(初始化線程內(nèi)部變量),幾種常用的使用方式)
1. 線程創(chuàng)建
通常創(chuàng)建線程有兩種方式,一個是繼承
Thread, 一個是實現(xiàn)Runnable; 下面則分別實現(xiàn)以做演示,然后說一下這兩種的區(qū)別,應(yīng)該如何選擇
創(chuàng)建線程
創(chuàng)建線程和使用的一個小case如下, 注意的是線程啟動是調(diào)用start方法, 而不是 run 方法; 其次實現(xiàn)Runnable 接口的類,啟動依然是放在一個Thread 對象中
public class ThreadCreate {
/**
* 通過繼承 Thread 方式來創(chuàng)建一個新的線程
*/
public static class ThreadExtend extends Thread {
@Test
public void run() {
System.out.println("new extend thread");
}
}
/**
* 通過實現(xiàn) Runnable 方式來創(chuàng)建一個線程
*/
public static class RunnableImplement implements Runnable {
@Override
public void run() {
System.out.println("new runnable thread");
}
}
@Test
public void testCreate() {
new ThreadExtend().start();
new Thread(new RunnableImplement()).start();
System.out.println("main!");
}
}
兩種方式對比
為什么會有兩種方式呢?這兩種的區(qū)別何在?
- 實現(xiàn)是可以有多個的,但是繼承只能有一個父類
- 查看 Runnable 的使用方法,最終是放在一個 Thread里面去執(zhí)行的,所以在多個相同的程序代碼處理一個資源時,這個還是有優(yōu)勢的;但是查看 Thread實際上就是 Runnable的實現(xiàn),同樣可以將一個自定義的Thread對象,創(chuàng)建多個 Thread對象來調(diào)用
通過上面的描述可以知道一點(diǎn),如果你希望數(shù)據(jù)多線程內(nèi)共享,不妨考慮實現(xiàn) Runnable 接口(當(dāng)然繼承Thread也是ok的);如果希望隔離,則不妨考慮繼承Thread (實際上使用 Runnable接口的實現(xiàn)也是ok的,多創(chuàng)建幾個實現(xiàn)類接口對象而已,每個對象放在一個新的Thread中執(zhí)行)
按照個人的理解,網(wǎng)上說的實現(xiàn)Runnable 方便資源共享,更多的是傾向于代碼的共享,通常是一個Runnable對象,放在多個 Thread實例中執(zhí)行;而繼承 Thead類,從出發(fā)點(diǎn)來看,繼承的一般是作為一個獨(dú)立線程來執(zhí)行使用,如果你真要像下面這么做,也不會報錯,也能正常運(yùn)行,只是有點(diǎn)違反設(shè)計理念而已
MyThread extreds Thread {...};
MyThread mythread = new MyThread();
new Thread(mythread).start();
case 舉例
舉一個例子,車站賣票,假設(shè)現(xiàn)在有三個窗口,總共只有30張車票,賣完就不賣了,怎么實現(xiàn)?如果每個窗口有10張車票,各個窗口把自己的賣完了就不賣了,怎么實現(xiàn)?
第一個case,符合數(shù)據(jù)共享的一種場景,那么我們的實現(xiàn)可以如下:
public static class TotalSaleTick implements Runnable {
private int total = 30;
@Override
public void run() {
while (true) {
if (total > 0) {
System.out.println(Thread.currentThread().getName() + "售出一張,剩余:" + --total);
} else {
break;
}
}
}
}
@Test
public void testTotalSale() {
TotalSaleTick totalSaleTick = new TotalSaleTick();
Thread thread1 = new Thread(totalSaleTick, "窗口1");
Thread thread2 = new Thread(totalSaleTick, "窗口2");
Thread thread3 = new Thread(totalSaleTick, "窗口3");
thread1.start();
thread2.start();
thread3.start();
System.out.println("master over!");
}
輸出如下, 基本上每次跑的輸出結(jié)果都不一樣, 可以看出的一點(diǎn)是三個窗口售出的票數(shù)不同,一個問題,上面這種情況,可能造成超賣么?
窗口1售出一張,剩余:29
master over!
窗口2售出一張,剩余:28
窗口2售出一張,剩余:25
窗口2售出一張,剩余:24
窗口1售出一張,剩余:27
窗口3售出一張,剩余:26
窗口1售出一張,剩余:22
窗口1售出一張,剩余:20
窗口1售出一張,剩余:19
窗口1售出一張,剩余:18
窗口1售出一張,剩余:17
窗口1售出一張,剩余:16
窗口1售出一張,剩余:15
窗口1售出一張,剩余:14
窗口1售出一張,剩余:13
窗口1售出一張,剩余:12
窗口1售出一張,剩余:11
窗口1售出一張,剩余:10
窗口2售出一張,剩余:23
窗口2售出一張,剩余:8
窗口2售出一張,剩余:7
窗口2售出一張,剩余:6
窗口2售出一張,剩余:5
窗口2售出一張,剩余:4
窗口1售出一張,剩余:9
窗口3售出一張,剩余:21
窗口3售出一張,剩余:1
窗口3售出一張,剩余:0
窗口1售出一張,剩余:2
窗口2售出一張,剩余:3
第二個case,則顯然更傾向于繼承 Thread 來實現(xiàn)了
public static class SplitSaleTick extends Thread {
private int total = 10;
public SplitSaleTick(String name) {
super(name);
}
@Override
public void run() {
while (true) {
if (total > 0) {
System.out.println(Thread.currentThread().getName() + "售出一張,剩余:" + --total);
} else {
break;
}
}
}
}
@Test
public void testSplitSaleTick() {
SplitSaleTick splitSaleTick1 = new SplitSaleTick("窗口1");
SplitSaleTick splitSaleTick2 = new SplitSaleTick("窗口2");
SplitSaleTick splitSaleTick3 = new SplitSaleTick("窗口3");
splitSaleTick1.start();
splitSaleTick2.start();
splitSaleTick3.start();
System.out.println("master over");
}
/**
* 繼承 Thread 也可以實現(xiàn)共享, 只不過比較惡心而已
*/
@Test
public void testSplitSaleTick2() {
SplitSaleTick splitSaleTick1 = new SplitSaleTick("saleTick");
Thread thread1 = new Thread(splitSaleTick1, "窗口1");
Thread thread2 = new Thread(splitSaleTick1, "窗口2");
Thread thread3 = new Thread(splitSaleTick1, "窗口3");
thread1.start();
thread2.start();
thread3.start();
}
輸出接過如下, 三個窗口可以并發(fā)賣,且每個窗口賣10張,賣完即止
窗口1售出一張,剩余:9
窗口2售出一張,剩余:9
窗口2售出一張,剩余:8
窗口1售出一張,剩余:8
窗口1售出一張,剩余:7
窗口1售出一張,剩余:6
窗口1售出一張,剩余:5
窗口1售出一張,剩余:4
窗口2售出一張,剩余:7
窗口1售出一張,剩余:3
窗口1售出一張,剩余:2
窗口1售出一張,剩余:1
窗口1售出一張,剩余:0
窗口3售出一張,剩余:9
窗口3售出一張,剩余:8
窗口3售出一張,剩余:7
窗口3售出一張,剩余:6
窗口3售出一張,剩余:5
窗口3售出一張,剩余:4
窗口3售出一張,剩余:3
窗口3售出一張,剩余:2
窗口3售出一張,剩余:1
窗口3售出一張,剩余:0
master over
窗口2售出一張,剩余:6
窗口2售出一張,剩余:5
窗口2售出一張,剩余:4
窗口2售出一張,剩余:3
窗口2售出一張,剩余:2
窗口2售出一張,剩余:1
窗口2售出一張,剩余:0
---- test2 輸出 ----
窗口1售出一張,剩余:9
窗口1售出一張,剩余:6
窗口1售出一張,剩余:5
窗口1售出一張,剩余:4
窗口1售出一張,剩余:3
窗口1售出一張,剩余:2
窗口3售出一張,剩余:7
窗口2售出一張,剩余:8
窗口3售出一張,剩余:0
窗口1售出一張,剩余:1
2. 線程狀態(tài)(線程生命周期)
線程創(chuàng)建之后,即調(diào)用了start方法之后,線程是否開始運(yùn)行了?這個運(yùn)行過程是否會暫停呢?如果需要暫停應(yīng)該怎么辦;如果一個線程依賴另一個線程的計算結(jié)果,又該如何處理?

- 創(chuàng)建:新建一個線程對象,如
Thread thd=new Thread() - 就緒:創(chuàng)建了線程對象后,調(diào)用了線程的
start()方法(此時線程知識進(jìn)入了線程隊列,等待獲取CPU服務(wù) ,具備了運(yùn)行的條件,但并不一定已經(jīng)開始運(yùn)行了) - 運(yùn)行:處于就緒狀態(tài)的線程,一旦獲取了CPU資源,便進(jìn)入到運(yùn)行狀態(tài),開始執(zhí)行run()方法里面的邏輯
- 終止:線程的
run()方法執(zhí)行完畢,或者線程調(diào)用了stop()方法,線程便進(jìn)入終止?fàn)顟B(tài) - 阻塞:一個正在執(zhí)行的線程在某些情況系,由于某種原因而暫時讓出了CPU資源,暫停了自己的執(zhí)行,便進(jìn)入了阻塞狀態(tài),如調(diào)用了
sleep()方法 - 線程讓步:
join等待其他線程終止。在當(dāng)前線程中調(diào)用另一個線程的join()方法,則當(dāng)前線程轉(zhuǎn)入阻塞狀態(tài),直到另一個進(jìn)程運(yùn)行結(jié)束,當(dāng)前線程再由阻塞轉(zhuǎn)為就緒狀態(tài)
3. 方法說明
一個Thread實例有一些常用的方法如:
start,sleep,run,yield,join,wait等, 這些方法是干嘛用的,什么場景下使用,使用時需要注意些什么?方法的執(zhí)行,將對應(yīng)線程狀態(tài)進(jìn)行說明
run 方法
run 方法中為具體的線程執(zhí)行的代碼邏輯,一般而言,都不應(yīng)該被直接進(jìn)行調(diào)用
無論我們采用哪種方法創(chuàng)建線程,基本上都是要重寫run 方法,這個方法會在線程執(zhí)行時調(diào)用
start 方法
執(zhí)行該方法之后,線程進(jìn)入就緒狀態(tài),對使用者而言,希望線程執(zhí)行就是調(diào)用的這個方法(注意調(diào)用之后不會立即執(zhí)行)
這個方法的主要目的就是告訴系統(tǒng),我們的線程準(zhǔn)備好了,cpu有空了趕緊來執(zhí)行我們的線程
sleep 方法
睡眠一段時間,這個過程中不會釋放線程持有的鎖, 傳入int類型的參數(shù),表示睡眠多少ms
讓出CUP的使用、目的是不讓當(dāng)前線程獨(dú)自霸占該進(jìn)程所獲的CPU資源,以留一定時間給其他線程執(zhí)行的機(jī)會
我們最常見的一種使用方式是在主線程中直接調(diào)用 Thread.sleep(100) , 表示先等個100ms, 然后再繼續(xù)執(zhí)行
wait 方法
wait()方法是Object類里的方法;當(dāng)一個線程執(zhí)行到wait()方法時,它就進(jìn)入到一個和該對象相關(guān)的等待池中,同時失去(釋放)了對象的機(jī)鎖(暫時失去機(jī)鎖,wait(long timeout)超時時間到后還需要返還對象鎖);其他線程可以訪問
wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當(dāng)前等待池中的線程
通常我們執(zhí)行wait方法是因為當(dāng)前線程的執(zhí)行,可能依賴到其他線程,如登錄線程中,若發(fā)現(xiàn)用戶沒有注冊,則等待,等用戶注冊成功后繼續(xù)走登錄流程(我們不考慮這個邏輯是否符合實際),
這里就可以在登錄線程中調(diào)用 wait方法, 在注冊線程中,在執(zhí)行完畢之后,調(diào)用notify方法通知登錄線程,注冊完畢,然后繼續(xù)進(jìn)行登錄后續(xù)action
yield 方法
暫停當(dāng)前正在執(zhí)行的線程對象,并執(zhí)行其他線程
yield()應(yīng)該做的是讓當(dāng)前運(yùn)行線程回到可運(yùn)行狀態(tài),以允許具有相同優(yōu)先級的其他線程獲得運(yùn)行機(jī)會。因此,使用yield()的目的是讓相同優(yōu)先級的線程之間能適當(dāng)?shù)妮嗈D(zhuǎn)執(zhí)行。但是,實際中無法保證yield()達(dá)到讓步目的,因為讓步的線程還有可能被線程調(diào)度程序再次選中
這個方法的執(zhí)行,有點(diǎn)像一個拿到面包的人對另外幾個人說,我把面包放在桌上,我們從新開始搶,那么下一個拿到面包的還是這些人中的某個(大家機(jī)會均等)
想象不出啥時候會這么干
join 方法
啟動線程后直接調(diào)用,即join()的作用是:“等待該線程終止”,這里需要理解的就是該線程是指的主線程等待子線程的終止。也就是在子線程調(diào)用了join()方法后面的代碼,只有等到子線程結(jié)束了才能執(zhí)行
從上面的描述也可以很容易看出什么場景需要調(diào)用這個方法,主線程和子線程誰先結(jié)束不好說,如果主線程提前結(jié)束了,導(dǎo)致整個應(yīng)用都關(guān)了,這個時候子線程沒執(zhí)行完,就呵呵了;其次就是子線程執(zhí)行一系列計算,主線程會用到計算結(jié)果,那么就可以執(zhí)行這個方法,保證子線程執(zhí)行完畢后再使用計算結(jié)果
4. 數(shù)據(jù)共享
多線程間數(shù)據(jù)共享,當(dāng)多線程公用一個Runnable對象時,這個對象中的成員變量即可以達(dá)到數(shù)據(jù)共享的目的;多線程采用不同的Runnable對象時,數(shù)據(jù)怎么共享
公用 Runnable對象時
上面的售票例子中,其實就有這個場景,上面提出了一個問題,是否會出現(xiàn)超賣的情況?
-
因為我們知道
++不是原子操作, 實際可以拆分為三步:- 內(nèi)存到寄存器
- 寄存器自增
- 寫回內(nèi)存
假設(shè)num為10時, 線程A和線程B都調(diào)用 ++num操作;對于內(nèi)存到寄存器這一步,兩個線程都到了這一步,A自增將11寫回內(nèi)存,B也進(jìn)行自增將11寫會內(nèi)存,這個時候就少+1了
讀一個long,double類型的共享變量時,也不是原子操作,在32位操作系統(tǒng)上對64位的數(shù)據(jù)的讀寫要分兩步完成,每一步取32位數(shù)據(jù),如果有兩個線程同時寫一個變量內(nèi)存,一個進(jìn)程寫低32位,而另一個寫高32位,這樣將導(dǎo)致獲取的64位數(shù)據(jù)是失效的數(shù)據(jù)
在多線程中,共享數(shù)據(jù)的獲取or更新,請確保是原子操作;可以考慮同步鎖(synchronized)修改共享變量,共享變量前添加volatile, 使用原子數(shù)據(jù)類型 AtomicInteger
修改上面的售票代碼如下
public static class TotalSaleTick implements Runnable {
private int total = 30;
@Override
public void run() {
while (true) {
synchronized (this) {
if (total > 0) {
System.out.println(Thread.currentThread().getName() + "售出一張,剩余:" + --total);
} else {
break;
}
}
}
}
}
一個小疑惑,在實際的測試中,即便是上面不加上同步塊,好像也沒有出問題,對于上面的操作可能運(yùn)行很多遍都是正確的, 好像和我們預(yù)期的不相符,有沒有可能是因為總數(shù)太少,導(dǎo)致沖突的機(jī)率變小了?
private AtomicInteger count = new AtomicInteger(0);
private int sum = 3000;
public class MyThread extends Thread {
public void run() {
while (true) {
if (sum > 0) {
count.addAndGet(1);
--sum;
}else {
break;
}
}
System.out.println(Thread.currentThread().getName() + " over " + sum);
}
}
@Test
public void testAdd() throws InterruptedException {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
myThread1.join();
myThread2.join();
System.out.println("num: " + sum + " count: " + count.get());
}
對上面的場景,多運(yùn)行幾次,發(fā)現(xiàn)輸出結(jié)果果然是超賣了
Thread-1 over -1
Thread-0 over -1
num: -1 count: 3008
非公用的 Runnable 對象時
共享全局變量 + 共享局部變量兩種情況,有點(diǎn)區(qū)別
上面的case就是一個共享全局變量的demo,上面出現(xiàn)了并發(fā)沖突,可以如下解決, 針對類進(jìn)行加鎖
public class ThreadShareTest {
private AtomicInteger count = new AtomicInteger(0);
private int sum = 3000;
public class MyThread extends Thread {
public void run() {
while (true) {
if (sum > 0) {
synchronized (ThreadShareTest.class) {
if (sum > 0) {
count.addAndGet(1);
--sum;
}
}
}else {
break;
}
}
System.out.println(Thread.currentThread().getName() + " over " + sum);
}
}
@Test
public void testAdd() throws InterruptedException {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
myThread1.join();
myThread2.join();
System.out.println("num: " + sum + " count: " + count.get());
}
}
共享局部變量,需要注意的是局部變量要求是final, 所以下面的int采用了數(shù)組的形式(基本類型無法修改,引用類型可以改其內(nèi)部的值, 不能改引用)
@Test
public void testAdd2() throws InterruptedException {
final int[] num = {3000};
final AtomicInteger c = new AtomicInteger(0);
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
if (num[0] > 0) {
c.addAndGet(1);
num[0]--;
} else {
break;
}
}
System.out.println(Thread.currentThread().getName() + " over " + num[0]);
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("num: " + num[0] + " count: " + c.get());
}
多運(yùn)行幾次,輸出如下,說明也存在并發(fā)的問題了, 修正方式同樣是加鎖
Thread-0 over -1
Thread-1 over -1
num: -1 count: 3001
修改后的run方法內(nèi)部如下
while (true) {
if (num[0] > 0) {
synchronized (this) {
if (num[0] > 0) {
c.addAndGet(1);
num[0]--;
} else {
break;
}
}
} else {
break;
}
}
線程數(shù)據(jù)隔離
上面是數(shù)據(jù)在多線程中共享,很容易出現(xiàn)的就是并發(fā)問題;還有一個場景就是我希望不存在數(shù)據(jù)共享,線程操作的內(nèi)部變量不影響其他的線程; 最簡單的想法就是一個繼承了Thread的類,其內(nèi)部類正常來講就是隔離的,只要你不把它當(dāng)成
Runnable接口的使用方式就行
使用 ThreadLocal 來保證變量在線程之間的隔離, 下面是一個簡單的演示,兩個線程都是在修改threadLocal中的值, 但是兩個線程的修改,對彼此而言是獨(dú)立的
public static class LocalT implements Runnable {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
@Override
public void run() {
int start = (int) (Math.random() * 100);
for (int i =0 ; i < 100; i = i+2) {
threadLocal.set(start + i);
System.out.println(Thread.currentThread().getName() + " : " + get());
}
}
public int get() {
return threadLocal.get();
}
}
@Test
public void testLocal() throws InterruptedException {
LocalT local = new LocalT();
Thread thread1 = new Thread(local);
Thread thread2 = new Thread(local);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
5. 數(shù)據(jù)傳遞
數(shù)據(jù)如何傳遞給線程,有如何把線程計算的結(jié)果拋出來
傳遞數(shù)據(jù)
比較容易想到的就是在創(chuàng)建對象時,傳入數(shù)據(jù);或者調(diào)用線程對象的setXXX方法傳入數(shù)據(jù), 當(dāng)做正常的對來操作處理即可
需要注意的是,在線程的執(zhí)行期間,你修改了其中的局部變量,會出現(xiàn)什么情況呢?
public static class ThreadData implements Runnable {
private int num = 0;
public void run() {
while (num < 100) {
System.out.println(Thread.currentThread().getName() + " now: " + num++);
}
System.out.println(Thread.currentThread().getName() + " num: " + num);
}
public void setNum(int num) {
System.out.println(this.num + " now set to " + num);
this.num = num;
}
}
@Test
public void testThreadSetData() throws InterruptedException {
ThreadData threadData = new ThreadData();
Thread thread1 = new Thread(threadData);
Thread thread2 = new Thread(threadData);
thread1.start();
thread2.start();
threadData.setNum(200);
thread1.join();
thread1.join();
}
輸出如下, 將num設(shè)置為200之后,并沒有如我們預(yù)期的結(jié)束線程,依然在往下走, 這里就相當(dāng)于是有一個你修改了這個數(shù)據(jù),是否會立馬就生效呢?特別是對其他的線程而言
...
Thread-1 now: 24
Thread-1 now: 25
Thread-0 now: 14
26 now set to 200
Thread-0 now: 27
Thread-0 now: 28
Thread-1 now: 26
Thread-0 now: 29
Thread-1 now: 30
....
輸出結(jié)果
線程執(zhí)行了一個任務(wù)之后,輸出的結(jié)果可以怎么處理
一個實例,一個線程實現(xiàn)累加的過程,我現(xiàn)在希望實現(xiàn)1 加到 100, 開四個線程,怎么做?
下面是一個實現(xiàn),不知道有沒有什么問題
public static class CalculateThread extends Thread {
private int start;
private int end;
private int ans;
public CalculateThread(int start, int end) {
this.start = start;
this.end = end;
}
public void run() {
for (int i = start; i <= end; i++) {
ans += i;
}
}
public int getAns() {
return ans;
}
}
@Test
public void testCalculate() throws InterruptedException {
CalculateThread c1 = new CalculateThread(1, 25);
CalculateThread c2 = new CalculateThread(26, 50);
CalculateThread c3 = new CalculateThread(51, 75);
CalculateThread c4 = new CalculateThread(76, 100);
c1.start();
c2.start();
c3.start();
c4.start();
c1.join();
c2.join();
c3.join();
c4.join();
System.out.println("ans1: " + c1.getAns() + " ans2: " + c2.getAns() + " ans3: " + c3.getAns() + " ans4: " + c4.getAns());
int ans = c1.getAns() + c2.getAns() + c3.getAns() + c4.getAns();
System.out.println("ans : " + ans);
}