筆記---java多線程

這篇文章的問題和答案是我在準備面試複習時查詢資料和代碼測試自己整理的,分享給大家,如有不正確的地方,歡迎大家批評指正。

 1、線程池的原理,爲什麼要創建線程池?創建線程池的方式?
        原理及爲什麼:
            假設一個服務器完成一項任務所需時間爲:T1創建線程時間+T2在線程中執行任務的時間+T3銷燬線程時間。線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高服務器程序性能的。
        當我們需要頻繁調用系統A的服務時,就會頻繁的創建線程和關閉線程。創建線程和回收線程都會佔用系統資源,大量創建回收線程都會增加系統負擔、降低系統性能。因此,爲了提高系統性能,
        我們提前創建一些線程,這些線程由線程池管理,使用的時候就從線程池裏選一個,使用完畢就歸還給線程池。當然創建線程池肯定是會佔用一點內存空間。
        創建方式:
            Java通過Executors(jdk1.5併發包)提供四種線程池,分別爲:
            1.ExecutorService fixedExecutor = Executors.newFixedThreadPool(3);
            定長線程池,每當提交一個任務就創建一個線程,直到達到線程池的最大數量,可控制線程最大併發數,超出的線程會在隊列中等待。
            2.ExecutorService cachedExecutor = Executors.newCachedThreadPool();
            可緩存的線程池,如果線程池的容量超過了任務數,自動回收空閒線程,任務增加時可以自動添加新線程,線程池的容量不限制。
            3.ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(3);
            定長線程池,可執行週期性的任務。
            4.ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
            單線程的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
            5.ScheduledExecutorService singleScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
            單線程可執行週期性任務的線程池。
 
2、線程的生命週期,什麼時候會出現僵死進程?
        生命週期:
            1.New(初始化狀態)
            new Thread 在Java層面的線程被創建了,而在操作系統中的線程其實是還沒被創建的,所以這個時候是不可能分配CPU執行這個線程的!
            2.Runnable(可運行/運行狀態)
            調用start()方法後分配CPU執行,線程就處於這個狀態。
            3.Blocked(阻塞狀態)
            這個狀態下是不能分配CPU執行的,只有一種情況會導致線程阻塞,就是synchronized!被synchronized修飾的方法或者代碼塊同一時刻只能有一個線程執行,而其他競爭鎖的線程就從Runnable到了Blocked狀態!
            當某個線程競爭到鎖了它就變成了Runnable狀態。注意併發包中的Lock,是會讓線程屬於等待狀態而不是阻塞,只有synchronized是阻塞。
            4.Waiting(無時間限制的等待狀態)
            這個狀態下也是不能分配CPU執行的。有三種情況會使得Runnable狀態到waiting狀態:
                調用無參的Object.wait()方法。等到notifyAll()或者notify()喚醒就會回到Runnable狀態。
                調用無參的Thread.join()方法。也就是比如你在主線程裏面建立了一個線程A,調用A.join(),那麼你的主線程是得等A執行完了纔會繼續執行,這時你的主線程就是等待狀態。
                調用LockSupport.park()方法。LockSupport是Java6引入的一個工具類Java併發包中的鎖都是基於它實現的,再調用LocakSupport.unpark(Thread thread),就會回到Runnable狀態。
            5.Timed_Waiting(有時間限制的等待狀態)
            這個狀態和Waiting就是有沒有超時時間的差別,這個狀態下也是不能分配CPU執行的。有五種情況會使得Runnable狀態到waiting狀態:
                Object.wait(long timeout)。
                Thread.join(long millis)。
                Thread.sleep(long millis)。注意 Thread.sleep(long millis, int nanos) 內部調用的其實也是Thread.sleep(long millis)。
                LockSupport.parkNanos(Object blocked,long deadline)。
                LockSupport.parkUntil(long deadline)。
            6.Terminated(終止狀態)
            在我們的線程正常run結束之後或者run一半異常了就是終止狀態!
            注意有個方法Thread.stop()是讓線程終止的,但是這個方法已經被廢棄了,不推薦使用,因爲比如你這個線程得到了鎖,你stop了之後這個鎖也隨着沒了,其它線程就都拿不到這個鎖了!所以推薦使用interrupt()方法。
            interrupt()會使得線程Waiting和Timed_Waiting狀態的線程拋出 interruptedException異常,使得Runnabled狀態的線程如果是在I/O操作會拋出其它異常。
            如果Runnabled狀態的線程沒有阻塞在I/O狀態的話,那隻能主動檢測自己是不是被中斷了,使用isInterrupted()。
        僵死進程:
            一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲僵死進程。
        怎麼處理僵死進程:
            1、通過信號機制
            子進程退出時向父進程發送SIGCHILD信號,父進程處理SIGCHILD信號。調用wait()或者waitpid(),讓父進程阻塞等待殭屍進程的出現,處理完在繼續運行父進程。
            2、殺死父進程
            當父進程陷入死循環等無法處理殭屍進程時,強制殺死父進程,那麼它的子進程,即殭屍進程會變成孤兒進程,由系統來回收。
            3、重啓系統
            當系統重啓時,所有進程在系統關閉時被停止,包括殭屍進程,開啓時init進程會重新加載其他進程。
 
    3、 說說線程安全問題,什麼是線程安全,如何實現線程安全?
 
     什麼是線程安全:    
        線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程纔可使用。不會出現數據不一致或者數據污染。
