JAVA併發編程梳理與學習--入門(線程基礎概念解讀)一

引言:編程3年多了,感到自己知識體系零散,把自己知識體系梳理和學習一下,每個體系都由入門–》初步應用–》高階–》源碼分析組成。歡迎大家提意見,共同學習。
併發編程知識體系:線程基礎概念解讀、線程之間的共享和協作、線程併發工具類、原子操作CAS、顯示鎖和AQS、併發容器、線程池、併發安全、JVM、垃圾回收
一、進程和線程的定義
進程:操作系統進行資源(cpu、內存、磁盤I/O等)分配的最小單位。當你運行一個程序,你就啓動了一 個進程,是活的,應用程序是死的。進程和進程之間是相互獨立的,進程是系統進行資源分配和調度的一個獨立單位。每一個進程都有它自己的地址空間,由程序、數據和進程控制塊三部分組成。進程可以分爲系統進 程和用戶進程。
線程:cpu調度的最小單位。不能獨立於進程存在,他是比線程更小的、能獨立運行的基本單位。一個進程可以有多個線程,線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),能共享進程資源(會有線程安全問題),進程是不執行任務的,真正執行任務的是線程。
:任何一個程序都必須要創建線程,特別是 Java 不管任何程序都必須啓動一個 main 函數的主線程; Java Web 開發裏面的定時任務、定時器、JSP 和 Servlet等,任何一個監聽事件, onclick的觸發事件等都離不開線程和併發的知識
二、CPU核心數和線程數的關係
windows系統cpu內核數、邏輯處理器數
一般內核數和我們線程數是1:1關係,一個cpu內核可以執行一個線程,但是intel採用超線程技術使其變成1:2關係。那麼問題來了,我們平時使用電腦好像並沒有受到cpu核心數的限制,我們可以開遠遠大於邏輯處理器數的線程數?那就要說一下,cpu的時間片輪轉機制。
三、cpu的時間片輪轉機制(RR調度)
人的反應時間:人的反應時間最快爲0.2秒左右。一般來說,經過訓練的運動員應該也不會低於0.1秒。
一個1.6G的cpu執行一條指令:大約0.6ns
通過對比,在人反應時間內,cpu大約能執行大約百萬到千萬的指令,所以當我們比如說打開一個網頁時間,cpu會把時間劃分成很多很多片段,然後拿出其中一個片段用來打開網頁,我們肉眼是分辨不出來的,所以我們感覺自己操作電腦好像沒受cpu核心數的限制。所以是我們感覺欺騙了我們。
當然,cpu的時間片輪轉機制也是有代價的,cpu在輪轉時,要做上下文切換,是非常耗費性能的。
所以,我們編寫代碼時間要儘量減少因爲編寫代碼的不合適引起的上下文切換
:假如進程切( processwitch),有時稱爲上下文切換( context switch),需要 10ms, 再假設時間片設爲 100ms,則在做完 100ms 有用的工作之後,CPU 將花費 10ms 來進行 進程切換。CPU 時間的 10%被浪費在了管理開銷上了。
爲了提高 CPU 效率,我們可以將時間片設爲 5000ms。這時浪費的時間只有 0.1%。但考慮到一個9個線程系統中,如果有 10 個交互用戶幾乎同時按下回車鍵, 將發生什麼情況?假設所有其他進程都用足它們的時間片的話,最後一個不幸的進程不得不等待 5s 才獲得運行機會。多數用戶無法忍受一條簡短命令要 5 s才能做出響應。 所以從上面可以看出時間片設得太短會導致過多的上下文切換,降低了 CPU 效率;而設得太長又可能引起對用戶交互請求的響應變差,降低系統和用戶體驗效果。將時間片設爲 100ms通常是一個比較好的。
四、併發和並行
並行:可以同時運行的任務數,確確實實的同時執行
併發:交替執行,但是我們肉眼發覺不到,感覺也是同時執行(上面說的cpu的時間片輪轉機制)
舉個例子:我們排隊打飯,要是排2隊在一個窗口打飯,就是併發。排2隊在2個窗口打飯就是並行
:當談論併發的時候一定要加個單位時間,也就是說單位時間內併發量是多少? 離開了單位時間其實是沒有意義的。
五、高併發編程的意義、好處和注意事項
1.可以充分利用cpu的資源(比如:我們機器可以並行執行8個線程,我們要是隻寫一個單線程程序,那麼其餘7個cpu就不可以充分利用起來)
2.加快相應用戶時間(比如:迅雷下載,開多個下載總歸比開一個下載快)
3.可以使我們的代碼模塊化、異步化、簡單化(比如:電商平臺,我們下訂單後,先減庫存,在發訂單,發短信,發郵件,如果用並行那麼,整個流程就是時間相加。如果用併發,我們可以把他們交給不同模塊區同時去執行)
有優點就會有缺點,畢竟沒有十全十美的。
缺點
1.線程安全問題(共享資源),會引入鎖(後面會提到),但是可能出現死鎖、會有線程之間競爭,在競爭中可能會性能反而下降,甚至還不如單線程的速度快,不要認爲多線程一定會比單線程快,如果寫不好,還不如單線程。
注意:只讀是沒有線程安全問題的,之所以發生線程安全問題,是不同線程對共享的變量進行了寫的操作。
死鎖:不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成,產生死鎖問題。比如:A和B要完成作業,A有筆,B有本子。A拿着筆會去等待本子,B拿着本子會去等待A的筆,結果就是A和B會一直等待下去,進入死鎖狀態。
2.線程是可以提高速度,但是並不意味着可以無限去創建線程(os限制,每創建一個線程jvm要分配棧空間,不調整參數大約1M,每創建一個線程會消耗資源,有可能把服務器搞死,所以會有線程池)
五、java裏的線程
java中啓動線程的方式
有的人說有3種,有的說有2種,我查了jdk源碼,源碼註釋說有2種
在Thread類第73行有說明
1.繼承Thread類
1】定義Thread類的子類,並重寫該類的run()方法,run()方法也稱爲線程執行體。
2】啓動線程,即調用線程的start()方法

