05.java多線程問題

目錄介紹

  • 5.0.0.1 線程池具備什麼優勢和缺點?爲何說開啓大量的線程,會下降程序的性能,那麼該如何作才能下降性能?
  • 5.0.0.3 線程中start和run方法有什麼區別?wait和sleep方法的不一樣?sleep() 、join()、yield()有什麼區別?
  • 5.0.0.4 用Java手寫一個會致使死鎖的程序,遇到這種問題解決方案是什麼?如何預防死鎖的產生?
  • 5.0.0.5 ThreadLocal(線程變量副本)這個類的做用是什麼?ThreadLocal爲什麼要設計key存儲當前的threadlocal對象?
  • 5.0.0.6 什麼是線程安全?線程安全有那幾個級別?保障線程安全有哪些手段?ReentrantLock和synchronized的區別?
  • 5.0.0.7 Volatile和Synchronized各自用途是什麼?有哪些不一樣點?Synchronize在編譯時如何實現鎖機制?
  • 5.0.0.8 wait()和sleep()的區別?各自有哪些使用場景?怎麼喚醒一個阻塞的線程?Thread.sleep(0)的做用是啥?
  • 5.0.0.9 同步和非同步、阻塞和非阻塞的概念?分別有哪些使用場景?說說你是如何理解他們之間的區別?
  • 5.0.1.0 線程的有哪些狀態?請繪製該狀態的流程圖?講一下線程的執行生命週期流程?線程若是出現了運行時異常會怎麼樣?
  • 5.0.1.1 synchronized鎖什麼?synchronized同步代碼塊還有同步方法本質上鎖住的是誰?爲何?
  • 5.0.1.3 CAS是什麼?CAS原理是什麼?CAS實現原子操做會出現什麼問題?對於多個共享變量CAS能夠保證原子性嗎?
  • 5.0.1.4 假若有n個網絡線程,須要當n個網絡線程完成以後,再去作數據處理,你會怎麼解決這個問題?
  • 5.0.1.5 Runnable接口和Callable接口的區別?Callable中是如何處理線程異常的狀況?如何監測runnable異常?
  • 5.0.1.6 若是提交任務時,線程池隊列已滿,這時會發生什麼?線程調度算法是什麼?
  • 5.0.1.7 什麼是樂觀鎖和悲觀鎖?悲觀鎖機制存在哪些問題?樂觀鎖是如何實現衝突檢測和數據更新?
  • 5.0.1.8 線程類的構造方法、靜態塊是被哪一個線程調用的?同步方法和同步塊,哪一個是更好的選擇?同步的範圍越少越好嗎?
  • 5.0.1.9 synchonized(this)和synchonized(object)區別?Synchronize做用於方法和靜態方法區別?
  • 5.0.2.0 volatile是什麼?volatile的用途是什麼?線程在工做內存進行操做後什麼時候會寫到主內存中?
  • 5.0.2.1 被volatile修飾變量在多線程下如何獲最新值?理解volatile的happens-before關係?多線程下執行volatile讀寫後的內存狀態?
  • 5.0.2.2 Volatile實現原理?一個int變量,用volatile修飾,多線程去操做++,線程安全嗎?那如何才能保證i++線程安全?
  • 5.0.2.3 在Java內存模型有哪些能夠保證併發過程的原子性、可見性和有序性的措施?

好消息

  • 博客筆記大彙總【15年10月到至今】,包括Java基礎及深刻知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug彙總,固然也在工做之餘收集了大量的面試題,長期更新維護而且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計500篇[近100萬字],將會陸續發表到網上,轉載請註明出處,謝謝!
  • 連接地址:github.com/yangchong21…
  • 若是以爲好,能夠star一下,謝謝!固然也歡迎提出建議,萬事起於忽微,量變引發質變!全部博客將陸續開源到GitHub!

5.0.0.1 線程池具備什麼優勢和缺點?爲何說開啓大量的線程,會下降程序的性能,那麼該如何作才能下降性能?

  • 線程池好處:
    • 1)下降資源消耗;
    • 2)提升相應速度;
    • 3)提升線程的可管理性。技術博客大總結
  • 線程池的實現原理:
    • 當提交一個新任務到線程池時,判斷核心線程池裏的線程是否都在執行。若是不是,則建立一個新的線程執行任務。若是核心線程池的線程都在執行任務,則進入下個流程。
    • 判斷工做隊列是否已滿。若是未滿,則將新提交的任務存儲在這個工做隊列裏。若是工做隊列滿了,則進入下個流程。
    • 判斷線程池是否都處於工做狀態。若是沒有,則建立一個新的工做線程來執行任務。若是滿了,則交給飽和策略來處理這個任務。
  • 線程池是如何提升性能的?