線程不安全就是不提供數據訪問保護,有可能出現多個線程先後更改數據造成所得到的數據是髒數據。
       如何實現線程安全:
            1.無狀態的確定性函數。給定一個特定的輸入,它總是產生相同的輸出。 該方法既不依賴於外部狀態,也不維護狀態。
            2.當一個類實例的內部狀態在構造之後不能被修改時,它就是不可變的。並且他也是線程安全的。
            3.線程本地變量。創建線程安全的類,在此類中創建變量,那麼這些變量就不會再線程之間共享,因此也是線程安全的。
            4.同步集合-- Collections.synchronizedCollection(new ArrayList<>());
同步集合的方法都用synchronized修飾(內部鎖), 意味着方法在某一時刻只能被一個線程訪問,而其他線程將被阻塞,直到該方法被第一個線程解鎖。 但是同步的性能在併發量高的情況下會受到影響。
            5.併發集合-- new ConcurrentHashMap<>(); 與同步集合不同,併發集合通過將數據劃分爲段來實現線程安全性。 例如在 ConcurrentHashMap中,多個線程可以獲取不同段上的鎖,因此多個線程可以同時訪問。也因此 併發集合比同步集合具有更高的性能, 同步和併發集合只使集合本身成爲線程安全的,而不是內容
            6.原子對象--Java 提供的一組原子類(包括 AtomicInteger、 AtomicLong、 AtomicBoolean 和 AtomicReference)實現線程安全。原子類允許我們在不使用同步的情況下執行線程安全的原子操作。 原子操作在單個機器級別的操作中執行。
            7.用synchronized關鍵字修飾方法或代碼塊&語句 -- 同步鎖
            8.用volatile關鍵字修飾變量 -- 可見性,直接對主存讀寫
            9.Lock鎖機制
                9.1.Reentrantlock 構造函數接受一個可選的布爾參數。 當參數設置爲 true 時,並且多個線程試圖獲取鎖時,JVM 將優先考慮等待時間最長的線程,並授予對鎖的訪問權。
                9.2.讀 / 寫鎖
        private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();                private final Lock readLock = rwLock.readLock(); 
        private final Lock writeLock = rwLock.writeLock();
 
    4、 創建線程池有哪幾個核心參數?如何合理配置線程池的大小?
核心參數:
            1.corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。
            如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啓動所有基本線程。
            2.runnableTaskQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。可以選擇以下幾個阻塞隊列:
                1.ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
                2.LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
                3.SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
                4.PriorityBlockingQueue:一個具有優先級得無限阻塞隊列。
            3.maximumPoolSize(線程池最大大小):線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什麼效果。
            4.ThreadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字,Debug和定位問題時非常又幫助。
            5.RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。
                以下是JDK1.5提供的四種策略:
                    1、ThreadPoolExecutor.AbortPolicy:
                        當線程池中的數量等於最大線程數時拋 java.util.concurrent.RejectedExecutionException 異常,涉及到該異常的任務也不會被執行,線程池默認的拒絕策略就是該策略。
                    2、ThreadPoolExecutor.DiscardPolicy():
                        當線程池中的數量等於最大線程數時,默默丟棄不能執行的新加任務,不報任何異常。
                    3、ThreadPoolExecutor.CallerRunsPolicy():
                        當線程池中的數量等於最大線程數時,重試添加當前的任務;它會自動重複調用execute()方法。
                    4、ThreadPoolExecutor.DiscardOldestPolicy():
                        當線程池中的數量等於最大線程數時,拋棄線程池中工作隊列頭部的任務(即等待時間最久的任務),並執行當前任務。
            6.keepAliveTime(線程活動保持時間):線程池的工作線程空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。
            7.TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
        線程池的工作原則:
            當線程池中線程數量小於 corePoolSize 則創建線程,並處理請求。
            當線程池中線程數量大於等於 corePoolSize 時,則把請求放入 workQueue 中,隨着線程池中的核心線程們不斷執行任務,只要線程池中有空閒的核心線程,線程池就從 workQueue 中取任務並處理。
            當 taskQueue 已存滿,放不下新任務時則新建非核心線程入池,並處理請求直到線程數目達到 maximumPoolSize(最大線程數量設置值)。
            如果線程池中線程數大於 maximumPoolSize 則使用 RejectedExecutionHandler 來進行任務拒絕處理。
        如何合理配置:
            Little's Law(利特爾法則)
            最佳線程數目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數目
            線程等待時間所佔比例越高,需要越多線程。線程CPU時間所佔比例越高,需要越少線程。
            (1)高併發、任務執行時間短的業務,線程池線程數可以設置爲CPU核數+1,減少線程上下文的切換
            (2)併發不高、任務執行時間長的業務要區分開看:
              a)IO密集型的任務,因爲IO操作並不佔用CPU,可以適當加大線程池中的線程數目,讓CPU處理更多的業務
              b)計算密集型任務,線程池中的線程數設置得少一些,減少線程上下文的切換
            (3)併發高、業務執行時間長,解決這種類型任務的關鍵不在於線程池而在於整體架構的設計,看看這些業務裏面某些數據是否能做緩存是第一步,增加服務器是第二步,至於線程池的設置,設置參考(2)。
            最後,業務執行時間長的問題,也可能需要分析一下,看看能不能使用中間件對任務進行拆分和解耦。
 