public class MyThread extends Thread{//繼承Thread類
        @Override
        public void run(){
            //重寫run方法
        }
    public static void main(String[] args){
        new MyThread().start();

    }
}

2.實現Runnable接口(把Callable和Future創建線程歸到這類裏面)
1】定義Runnable接口的實現類,重寫run()方法。
2】創建Runnable實現類的實例,並用這個實例作爲Thread的參數來創建Thread對象,這個Thread對象纔是真正的線程對象
3】調用線程對象的start()方法來啓動線程

public class MyRunnable implements Runnable {//實現Runnable接口
    @Override
    public void run() {
        //重寫run方法
    }

    public static void main(String[] args){

        MyRunnable myThread=new MyRunnable();

        Thread thread=new Thread(myThread);

        thread.start();
       //或者
       new Thread(new MyRunnable()).start();
    }
}

Callable和Future創建線程
1】創建Callable接口的實現類,並實現call()方法,然後創建該實現類的實例(從java8開始可以直接使用Lambda表達式創建Callable對象)。
2】使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了Callable對象的call()方法的返回值
3】使用FutureTask對象作爲Thread對象的target創建並啓動線程(因爲FutureTask實現了Runnable接口)
4】調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值

public class CallableTest {

    public  static void main(String[] args){

        CallableTest callableTest=new CallableTest();
        FutureTask<Integer> future=new FutureTask<Integer>(
                (Callable<Integer>)()->{ return 5;}
                );

       new Thread(future,"有返回值的線程").start();

        try{
            System.out.println("子線程返回值 : " + future.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

2種創建方法的實質及優缺點
Thread是真正對線程的抽象,Runnable是對任務的抽象或者說是對業務邏輯的抽象。從上面代碼也可以看出,最終啓動都需要Thread來啓動
1.採用實現Runnable、Callable接口的方式創建多線程:
優勢:
(1)線程類只是實現了Runnable接口與Callable接口,還可以繼承其他類。
(2)在這種方式下,多個線程可以共享一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。
劣勢:
編程稍稍複雜,如果需要訪問當前線程,則要用Thread.currentThread()方法。
2.採用繼承Thread類的方法創建多線程:
劣勢:因爲線程類已經繼承了Thread類,所以不能再繼承其他父類。
優勢:編寫簡單,如果需要訪問當前線程,用Thread.currentThread()方法,直接使用this即可獲得當前線程
六、線程終結方法解讀
1.suspend(暫停)、resume(恢復) 和 stop(停止)方法,jdk已廢棄,不建議使用。
原因:以 suspend()方法爲例,在調用後,線程不會釋放已經佔有的資源(比如鎖),而是佔有着資源進入睡眠狀態,這樣容易引發死鎖問題。同樣,stop()方法在終結一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會,因此會導致程序可能工作在不確定狀態下。正因爲 suspend()、 resume()和 stop()方法帶來的副作用,這些方法才被標註爲不建議使用的過期方 法
2.interrupt、static的interrupted、isInterrupted
jdk裏面線程是協作式,不是搶佔式
interrupt是設置線程一箇中端標誌位,被中斷的線程不一定要立即停止正在做的事情。相反,中斷是禮貌地請求另一個線程在它願意並且方便的時候停止它正在做的事情。
isInterrupted和static的interrupted都可以測試線程是否中端,但是有區別。看源碼
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
1》 interrupted會清除中斷位,isInterrupted則不會
2》interrupted()方法爲Thread類的static方法,而isInterrupted()方法不是;
如果一個線程處於了阻塞狀態(如線程調用了 thread.sleep、thread.join、 thread.wait 等),則在線程在檢查中斷標示時如果發現中斷標示爲 true,則會在 這些阻塞方法調用處拋出 InterruptedException 異常,並且在拋出異常後會立即 將線程的中斷標示位清除,即重新設置爲 false。
不建議自定義一個取消標誌位來中止線程的運行。因爲 run 方法裏有阻塞調 用時會無法很快檢測到取消標誌,線程必須從阻塞調用返回後,纔會檢查這個取 消標誌。這種情況下,使用中斷會更好,因爲, 一、一般的阻塞方法,如 sleep 等本身就支持中斷的檢查, 二、檢查中斷位的狀態和檢查取消標誌位沒什麼區別,用中斷位的狀態還可 以避免聲明取消標誌位,減少資源的消耗。 注意:處於死鎖狀態的線程無法被中斷
七、理解run和start方法
先看start源碼
start源碼描述
我們創建線程時,會先new一個線程對象,然後通過start方法來和操作系統交互,所以start是啓動一個線程。
而run方法一般寫業務邏輯,只是一個java方法,在哪個線程調用就是實現哪個線程業務邏輯。
八、線程生命週期
在這裏插入圖片描述
狀態解讀:
新建:使用new方法,new出來的線程;
就緒:調用的線程的start()方法後,這時候線程處於等待CPU分配資源階段,誰先搶的CPU資源,誰開始執行;
運行:就緒的線程被調度並獲得CPU資源時,便進入運行狀態,run方法定義了線程的操作和功能;
阻塞:在運行狀態的時候,可能因爲某些原因導致運行狀態的線程變成了阻塞狀態,比如sleep()、wait()之後線程就處於了阻塞狀態,這個時候需要其他機制將處於阻塞狀態的線程喚醒,比如調用notify或者notifyAll()方法。喚醒的線程不會立刻執行run方法,它們要再次等待CPU分配資源進入運行狀態;
死亡:如果線程正常執行完畢後或線程被提前強制性的終止或出現異常導致結束,那麼線程就要被銷燬,釋放資源;
主要方法解讀
yield():使當前線程讓出 CPU 佔有權,但讓出的時間是不可設定的。也 不會釋放鎖資源。 所有執行 yield()的線程有可能在進入到就緒狀態後會被操作系統再次選中 馬上又被執行。
wait()/notify()/notifyAll():後面再說
join:把指定的線程加入到當前線程,可以將兩個交替執行的線程合併爲順序執行。
線程的優先級
感覺沒啥用。
通過setPriority(1-10)方法來修改優先級。在不同的 JVM 以及操作系統上,線程規劃會 存在差異,有些操作系統甚至會忽略對線程優先級的設定,所以感覺沒啥用,我是從沒用過。
守護線程
定義:Daemon(守護)線程是一種支持型線程,因爲它主要被用作程序中後臺調度以及支持性工作。
注意:當jvm中不存在非守護線程的時候,守護線程也將停止。守護線程finally也不一定起作用,所以在構建 Daemon 線程時,不能依靠 finally 塊中 的內容來確保執行關閉或清理資源的邏輯。
設置方法:Thread.setDaemon(true),比如垃圾回收線程就是 Daemon 線程。

下一篇:https://blog.csdn.net/m0_37611440/article/details/106201225

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章