5.0.0.3 線程中start和run方法有什麼區別?wait和sleep方法的不一樣?sleep() 、join()、yield()有什麼區別?

  • 線程中start和run方法有什麼區別
    • 爲何咱們調用start()方法時會執行run()方法,爲何咱們不能直接調用run()方法?這是一個很是經典的java多線程面試問題。當你調用start()方法時你將建立新的線程,而且執行在run()方法裏的代碼。可是若是你直接調用run()方法,它不會建立新的線程也不會執行調用線程的代碼。
  • wait和sleep方法的不一樣
    • 最大的不一樣是在等待時wait會釋放鎖,而sleep一直持有鎖。Wait一般被用於線程間交互,sleep一般被用於暫停執行。
  • 一、sleep()方法
    • 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操做受到系統計時器和調度程序精度和準確性的影響。 讓其餘線程有機會繼續執行,但它並不釋放對象鎖。也就是若是有Synchronized同步塊,其餘線程仍然不能訪問共享數據。注意該方法要捕獲異常
    • 好比有兩個線程同時執行(沒有Synchronized),一個線程優先級爲MAX_PRIORITY,另外一個爲MIN_PRIORITY,若是沒有Sleep()方法,只有高優先級的線程執行完成後,低優先級的線程才能執行;但當高優先級的線程sleep(5000)後,低優先級就有機會執行了。
    • 總之,sleep()可使低優先級的線程獲得執行的機會,固然也可讓同優先級、高優先級的線程有執行的機會。
  • 二、yield()方法技術博客大總結
    • yield()方法和sleep()方法相似,也不會釋放「鎖標誌」,區別在於,它沒有參數,即yield()方法只是使當前線程從新回到可執行狀態,因此執行yield()的線程有可能在進入到可執行狀態後立刻又被執行,另外yield()方法只能使同優先級或者高優先級的線程獲得執行機會,這也和sleep()方法不一樣。
  • 三、join()方法
    • Thread的非靜態方法join()讓一個線程B「加入」到另一個線程A的尾部。在A執行完畢以前,B不能工做。
    • Thread t = new MyThread(); t.start(); t.join();保證當前線程中止執行,直到該線程所加入的線程完成爲止。然而,若是它加入的線程沒有存活,則當前線程不須要中止。
  • Thread的join()有什麼做用?
    • Thread的join()的含義是等待該線程終止,即將掛起調用線程的執行,直到被調用的對象完成它的執行。好比存在兩個線程t1和t2,下述代碼表示先啓動t1,直到t1的任務結束,才輪到t2啓動。
    t1.start();
    t1.join(); 
    t2.start();
    複製代碼