5、volatile、ThreadLocal的使用場景和原理?
 ThreadLocal:
        ThreadLocal是用於解決多線程共享類的成員變量,原理:在每個線程中都存有一個本地ThreadMap,相當於存了一個對象的副本,key爲threadlocal對象本身,value爲需要存儲的對象值,這樣各個線程之間對於某個成員變量都有自己的副本,不會衝突。
 
    volatile:
        Volatile可以看做是一個輕量級的synchronized,它可以在多線程併發的情況下保證變量的“可見性”,可見性就是在一個線程的工作內存中修改了該變量的值,該變量的值立即能回顯到主內存中,從而保證所有的線程看到這個變量的值是一致的。
        所以在處理同步問題上它大顯作用,而且它的開銷比synchronized小、使用成本更低。
        但是volatile不具有操作的原子性,也就是它不適合在對該變量的寫操作依賴於變量本身自己。舉個最簡單的栗子:在進行計數操作時count++,實際是count=count+1;,count最終的值依賴於它本身的值。
        所以使用volatile修飾的變量在進行這麼一系列的操作的時候,就有併發的問題。
 
     6、ThreadLocal什麼時候會出現OOM的情況?爲什麼?
    
    出現OOM的情況:
        threadLocal設爲null和線程結束這段時間(線程對象不被回收)的情況,容易發生內存泄露
    爲什麼:
        ThreadLocal裏面使用了一個存在弱引用的map,不過弱引用只是針對key,每個key都弱引用指向threadlocal. 當把threadlocal實例置爲null以後,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收. 但是,value卻不能回收,因爲存在一條從current thread連接過來的強引用. 只有當前thread結束以後, currentthread就不會存在棧中,強引用斷開, CurrentThread, Map, value將全部被GC回收.所以,存在着內存泄露.的可能,最好的做法是當threadlocal無用時調用其remove方法。
    補充:什麼是弱引用?什麼是強引用?
PS.Java爲了最小化減少內存泄露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map裏所有key爲null的value。所以最怕的情況就是,threadLocal對象設null了,開始發生“內存泄露”,然後使用線程池,這個線程結束,線程放回線程池中不銷燬,這個線程一直不被使用,或者分配使用了又不再調用get,set方法,那麼這個期間就會發生真正的內存泄露。使用線程池的時候,線程結束是不會銷燬的,會再次使用的。就可能出現內存泄露。
 
   7、 synchronized、volatile區別、synchronized鎖粒度、模擬死鎖場景、原子性與可見性?
    volatile:
    volatile是變量修飾符,修飾的變量具有可見性,一旦某一個線程修改了被volatile修飾的變量,該變量會立即刷新到主內存,普通的變量操作是線程在寄存器或者CPU緩存上進行的,操作完纔會同步到主內存中,但是被volatile修飾的變量是直接讀寫主內存。
    volatile可以禁止指令重排, 程序執行到volatile修飾變量的讀操作或者寫操作時,前面的操作已經完成,且結果對後面的操作可見,後面的操作還沒有進行。
    synchronized
    synchronized可作用於一段代碼或方法,既可以保證可見性,又能夠保證原子性。
    可見性體現在:通過synchronized或者Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存中。
    原子性表現在:要麼不執行,要麼執行到底。
異同:
(1)從而我們可以看出volatile雖然具有可見性但是並不能保證原子性。
 
(2)性能方面,synchronized關鍵字是防止多個線程同時執行一段代碼,就會影響程序執行效率,而volatile關鍵字在某些情況下性能要優於synchronized。
 
但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因爲volatile關鍵字無法保證操作的原子性。

 

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