- 如何建立線程(兩種方式,區別,使用場景)
- 線程狀態調度
- 多線程數據共享(會有什麼問題,如何實現共享,多線程操做同一個變量會有什麼問題,若是不但願有問題怎麼作)
- 數據傳遞
- 線程池相關(如何建立線程池,要注意什麼(初始化線程內部變量),幾種經常使用的使用方式)
一般建立線程有兩種方式,一個是繼承
Thread
, 一個是實現Runnable
; 下面則分別實現以作演示,而後說一下這兩種的區別,應該如何選擇html
建立線程和使用的一個小case以下, 注意的是線程啓動是調用start
方法, 而不是 run
方法; 其次實現Runnable
接口的類,啓動依然是放在一個Thread
對象中java
public class ThreadCreate { /** * 經過繼承 Thread 方式來建立一個新的線程 */ public static class ThreadExtend extends Thread { @Test public void run() { System.out.println("new extend thread"); } } /** * 經過實現 Runnable 方式來建立一個線程 */ 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!"); } }
爲何會有兩種方式呢?這兩種的區別何在?數組
經過上面的描述能夠知道一點,若是你但願數據多線程內共享,不妨考慮實現 Runnable
接口(固然繼承Thread也是ok的);若是但願隔離,則不妨考慮繼承Thread
(實際上使用 Runnable接口的實現也是ok的,多建立幾個實現類接口對象而已,每一個對象放在一個新的Thread中執行)多線程
按照我的的理解,網上說的實現Runnable
方便資源共享,更多的是傾向於代碼的共享,一般是一個Runnable
對象,放在多個 Thread
實例中執行;而繼承 Thead
類,從出發點來看,繼承的通常是做爲一個獨立線程來執行使用,若是你真要像下面這麼作,也不會報錯,也能正常運行,只是有點違反設計理念而已併發
MyThread extreds Thread {...}; MyThread mythread = new MyThread(); new Thread(mythread).start();
舉一個例子,車站賣票,假設如今有三個窗口,總共只有30張車票,賣完就不賣了,怎麼實現?若是每一個窗口有10張車票,各個窗口把本身的賣完了就不賣了,怎麼實現?dom
第一個case,符合數據共享的一種場景,那麼咱們的實現能夠以下:ide
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!"); }
輸出以下, 基本上每次跑的輸出結果都不同, 能夠看出的一點是三個窗口售出的票數不一樣,一個問題,上面這種狀況,可能形成超賣麼?學習
窗口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
來實現了測試
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 也能夠實現共享, 只不過比較噁心而已 */ @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(); }
輸出接過以下, 三個窗口能夠併發賣,且每一個窗口賣10張,賣完即止this
窗口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
線程建立以後,即調用了start方法以後,線程是否開始運行了?這個運行過程是否會暫停呢?若是須要暫停應該怎麼辦;若是一個線程依賴另外一個線程的計算結果,又該如何處理?
Thread thd=new Thread()
start()
方法(此時線程知識進入了線程隊列,等待獲取CPU服務 ,具有了運行的條件,但並不必定已經開始運行了)run()
方法執行完畢,或者線程調用了stop()
方法,線程便進入終止狀態sleep()
方法join
等待其餘線程終止。在當前線程中調用另外一個線程的join()方法,則當前線程轉入阻塞狀態,直到另外一個進程運行結束,當前線程再由阻塞轉爲就緒狀態一個Thread實例有一些經常使用的方法如:
start
,sleep
,run
,yield
,join
,wait
等, 這些方法是幹嗎用的,什麼場景下使用,使用時須要注意些什麼?方法的執行,將對應線程狀態進行說明
run 方法中爲具體的線程執行的代碼邏輯,通常而言,都不該該被直接進行調用
不管咱們採用哪一種方法建立線程,基本上都是要重寫run
方法,這個方法會在線程執行時調用
執行該方法以後,線程進入就緒狀態,對使用者而言,但願線程執行就是調用的這個方法(注意調用以後不會當即執行)
這個方法的主要目的就是告訴系統,咱們的線程準備好了,cpu有空了趕忙來執行咱們的線程
睡眠一段時間,這個過程當中不會釋放線程持有的鎖, 傳入int類型的參數,表示睡眠多少ms
讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留必定時間給其餘線程執行的機會
咱們最多見的一種使用方式是在主線程中直接調用 Thread.sleep(100)
, 表示先等個100ms, 而後再繼續執行
wait()方法是Object類裏的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機鎖,wait(long timeout)超時時間到後還須要返還對象鎖);其餘線程能夠訪問
wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程
一般咱們執行wait方法是由於當前線程的執行,可能依賴到其餘線程,如登陸線程中,若發現用戶沒有註冊,則等待,等用戶註冊成功後繼續走登陸流程(咱們不考慮這個邏輯是否符合實際),
這裏就能夠在登陸線程中調用 wait方法, 在註冊線程中,在執行完畢以後,調用notify方法通知登陸線程,註冊完畢,而後繼續進行登陸後續action
暫停當前正在執行的線程對象,並執行其餘線程
yield()應該作的是讓當前運行線程回到可運行狀態,以容許具備相同優先級的其餘線程得到運行機會。所以,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。可是,實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中
這個方法的執行,有點像一個拿到麪包的人對另外幾我的說,我把麪包放在桌上,咱們重新開始搶,那麼下一個拿到麪包的仍是這些人中的某個(你們機會均等)
想象不出啥時候會這麼幹
啓動線程後直接調用,即join()的做用是:「等待該線程終止」,這裏須要理解的就是該線程是指的主線程等待子線程的終止。也就是在子線程調用了join()方法後面的代碼,只有等到子線程結束了才能執行
從上面的描述也能夠很容易看出什麼場景須要調用這個方法,主線程和子線程誰先結束很差說,若是主線程提早結束了,致使整個應用都關了,這個時候子線程沒執行完,就呵呵了;其次就是子線程執行一系列計算,主線程會用到計算結果,那麼就能夠執行這個方法,保證子線程執行完畢後再使用計算結果
多線程間數據共享,當多線程公用一個Runnable對象時,這個對象中的成員變量便可以達到數據共享的目的;多線程採用不一樣的Runnable對象時,數據怎麼共享
Runnable
對象時上面的售票例子中,其實就有這個場景,上面提出了一個問題,是否會出現超賣的狀況?
由於咱們知道 ++
不是原子操做, 實際能夠拆分爲三步:
假設num爲10時, 線程A和線程B都調用 ++num操做;對於內存到寄存器這一步,兩個線程都到了這一步,A自增將11寫回內存,B也進行自增將11寫會內存,這個時候就少+1了
讀一個long,double類型的共享變量時,也不是原子操做,在32位操做系統上對64位的數據的讀寫要分兩步完成,每一步取32位數據,若是有兩個線程同時寫一個變量內存,一個進程寫低32位,而另外一個寫高32位,這樣將致使獲取的64位數據是失效的數據
在多線程中,共享數據的獲取or更新,請確保是原子操做;能夠考慮同步鎖(synchronized
)修改共享變量,共享變量前添加volatile
, 使用原子數據類型 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; } } } } }
一個小疑惑,在實際的測試中,即使是上面不加上同步塊,好像也沒有出問題,對於上面的操做可能運行不少遍都是正確的, 好像和咱們預期的不相符,有沒有多是由於總數太少,致使衝突的機率變小了?
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()); }
對上面的場景,多運行幾回,發現輸出結果果真是超賣了
Thread-1 over -1 Thread-0 over -1 num: -1 count: 3008
Runnable
對象時共享全局變量 + 共享局部變量兩種狀況,有點區別
上面的case就是一個共享全局變量的demo,上面出現了併發衝突,能夠以下解決, 針對類進行加鎖
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採用了數組的形式(基本類型沒法修改,引用類型能夠改其內部的值, 不能改引用)
@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()); }
多運行幾回,輸出以下,說明也存在併發的問題了, 修正方式一樣是加鎖
Thread-0 over -1 Thread-1 over -1 num: -1 count: 3001
修改後的run方法內部以下
while (true) { if (num[0] > 0) { synchronized (this) { if (num[0] > 0) { c.addAndGet(1); num[0]--; } else { break; } } } else { break; } }
上面是數據在多線程中共享,很容易出現的就是併發問題;還有一個場景就是我但願不存在數據共享,線程操做的內部變量不影響其餘的線程; 最簡單的想法就是一個繼承了Thread的類,其內部類正常來說就是隔離的,只要你不把它當成
Runnable
接口的使用方式就行
使用 ThreadLocal
來保證變量在線程之間的隔離, 下面是一個簡單的演示,兩個線程都是在修改threadLocal中的值, 可是兩個線程的修改,對彼此而言是獨立的
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(); }
數據如何傳遞給線程,有如何把線程計算的結果拋出來
比較容易想到的就是在建立對象時,傳入數據;或者調用線程對象的setXXX方法傳入數據, 當作正常的對來操做處理便可
須要注意的是,在線程的執行期間,你修改了其中的局部變量,會出現什麼狀況呢?
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設置爲200以後,並無如咱們預期的結束線程,依然在往下走, 這裏就至關因而有一個你修改了這個數據,是否會立馬就生效呢?特別是對其餘的線程而言
... 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 ....
線程執行了一個任務以後,輸出的結果能夠怎麼處理
一個實例,一個線程實現累加的過程,我如今但願實現1 加到 100, 開四個線程,怎麼作?
下面是一個實現,不知道有沒有什麼問題
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); }
多線程技術主要解決處理器單元內多個線程執行的問題,它能夠顯著減小處理器單元的閒置時間,增長處理器單元的吞吐能力 線程的頻繁建立和銷燬可能浪費大量的時間,線程池就是爲了解決這個問題而產生