5.0.0.4 用Java手寫一個會致使死鎖的程序,遇到這種問題解決方案是什麼?如何預防死鎖的產生?

  • 死鎖是怎麼一回事
    • 線程A和線程B相互等待對方持有的鎖致使程序無限死循環下去。
  • 深刻理解死鎖的原理
    • 兩個線程裏面分別持有兩個Object對象:lock1和lock2。這兩個lock做爲同步代碼塊的鎖;
    • 線程1的run()方法中同步代碼塊先獲取lock1的對象鎖,Thread.sleep(xxx),時間不須要太多,50毫秒差很少了,而後接着獲取lock2的對象鎖。這麼作主要是爲了防止線程1啓動一會兒就連續得到了lock1和lock2兩個對象的對象鎖
    • 線程2的run)(方法中同步代碼塊先獲取lock2的對象鎖,接着獲取lock1的對象鎖,固然這時lock1的對象鎖已經被線程1鎖持有,線程2確定是要等待線程1釋放lock1的對象鎖的
  • 死鎖的簡單代碼
    • 思路是建立兩個字符串a和b,再建立兩個線程A和B,讓每一個線程都用synchronized鎖住字符串(A先鎖a,再去鎖b;B先鎖b,再鎖a),若是A鎖住a,B鎖住b,A就沒辦法鎖住b,B也沒辦法鎖住a,這時就陷入了死鎖。
    • 打印結果:能夠看到,Lock1獲取obj1,Lock2獲取obj2,可是它們都沒有辦法再獲取另一個obj,由於它們都在等待對方先釋放鎖,這時就是死鎖。
    public class DeadLock {
        public static String obj1 = "obj1";
        public static String obj2 = "obj2";
        public static void main(String[] args){
            Thread a = new Thread(new Lock1());
            Thread b = new Thread(new Lock2());
            a.start();
            b.start();
        }    
    }
    class Lock1 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock1 running");
                while(true){
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock1 lock obj1");
                        Thread.sleep(3000);//獲取obj1後先等一下子,讓Lock2有足夠的時間鎖住obj2
                        synchronized(DeadLock.obj2){
                            System.out.println("Lock1 lock obj2");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    class Lock2 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock2 running");
                while(true){
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock2 lock obj2");
                        Thread.sleep(3000);
                        synchronized(DeadLock.obj1){
                            System.out.println("Lock2 lock obj1");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    複製代碼
    • 若是咱們只運行Lock1呢?修改一下main函數,把線程b註釋掉。
  • 如何預防死鎖的產生?
    • 死鎖發生時的四個必要條件,只要破壞這四個必要條件中的任意一個條件,死鎖就不會發生。這就爲咱們解決死鎖問題提供了可能。通常地,解決死鎖的方法分爲死鎖的預防,避免,檢測[定位死鎖的位置]與恢復三種(注意:死鎖的檢測與恢復是一個方法)。 鎖的預防是保證系統不進入死鎖狀態的一種策略。它的基本思想是要求進程申請資源時遵循某種協議,從而打破產生死鎖的四個必要條件中的一個或幾個,保證系統不會進入死鎖狀態。
    • 打破互斥條件。即容許進程同時訪問某些資源。可是,有的資源是不容許被同時訪問的,像打印機等等,這是由資源自己的屬性所決定的。因此,這種辦法並沒有實用價值。
    • 打破不可搶佔條件。即容許進程強行從佔有者那裏奪取某些資源。就是說,當一個進程已佔有了某些資源,它又申請新的資源,但不能當即被知足時,它必須釋放所佔有的所有資源,之後再從新申請。它所釋放的資源能夠分配給其它進程。這就至關於該進程佔有的資源被隱蔽地強佔了。這種預防死鎖的方法實現起來困難,會下降系統性能。
    • 打破佔有且申請條件。能夠實行資源預先分配策略。即進程在運行前一次性地向系統申請它所須要的所有資源。若是某個進程所需的所有資源得不到知足,則不分配任何資源,此進程暫不運行。只有當系統可以知足當前進程的所有資源需求時,才一次性地將所申請的資源所有分配給該進程。因爲運行的進程已佔有了它所需的所有資源,因此不會發生佔有資源又申請資源的現象,所以不會發生死鎖。可是,這種策略也有以下缺點:
      • 在許多狀況下,一個進程在執行以前不可能知道它所須要的所有資源。這是因爲進程在執行時是動態的,不可預測的;
      • 資源利用率低。不管所分資源什麼時候用到,一個進程只有在佔有所需的所有資源後才能執行。即便有些資源最後才被該進程用到一次,但該進程在生存期間卻一直佔有它們,形成長期佔着不用的情況。這顯然是一種極大的資源浪費;
      • 下降了進程的併發性。由於資源有限,又加上存在浪費,能分配到所需所有資源的進程個數就必然少了。
    • 打破循環等待條件,實行資源有序分配策略。採用這種策略,即把資源事先分類編號,按號分配,使進程在申請,佔用資源時不會造成環路。全部進程對資源的請求必須嚴格按資源序號遞增的順序提出。進程佔用了小號資源,才能申請大號資源,就不會產生環路,從而預防了死鎖。這種策略與前面的策略相比,資源的利用率和系統吞吐量都有很大提升,可是也存在如下缺點:
      • 限制了進程對資源的請求,同時給系統中全部資源合理編號也是件困難事,並增長了系統開銷;
      • 爲了遵循按編號申請的次序,暫不使用的資源也須要提早申請,從而增長了進程對資源的佔用時間。

5.0.0.5 ThreadLocal(線程變量副本)這個類的做用是什麼?ThreadLocal爲什麼要設計key存儲當前的threadlocal對象?

  • ThreadLocal即線程變量
    • ThreadLocal爲每一個線程維護一個本地變量。
    • 採用空間換時間,它用於線程間的數據隔離,它爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其它線程所對應的副本。從線程的角度看,目標變量就象是線程的本地變量,這也是類名中「Local」所要表達的意思。ThreadLocal的實現是以ThreadLocal對象爲鍵。任意對象爲值得存儲結構。這個結構被附帶在線程上,也就是說一個線程能夠根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
  • ThreadLocal類是一個Map
    • ThreadLocal類中維護一個Map,用於存儲每個線程的變量副本,Map中元素的鍵爲線程對象,而值爲對應線程的變量副本。
    • ThreadLocal在Spring中發揮着巨大的做用,在管理Request做用域中的Bean、事務管理、任務調度、AOP等模塊都出現了它的身影。
    • Spring中絕大部分Bean均可以聲明成Singleton做用域,採用ThreadLocal進行封裝,所以有狀態的Bean就可以以singleton的方式在多線程中正常工做了。
  • 更多詳細參考博客:深刻研究java.lang.ThreadLocal類

5.0.0.6 什麼是線程安全?線程安全有那幾個級別?保障線程安全有哪些手段?ReentrantLock和synchronized的區別?

  • 什麼是線程安全
    • 線程安全就是當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象是線程安全的。
  • 線程安全也是有幾個級別
    • 技術博客大總結
    • 不可變:
      • 像String、Integer、Long這些,都是final類型的類,任何一個線程都改變不了它們的值,要改變除非新建立一個,所以這些不可變對象不須要任何同步手段就能夠直接在多線程環境下使用
    • 絕對線程安全
      • 無論運行時環境如何,調用者都不須要額外的同步措施。要作到這一點一般須要付出許多額外的代價,Java中標註本身是線程安全的類,實際上絕大多數都不是線程安全的,不過絕對線程安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet
    • 相對線程安全
      • 相對線程安全也就是咱們一般意義上所說的線程安全,像Vector這種,add、remove方法都是原子操做,不會被打斷,但也僅限於此,若是有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的狀況下都會出現ConcurrentModificationException,也就是fail-fast機制。
    • 線程非安全技術博客大總結
      • ArrayList、LinkedList、HashMap等都是線程非安全的類.
  • 保障線程安全有哪些手段。保證線程安全可從多線程三特性出發:
    • 原子性(Atomicity):單個或多個操做是要麼所有執行,要麼都不執行
      • Lock:保證同時只有一個線程能拿到鎖,並執行申請鎖和釋放鎖的代碼
      • synchronized:對線程加獨佔鎖,被它修飾的類/方法/變量只容許一個線程訪問
    • 可見性(Visibility):當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改
      • volatile:保證新值能當即同步到主內存,且每次使用前當即從主內存刷新;
      • synchronized:在釋放鎖以前會將工做內存新值更新到主存中
    • 有序性(Ordering):程序代碼按照指令順序執行
      • volatile: 自己就包含了禁止指令重排序的語義
      • synchronized:保證一個變量在同一個時刻只容許一條線程對其進行lock操做,使得持有同一個鎖的兩個同步塊只能串行地進入
  • ReentrantLock和synchronized的區別
    • ReentrantLock與synchronized的不一樣在於ReentrantLock:
      • 等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其餘事情。
      • 公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖。而synchronized是非公平的,即在鎖被釋放時,任何一個等待鎖的線程都有機會得到鎖。ReentrantLock默認狀況下也是非公平的,但能夠經過帶布爾值的構造函數改用公平鎖。
      • 鎖綁定多個條件:一個ReentrantLock對象能夠經過屢次調用newCondition()同時綁定多個Condition對象。而在synchronized中,鎖對象wait()和notify()或notifyAl()只能實現一個隱含的條件,若要和多於一個的條件關聯不得不額外地添加一個鎖。
    • Synchronized是悲觀鎖機制,獨佔鎖。而Locks.ReentrantLock是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。
      • ReentrantLock適用場景
      • 某個線程在等待一個鎖的控制權的這段時間須要中斷
      • 須要分開處理一些wait-notify,ReentrantLock裏面的Condition應用,可以控制notify哪一個線程,鎖能夠綁定多個條件。
      • 具備公平鎖功能,每一個到來的線程都將排隊等候。
    • 更多詳細參考博客:Lock與synchronized 的區別

5.0.0.7 Volatile和Synchronized各自用途是什麼?有哪些不一樣點?Synchronize在編譯時如何實現鎖機制?

  • Volatile和Synchronized各自用途是什麼?有哪些不一樣點?
    • 1 粒度不一樣,前者針對變量 ,後者鎖對象和類
    • 2 syn阻塞,volatile線程不阻塞
    • 3 syn保證三大特性,volatile不保證原子性
    • 4 syn編譯器優化,volatile不優化 volatile具有兩種特性:
      • 1.保證此變量對全部線程的可見性,指一條線程修改了這個變量的值,新值對於其餘線程來講是可見的,但並非多線程安全的。
      • 2.禁止指令重排序優化。
    • Volatile如何保證內存可見性:
      • 1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
      • 2.當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
    • 同步:就是一個任務的完成須要依賴另一個任務,只有等待被依賴的任務完成後,依賴任務才能完成。
    • 異步:不須要等待被依賴的任務完成,只是通知被依賴的任務要完成什麼工做,只要本身任務完成了就算完成了,被依賴的任務是否完成會通知回來。(異步的特色就是通知)。 打電話和發短信來比喻同步和異步操做。
    • 阻塞:CPU停下來等一個慢的操做完成之後,纔會接着完成其餘的工做。
    • 非阻塞:非阻塞就是在這個慢的執行時,CPU去作其餘工做,等這個慢的完成後,CPU纔會接着完成後續的操做。
    • 非阻塞會形成線程切換增長,增長CPU的使用時間能不能補償系統的切換成本須要考慮。
  • Synchronize在編譯時如何實現鎖機制?
    • Synchronized進過編譯,會在同步塊的先後分別造成monitorenter和monitorexit這個兩個字節碼指令。在執行monitorenter指令時,首先要嘗試獲取對象鎖。若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計算器爲0時,鎖就被釋放了。若是獲取對象鎖失敗,那當前線程就要阻塞,直到對象鎖被另外一個線程釋放爲止。

5.0.0.8 wait()和sleep()的區別?各自有哪些使用場景?怎麼喚醒一個阻塞的線程?Thread.sleep(0)的做用是啥?

  • sleep來自Thread類,和wait來自Object類
    • 調用sleep()方法的過程當中,線程不會釋放對象鎖。而調用wait方法線程會釋放對象鎖
    • sleep睡眠後不出讓系統資源,wait讓出系統資源其餘線程能夠佔用CPU
    • sleep(milliseconds)須要指定一個睡眠時間,時間一到會自動喚醒
  • 通俗解釋
    • Java程序中wait 和 sleep都會形成某種形式的暫停,它們能夠知足不一樣的須要。wait()方法用於線程間通訊,若是等待條件爲真且其它線程被喚醒時它會釋放鎖,而 sleep()方法僅僅釋放CPU資源或者讓當前線程中止執行一段時間,但不會釋放鎖。
  • 怎麼喚醒一個阻塞的線程?
    • 若是線程是由於調用了wait()、sleep()或者join()方法而致使的阻塞,能夠中斷線程,而且經過拋出InterruptedException來喚醒它;若是線程遇到了IO阻塞,無能爲力,由於IO是操做系統實現的,Java代碼並無辦法直接接觸到操做系統。
    • 技術博客大總結
  • Thread.sleep(0)的做用是啥?
    • 因爲Java採用搶佔式的線程調度算法,所以可能會出現某條線程經常獲取到CPU控制權的狀況,爲了讓某些優先級比較低的線程也能獲取到CPU控制權,可使用Thread.sleep(0)手動觸發一次操做系統分配時間片的操做,這也是平衡CPU控制權的一種操做。

5.0.0.9 同步和非同步、阻塞和非阻塞的概念?分別有哪些使用場景?說說你是如何理解他們之間的區別?

  • 同步和非同步
    • 同步和異步體現的是消息的通知機制:所謂同步,方法A調用方法B後必須等到方法B返回結果才能繼續後面的操做;所謂異步,方法A調用方法B後可以讓方法B在調用結束後經過回調等方式通知方法A
  • 阻塞和非阻塞
    • 阻塞和非阻塞側重於等待消息時的狀態:所謂阻塞,就是在結果返回以前讓當前線程掛起;所謂非阻塞,就是在等待時可作其餘事情,經過輪詢去詢問是否已返回結果

5.0.1.0 線程的有哪些狀態?請繪製該狀態的流程圖?講一下線程的執行生命週期流程?線程若是出現了運行時異常會怎麼樣?

  • 在任意一個時間點,一個線程只能有且只有其中的一種狀態
    • 新建(New):線程建立後還沒有啓動
    • 技術博客大總結
    • 運行(Runable):包括正在執行(Running)和等待着CPU爲它分配執行時間(Ready)兩種 無限期等待(Waiting):該線程不會被分配CPU執行時間,要等待被其餘線程顯式地喚醒。如下方法會讓線程陷入無限期等待狀態:
    沒有設置Timeout參數的Object.wait()
    沒有設置Timeout參數的Thread.join()
    LockSupport.park()
    複製代碼
    • 限期等待(Timed Waiting):該線程不會被分配CPU執行時間,但在必定時間後會被系統自動喚醒。如下方法會讓線程進入限期等待狀態:
    Thread.sleep()
    設置了Timeout參數的Object.wai()
    設置了Timeout參數的Thread.join()
    LockSupport.parkNanos()
    LockSupport.parkUntil()
    複製代碼
    • 阻塞(Blocked):線程被阻塞。和等待狀態不一樣的是,阻塞狀態表示在等待獲取到一個排他鎖,在另一個線程放棄這個鎖的時候發生;而等待狀態表示在等待一段時間或者喚醒動做的發生,在程序等待進入同步區域的時候發生。
    • 結束(Terminated):線程已經結束執行
  • 繪製該狀態的流程圖
  • 線程若是出現了運行時異常會怎麼樣?
    • 若是這個異常沒有被捕獲的話,這個線程就中止執行了。另外重要的一點是:若是這個線程持有某個某個對象的監視器,那麼這個對象監視器會被當即釋放

5.0.1.1 synchronized鎖什麼?synchronized同步代碼塊還有同步方法本質上鎖住的是誰?爲何?

  • synchronized鎖什麼
    • 對於普通同步方法,鎖是當前實例對象;
    • 對於靜態同步方法,鎖是當前類的Class對象;
    • 對於同步方法塊,鎖是括號中配置的對象;
    • 當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖。synchronized用的鎖是存在Java對象頭裏的MarkWord,一般是32bit或者64bit,其中最後2bit表示鎖標誌位。
  • 本質上鎖住的是對象。
    • 在java虛擬機中,每一個對象和類在邏輯上都和一個監視器相關聯,synchronized本質上是對一個對象監視器的獲取。當執行同步代碼塊或同步方法時,執行方法的線程必須先得到該對象的監視器,才能進入同步代碼塊或同步方法;而沒有獲取到的線程將會進入阻塞隊列,直到成功獲取對象監視器的線程執行結束並釋放鎖後,纔會喚醒阻塞隊列的線程,使其從新嘗試對對象監視器的獲取。

5.0.1.3 CAS原理是什麼?CAS實現原子操做會出現什麼問題?對於多個共享變量CAS能夠保證原子性嗎?

  • CAS原理是什麼
    • CAS即compare and swap的縮寫,中文翻譯成比較並交換。CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。自旋就是不斷嘗試CAS操做直到成功爲止。
  • CAS實現原子操做會出現什麼問題
    • ABA問題。由於CAS須要在操做之的時候,檢查值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成,有變成A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但實際上發生了變化。ABA問題能夠經過添加版本號來解決。Java 1.5開始,JDK的Atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。
    • 循環時間長開銷大。pause指令優化。
    • 只能保證一個共享變量的原子操做。能夠合併成一個對象進行CAS操做。

5.0.1.4 假若有n個網絡線程,須要當n個網絡線程完成以後,再去作數據處理,你會怎麼解決?

  • 多線程同步的問題。這種狀況能夠可使用thread.join();join方法會阻塞直到thread線程終止才返回。更復雜一點的狀況也可使用CountDownLatch,CountDownLatch的構造接收一個int參數做爲計數器,每次調用countDown方法計數器減一。作數據處理的線程調用await方法阻塞直到計數器爲0時。

5.0.1.5 Runnable接口和Callable接口的區別?Callable中是如何處理線程異常的狀況?如何監測runnable異常?

  • Runnable接口和Callable接口的區別
    • Runnable接口中的run()方法的返回值是void,它作的事情只是純粹地去執行run()方法中的代碼而已;Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合能夠用來獲取異步執行的結果。
    • 這實際上是頗有用的一個特性,由於多線程相比單線程更難、更復雜的一個重要緣由就是由於多線程充滿着未知性,某條線程是否執行了?某條線程執行了多久?某條線程執行的時候咱們指望的數據是否已經賦值完畢?沒法得知,咱們能作的只是等待這條多線程的任務執行完畢而已。而Callable+Future/FutureTask卻能夠獲取多線程運行的結果,能夠在等待時間太長沒獲取到須要的數據的狀況下取消該線程的任務,真的是很是有用。

5.0.1.6 若是提交任務時,線程池隊列已滿,這時會發生什麼?線程調度算法是什麼?

  • 若是提交任務時,線程池隊列已滿,這時會發生什麼?
    • 若是使用的是無界隊列LinkedBlockingQueue,也就是無界隊列的話,不要緊,繼續添加任務到阻塞隊列中等待執行,由於LinkedBlockingQueue能夠近乎認爲是一個無窮大的隊列,能夠無限存聽任務
    • 技術博客大總結
    • 若是使用的是有界隊列好比ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,會根據maximumPoolSize的值增長線程數量,若是增長了線程數量仍是處理不過來,ArrayBlockingQueue繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,默認是AbortPolicy
  • 線程調度算法是什麼?
    • 搶佔式。一個線程用完CPU以後,操做系統會根據線程優先級、線程飢餓狀況等數據算出一個總的優先級並分配下一個時間片給某個線程執行。

5.0.1.7 什麼是樂觀鎖和悲觀鎖?悲觀鎖機制存在哪些問題?樂觀鎖是如何實現衝突檢測和數據更新?

  • 什麼是樂觀鎖和悲觀鎖?
    • 樂觀鎖:就像它的名字同樣,對於併發間操做產生的線程安全問題持樂觀狀態,樂觀鎖認爲競爭不老是會發生,所以它不須要持有鎖,將比較-替換這兩個動做做爲一個原子操做嘗試去修改內存中的變量,若是失敗則表示發生衝突,那麼就應該有相應的重試邏輯。
    • 悲觀鎖:仍是像它的名字同樣,對於併發間操做產生的線程安全問題持悲觀狀態,悲觀鎖認爲競爭老是會發生,所以每次對某資源進行操做時,都會持有一個獨佔的鎖,就像synchronized,直接上了鎖就操做資源。

5.0.1.8 線程類的構造方法、靜態塊是被哪一個線程調用的?同步方法和同步塊,哪一個是更好的選擇?同步的範圍越少越好嗎?

  • 線程類的構造方法、靜態塊是被哪一個線程調用的?
    • 線程類的構造方法、靜態塊是被new這個線程類所在的線程所調用的,而run方法裏面的代碼纔是被線程自身所調用的。
  • 舉個例子
    • 假設Thread2中new了Thread1,main函數中new了Thread2,那麼:
      • Thread2的構造方法、靜態塊是main線程調用的,Thread2的run()方法是Thread2本身調用的
      • Thread1的構造方法、靜態塊是Thread2調用的,Thread1的run()方法是Thread1本身調用的
  • 同步方法和同步塊,哪一個是更好的選擇?
    • 同步塊,這意味着同步塊以外的代碼是異步執行的,這比同步整個方法更提高代碼的效率。請知道一條原則:同步的範圍越小越好。
    • 技術博客大總結
  • 同步的範圍越少越好嗎?
    • 是的。雖然說同步的範圍越少越好,可是在Java虛擬機中仍是存在着一種叫作鎖粗化的優化方法,這種方法就是把同步範圍變大。這是有用的,比方說StringBuffer,它是一個線程安全的類,天然最經常使用的append()方法是一個同步方法,咱們寫代碼的時候會反覆append字符串,這意味着要進行反覆的加鎖->解鎖,這對性能不利,由於這意味着Java虛擬機在這條線程上要反覆地在內核態和用戶態之間進行切換,所以Java虛擬機會將屢次append方法調用的代碼進行一個鎖粗化的操做,將屢次的append的操做擴展到append方法的頭尾,變成一個大的同步塊,這樣就減小了加鎖-->解鎖的次數,有效地提高了代碼執行的效率。

5.0.1.9 synchonized(this)和synchonized(object)區別?Synchronize做用於方法和靜態方法區別?

  • synchonized(this)和synchonized(object)區別技術博客大總結
    • 其實並無很大的區別,synchonized(object)自己就包含synchonized(this)這種狀況,使用的場景都是對一個代碼塊進行加鎖,效率比直接在方法名上加synchonized高一些(下面分析),惟一的區別就是對象的不一樣。
    • 對synchronized(this)的一些理解
      • 1、當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程獲得執行。另外一個線程必須等待當前線程執行完這個代碼塊之後才能執行該代碼塊。
      • 2、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另外一個線程仍然能夠訪問該object中的非synchronized(this)同步代碼塊。
      • 3、尤爲關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其餘線程對object中全部其它synchronized(this)同步代碼塊的訪問將被阻塞。
      • 4、當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就得到了這個object的對象鎖。結果,其它線程對該object對象全部同步代碼部分的訪問都被暫時阻塞。
  • Synchronize做用於方法和靜態方法區別
    • 測試代碼以下所示
    private void test() {
        final TestSynchronized test1 = new TestSynchronized();
        final TestSynchronized test2 = new TestSynchronized();
        Thread t1 = new Thread(new Runnable() {
    
            @Override
            public void run() {
                test1.method01("a");
                //test1.method02("a");
            }
        });
        Thread t2 = new Thread(new Runnable() {
    
            @Override
            public void run() {
                test2.method01("b");
                //test2.method02("a");
            }
        });
        t1.start();
        t2.start();
    }
    
    private static class TestSynchronized{
        private int num1;
        public synchronized void method01(String arg) {
            try {
                if("a".equals(arg)){
                    num1 = 100;
                    System.out.println("tag a set number over");
                    Thread.sleep(1000);
                }else{
                    num1 = 200;
                    System.out.println("tag b set number over");
                }
                System.out.println("tag = "+ arg + ";num ="+ num1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        private static int  num2;
        public static synchronized void method02(String arg) {
            try {
                if("a".equals(arg)){
                    num2 = 100;
                    System.out.println("tag a set number over");
                    Thread.sleep(1000);
                }else{
                    num2 = 200;
                    System.out.println("tag b set number over");
                }
                System.out.println("tag = "+ arg + ";num ="+ num2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    //調用method01方法打印日誌【普通方法】
    tag a set number over
    tag b set number over
    tag = b;num =200
    tag = a;num =100
    
    
    //調用method02方法打印日誌【static靜態方法】
    tag a set number over
    tag = a;num =100
    tag b set number over
    tag = b;num =200
    複製代碼
    • 在static方法前加synchronized:靜態方法屬於類方法,它屬於這個類,獲取到的鎖,是屬於類的鎖。
    • 在普通方法前加synchronized:非static方法獲取到的鎖,是屬於當前對象的鎖。 技術博客大總結
    • 結論:類鎖和對象鎖不一樣,synchronized修飾不加static的方法,鎖是加在單個對象上,不一樣的對象沒有競爭關係;修飾加了static的方法,鎖是加載類上,這個類全部的對象競爭一把鎖。

5.0.2.0 volatile是什麼?volatile的用途是什麼?線程在工做內存進行操做後什麼時候會寫到主內存中?

  • volatile是什麼?
    • 輕量級鎖。synchronized是阻塞式同步,在線程競爭激烈的狀況下會升級爲重量級鎖。而volatile就能夠說是java虛擬機提供的最輕量級的同步機制。
  • volatile的用途是什麼?
    • 被volatile修飾的變量可以保證每一個線程可以獲取該變量的最新值,從而避免出現數據髒讀的現象。
  • 線程在工做內存進行操做後什麼時候會寫到主內存中?
    • Java內存模型告訴咱們,各個線程會將共享變量從主內存中拷貝到工做內存,而後執行引擎會基於工做內存中的數據進行操做處理。
    • 這個時機對普通變量是沒有規定的,而針對volatile修飾的變量給java虛擬機特殊的約定,線程對volatile變量的修改會馬上被其餘線程所感知,即不會出現數據髒讀的現象,從而保證數據的「可見性」。
  • 被volatile修飾變量生成彙編代碼有何特色?
    • 在生成彙編代碼時會在volatile修飾的共享變量進行寫操做的時候會多出Lock前綴的指令
    • 這個Lock指令確定有神奇的地方,那麼Lock前綴的指令在多核處理器下會發現什麼事情了?主要有這兩個方面的影響:
      • 1.將當前處理器緩存行的數據寫回系統內存;
      • 2.這個寫回內存的操做會使得其餘CPU裏緩存了該內存地址的數據無效

5.0.2.1 被volatile修飾變量在多線程下如何獲最新值?理解volatile的happens-before關係?多線程下執行volatile讀寫後的內存狀態?

  • 被volatile修飾變量在多線程下如何獲取最新值?
    • 若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。
    • 所以,通過分析咱們能夠得出以下結論:
      • 1.Lock前綴的指令會引發處理器緩存寫回內存;
      • 2.一個處理器的緩存回寫到內存會致使其餘處理器的緩存失效;
      • 3.當處理器發現本地緩存失效後,就會從內存中重讀該變量數據,便可以獲取當前最新值。
    • 這樣針對volatile變量經過這樣的機制就使得每一個線程都能得到該變量的最新值。即知足數據的「可見性」。
  • 如何理解volatile的happens-before關係?
  • 先來看兩個核心之一:volatile的happens-before關係。
    • volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。下面咱們結合具體的代碼,咱們利用這條規則推導下:
    private void test3() {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                new VolatileExample().writer();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                new VolatileExample().reader();
            }
        });
        thread1.start();
        thread2.start();
    }
    
    
    public class VolatileExample {
        private int a = 0;
        private volatile boolean flag = false;
        public void writer(){
            a = 1;          //1
            LogUtils.e("測試volatile數據1--"+a);
            flag = true;   //2
            LogUtils.e("測試volatile數據2--"+flag);
        }
        public void reader(){
            LogUtils.e("測試volatile數據3--"+flag);
            if(flag){      //3
                int i = a; //4
                LogUtils.e("測試volatile數據4--"+i);
            }
        }
    }
    複製代碼
    • 打印日誌以下所示
    //第一種狀況
    2019-03-07 17:17:30.294 25764-25882/com.ycbjie.other E/TestFirstActivity: │ 測試volatile數據3--false
    2019-03-07 17:17:30.294 25764-25881/com.ycbjie.other E/TestFirstActivity: │ 測試volatile數據1--1
    2019-03-07 17:17:30.295 25764-25881/com.ycbjie.other E/TestFirstActivity: │ 測試volatile數據2--true
    
    //第二種狀況
    2019-03-07 17:18:01.965 25764-25901/com.ycbjie.other E/TestFirstActivity: │ 測試volatile數據1--1
    2019-03-07 17:18:01.965 25764-25902/com.ycbjie.other E/TestFirstActivity: │ 測試volatile數據3--false
    2019-03-07 17:18:01.966 25764-25901/com.ycbjie.other E/TestFirstActivity: │ 測試volatile數據2--true
    複製代碼
    • 上面的實例代碼對應的happens-before關係以下圖所示:
      • VolatileExample的happens-before關係推導
    • 分析上面代碼執行過程
      • 加鎖線程A先執行writer方法,而後線程B執行reader方法圖中每個箭頭兩個節點就代碼一個happens-before關係,黑色的表明根據程序順序規則推導出來,紅色的是根據volatile變量的寫happens-before 於任意後續對volatile變量的讀,而藍色的就是根據傳遞性規則推導出來的。
      • 這裏的2 happen-before 3,一樣根據happens-before規則定義:若是A happens-before B,則A的執行結果對B可見,而且A的執行順序先於B的執行順序,咱們能夠知道操做2執行結果對操做3來講是可見的,也就是說當線程A將volatile變量 flag更改成true後線程B就可以迅速感知。
  • 多線程下執行volatile讀寫後的內存狀態?
    • 仍是按照兩個核心的分析方式,分析完happens-before關係後咱們如今就來進一步分析volatile的內存語義。仍是以上面的代碼爲例,假設線程A先執行writer方法,線程B隨後執行reader方法,初始時線程的本地內存中flag和a都是初始狀態,下圖是線程A執行volatile寫後的狀態圖。
      • 線程A執行volatile寫後的內存狀態圖
      • image
    • 當volatile變量寫後,線程中本地內存中共享變量就會置爲失效的狀態,所以線程B再須要讀取從主內存中去讀取該變量的最新值。下圖就展現了線程B讀取同一個volatile變量的內存變化示意圖。
      • 線程B讀volatile後的內存狀態圖
      • image
    • 結果分析
      • 從橫向來看,線程A和線程B之間進行了一次通訊,線程A在寫volatile變量時,實際上就像是給B發送了一個消息告訴線程B你如今的值都是舊的了,而後線程B讀這個volatile變量時就像是接收了線程A剛剛發送的消息。既然是舊的了,那線程B該怎麼辦了?天然而然就只能去主內存去取啦。

5.0.2.2 Volatile實現原理?一個int變量,用volatile修飾,多線程去操做++,線程安全嗎?那如何才能保證i++線程安全?

  • volatile的做用和原理
    • Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節碼,最終須要轉化爲彙編指令在CPU上執行。
    • volatile是輕量級的synchronized(volatile不會引發線程上下文的切換和調度),它在多處理器開發中保證了共享變量的「可見性」。可見性的意思是當一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。
    • 因爲內存訪問速度遠不及CPU處理速度,爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存後在進行操做,但操做完不知道什麼時候會寫到內存。普通共享變量被修改以後,何時被寫入主存是不肯定的,當其餘線程去讀取時,此時內存中可能仍是原來的舊值,所以沒法保證可見性。若是對聲明瞭volatile的變量進行寫操做,JVM就會想處理器發送一條Lock前綴的指令,表示將當前處理器緩存行的數據寫回到系統內存。
  • 一個int變量,用volatile修飾,多線程去操做++,線程安全嗎
    • 技術博客大總結
    • 不安全
    • 案例代碼,至於打印結果就不展現呢
    • volatile只能保證可見性,並不能保證原子性。
    • i++實際上會被分紅多步完成:
      • 1)獲取i的值;
      • 2)執行i+1;
      • 3)將結果賦值給i。
    • volatile只能保證這3步不被重排序,多線程狀況下,可能兩個線程同時獲取i,執行i+1,而後都賦值結果2,實際上應該進行兩次+1操做。
    private volatile int a = 0;
    for (int x=0 ; x<=100 ; x++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                a++;
                Log.e("小楊逗比Thread-------------",""+a);
            }
        }).start();
    }
    複製代碼
  • 如何才能保證i++線程安全
    • 可使用java.util.concurrent.atomic包下的原子類,如AtomicInteger。其實現原理是採用CAS自旋操做更新值。
    for (int x=0 ; x<=100 ; x++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                AtomicInteger atomicInteger = new AtomicInteger(a++);
                int i = atomicInteger.get();
                Log.e("小楊逗比Thread-------------",""+i);
            }
        }).start();
    }
    複製代碼

5.0.2.3 在Java內存模型有哪些能夠保證併發過程的原子性、可見性和有序性的措施?

  • 原子性(Atomicity):一個操做要麼都執行要麼都不執行。
    • 可直接保證的原子性變量操做有:read、load、assign、use、store和write,所以可認爲基本數據類型的訪問讀寫是具有原子性的。
    • 若須要保證更大範圍的原子性,可經過更高層次的字節碼指令monitorenter和monitorexit來隱式地使用lock和unlock這兩個操做,反映到Java代碼中就是同步代碼塊synchronized關鍵字。
  • 可見性(Visibility):當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。
    • 經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現。
    • 提供三個關鍵字保證可見性:volatile能保證新值能當即同步到主內存,且每次使用前當即從主內存刷新;synchronized對一個變量執行unlock操做以前能夠先把此變量同步回主內存中;被final修飾的字段在構造器中一旦初始化完成且構造器沒有把this的引用傳遞出去,就能夠在其餘線程中就能看見final字段的值。
  • 有序性(Ordering):程序代碼按照指令順序執行。
    • 若是在本線程內觀察,全部的操做都是有序的,指「線程內表現爲串行的語義」;若是在一個線程中觀察另外一個線程,全部的操做都是無序的,指「指令重排序」現象和「工做內存與主內存同步延遲」現象。
    • 提供兩個關鍵字保證有序性:volatile 自己就包含了禁止指令重排序的語義;synchronized保證一個變量在同一個時刻只容許一條線程對其進行lock操做,使得持有同一個鎖的兩個同步塊只能串行地進入。

其餘介紹

01.關於博客彙總連接

02.關於個人博客

相關文章
相關標籤/搜索