01 線程基礎(chǔ)、線程之間的共享和協(xié)作

1 基礎(chǔ)概念

1.1 CPU核心數(shù)和線程數(shù)的關(guān)系

1.1.1 CPU與線程數(shù)量

cpu個數(shù):是指物理上,也及硬件上的核心數(shù);
核數(shù):是邏輯上的,簡單理解為邏輯上模擬出的核心數(shù);
邏輯核心數(shù):線程數(shù)=1:1 ;
使用了超線程技術(shù)后---> 1:2

1.1.2 CPU和Java線程的關(guān)系

(1) 單個cpu線程在同一時刻只能執(zhí)行單一Java程序,也就是一個線程
(2) 單個線程同時只能在單個cpu線程中執(zhí)行
(3) 線程是操作系統(tǒng)最小的調(diào)度單位,進程是資源(比如:內(nèi)存)分配的最小單位
(4)Java中的所有線程在JVM進程中,CPU調(diào)度的是進程中的線程
(5)Java多線程并不是由于cpu線程數(shù)為多個才稱為多線程,當Java線程數(shù)大于cpu線程數(shù),操作系統(tǒng)使用時間片機制,采用線程調(diào)度算法,頻繁的進行線程切換。

1.2 CPU時間片輪轉(zhuǎn)機制

1.2.1 時間片輪轉(zhuǎn)調(diào)度

是一種最古老,最簡單,最公平且使用最廣的算法。每個進程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間。

1.2.2 進程切換(process switch)\上下文切換(context switch)

上下文切換也需要耗費一定的時間,當線程數(shù)多于CPU核數(shù)時,CPU就會使用上下文切換操作來保證給每個線程分配時間片段,頻繁的上下文切換會導(dǎo)致系統(tǒng)性能的降低,所以線程數(shù)需要控制在合理的范圍內(nèi)。

結(jié)論可以歸結(jié)如下:時間片設(shè)得太短會導(dǎo)致過多的進程切換,降低了CPU效率;而設(shè)得太長又可能引起對短的交互請求的響應(yīng)變差。將時間片設(shè)為100毫秒通常是一個比較合理的折中。

1.3 什么是進程和線程

進程:程序運行資源分配的最小單位,進程內(nèi)部有多個線程,會共享這個進程的資源
線程:CPU調(diào)度的最小單位,必須依賴進程而存在。

1.4 澄清并行和并發(fā)

并發(fā):并發(fā)的關(guān)鍵是你有處理多個任務(wù)的能力,不一定要同時。同一時刻,可以同時處理事情的能力 。
并行:并行的關(guān)鍵是你有同時處理多個任務(wù)的能力。 與單位時間相關(guān),在單位時間內(nèi)可以處理事情的能力。

1.5 高并發(fā)編程的意義、好處和注意事項

好處:充分利用cpu的資源、加快用戶響應(yīng)的時間,程序模塊化,異步化
問題:
線程共享資源,存在沖突;
容易導(dǎo)致死鎖;
啟用太多的線程,就有搞垮機器的可能

2 Java中線程的創(chuàng)建方式

2.1 實現(xiàn)Runnable

通過實現(xiàn)Runnable接口,重寫run()方法。然后借助Thread的start()方法開啟線程,調(diào)用run()方法是不會開啟新線程的,只是一次方法調(diào)用而已。

public class CreateThreads implements Runnable{
    @Override
    public void run() {
        System.out.println("實現(xiàn)Runnable接口創(chuàng)建線程");
    }
 
    public static void main(String[] args) {
        CreateThreads createThreads = new CreateThreads();
        Thread thread = new Thread(createThreads);
        thread.start();
    }
}

2.2 繼承Thread

繼承Thread類,重寫run()方法。

public class CreateThreads extends Thread{
 
    @Override
    public void run() {
        System.out.println("繼承Thread創(chuàng)建線程");
    }
 
    public static void main(String[] args) {
        CreateThreads createThreads = new CreateThreads();
        Thread thread = new Thread(createThreads);
        thread.start();
    }
}

注意:
①繼承Thread類,重寫run()方法,其本質(zhì)上與實現(xiàn)Runnable接口的方式一致,因為Thread類本身就實現(xiàn)了Runnable接口
②public class Thread implements Runnable 。再加上java中多實現(xiàn),單繼承的特點,在選用上述兩種方式創(chuàng)建線程時,應(yīng)該首先考慮第一種(通過實現(xiàn)Runnable接口的方式)。

2.3 實現(xiàn)Callable接口

通過Runnable與Thread的方式創(chuàng)建的線程,是沒有返回值的。然而在有些情況下,往往需要其它線程計算得到的結(jié)果供給另外線程使用( 例如:計算1+100的值,開啟三個線程,一個主線程,兩個計算線程,主線程需要獲取兩個計算線程的結(jié)算結(jié)果(一個計算線程計算1+2+...+50,另外一個線程計算51+52+..+100),進行相加,從而得到累加結(jié)果),這個時候可以采用Runnable與Thread的方式創(chuàng)建的線程,并通過自行編寫代碼實現(xiàn)結(jié)果返回,但是不可避免的會出現(xiàn)黑多錯誤和性能上的問題。基于此,JUC(java.util.concurrent)提供了解決方案,實現(xiàn)Callable的call()方法(這個類似Runnable接口),使用Future的get()方法進行獲取。

下面開始使用Callable、與Future創(chuàng)建多線程。創(chuàng)建過程為:1、自定義一個類實現(xiàn)Callable接口,重寫call()方法;2、使用JUC包下的ExecutorService,生成一個對象,主要使用submit()方法,返回得到Future對象(關(guān)于JUC包下的諸如ExecutorService解析使用,請關(guān)注博主的后序文章);3、采用Future的get()獲取返回值。

/**
 * 
 * @author dongyue
    *    演示實現(xiàn)Callable接口,獲取多線程的返回值
 */
public class CreateThreadByFutureAndCallable implements Callable<Integer>{

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //生成具有兩個線程的線程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
        //調(diào)用executorService.submit()方法獲取Future
        Future<Integer> result1 = fixedThreadPool.submit(new CreateThreadByFutureAndCallable());
        Future<Integer> result2 = fixedThreadPool.submit(new SubThread());
        //使用Future的get()方法等待子線程計算完成返回的結(jié)果
        //get方法會在運算結(jié)束后將結(jié)果返回,否則該方法就是阻塞的
        int result = result1.get() + result2.get();
        //關(guān)閉線程池
        fixedThreadPool.shutdown();
        //打印結(jié)果
        System.out.println(result);
    }

    @Override
    public Integer call() throws Exception {
        int count = 0;
        for(int i=0;i<50;i++) {
            count += i;
        }
        Thread.sleep(5000);
        return count;
    }

}
class SubThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int count = 0;
        for(int i=51;i<100;i++) {
            count += i;
        }
        return count;
    }
    
}

3 線程的停止方式

3.1 線程自然終止:自然執(zhí)行完或拋出未處理異常

3.2 stop(),resume(),suspend()已不建議使用,stop()會導(dǎo)致線程不會正確釋放資源,suspend()容易導(dǎo)致死鎖。

3.3 java線程是協(xié)作式,而非搶占式

調(diào)用一個線程的interrupt() 方法中斷一個線程,并不是強行關(guān)閉這個線程,只是跟這個線程打個招呼,將線程的中斷標志位置為true,線程是否中斷,由線程本身邏輯決定,開發(fā)者可以通過isInterrupted() 判定當前線程是否處于中斷狀態(tài)。

static方法interrupted() 判定當前線程是否處于中斷狀態(tài),同時中斷標志位改為false。

方法里如果拋出InterruptedException,線程的中斷標志位會被復(fù)位成false,如果確實是需要中斷線程,要求我們自己在catch語句塊里再次調(diào)用interrupt()。

4 線程的流程狀態(tài)

線程的五大狀態(tài)分別為:創(chuàng)建狀態(tài)(New)、就緒狀態(tài)(Runnable)、運行狀態(tài)(Running)、阻塞狀態(tài)(Blocked)、死亡狀態(tài)(Dead)。


image.png

(1)新建狀態(tài):即單純地創(chuàng)建一個線程,創(chuàng)建線程有三種方式
(2)就緒狀態(tài):在創(chuàng)建了線程之后,調(diào)用Thread類的start()方法來啟動一個線程,即表示線程進入就緒狀態(tài)!
(3)運行狀態(tài):當線程獲得CPU時間,線程才從就緒狀態(tài)進入到運行狀態(tài)!
(4)阻塞狀態(tài):線程進入運行狀態(tài)后,可能由于多種原因讓線程進入阻塞狀態(tài),如:調(diào)用sleep()方法讓線程睡眠,調(diào)用wait()方法讓線程等待,調(diào)用join()方法、suspend()方法(它現(xiàn)已被棄用?。┮约白枞絀O方法。
(5)死亡狀態(tài):run()方法的正常退出就讓線程進入到死亡狀態(tài),還有當一個異常未被捕獲而終止了run()方法的執(zhí)行也將進入到死亡狀態(tài)!

注意:線程的優(yōu)先級
取值為1~10,缺省為5,但線程的優(yōu)先級不可靠,不建議作為線程開發(fā)時候的手段

5 線程的常用方法

5.1 方法

start():start方法是Thread 類的方法,在這個方法中會調(diào)用native方法(start0())來啟動線程,為該線程分配資源。
sleep():使當前線程進入阻塞,可設(shè)置阻塞時間,sleep方法在進入阻塞隊列時不會釋放當前線程所持有的鎖。
yield():該方法會讓當前線程交出cpu權(quán)限,但是不能確定具體時間,和sleep方法一樣不會釋放當前線程所持有的鎖,該方法會讓線程直接進入就緒狀態(tài),很好理解。目的是為了讓同等優(yōu)先級的線程獲得cpu執(zhí)行的機會。
join():線程A,執(zhí)行了線程B的join方法,線程A必須要等待B執(zhí)行完成了以后,線程A才能繼續(xù)自己的工作
wait() ,notifyAll(),notify():
  這3個方法一般一同出現(xiàn)。且都是Object類的方法,用來輔助操作線程類。
  這3個方法都必須在同步代碼塊中,不然就會報錯。
  wait()方法會讓當前線程釋放鎖,并進入到條件等待隊列。
  notify()方法會隨機喚醒條件等待隊列的任意一個線程,并將其放到鎖池里面。
  notifyAll()方法則是喚醒條件等待隊列的所有線程,并將其都放到鎖池里面。
  而鎖池里面的線程來競爭所需要的對象鎖,成功獲取到鎖的線程將加入到就緒隊列里面。
注意:一般建議用notifyAll(),因為notify()方法會隨機喚醒條件等待隊列的任意一個線程,并不能保證喚醒開發(fā)者想要的線程。
interrupt(),isInterrupted(),interrupted():
  這3個方法表示線程的中斷,這個很有意思,分多鐘情況討論。(在java中,中斷不是強制停止改線程,而是給線程一個信號,讓其自行處理何時退出)
  isInterrupted:就是返回對應(yīng)線程的中斷標志位是否為true。
  interrupted:返回當前線程的中斷標志位是否為true,但它還有一個重要的副作用,就是清空中斷標志位,也就是說,連續(xù)兩次調(diào)用
  interrupted(),第一次返回的結(jié)果為true,第二次一般就是false 。
  interrupt:表示中斷對應(yīng)的線程。

中斷線程分情況討論:
  1.還沒start,或者已經(jīng)結(jié)束,無效果
  2.運行中,中斷無效,直到只能設(shè)置個中斷位,直到線程走完或者進入阻塞。
  3.鎖池,中斷無效。
  4.阻塞,等待,會拋出異常,可以中斷。
  ps(io等待大多都是可以中斷的,但是inputStream的read不會相應(yīng)中斷。)
  中斷不好使,因此最好自己在線程類里面提供關(guān)閉方法。

5.2 調(diào)用yield() 、sleep()、wait()、notify()等方法對鎖有何影響?

線程在執(zhí)行yield()以后,持有的鎖是不釋放的
sleep()方法被調(diào)用以后,持有的鎖是不釋放的
調(diào)動方法之前,必須要持有鎖。調(diào)用了wait()方法以后,鎖就會被釋放,當wait方法返回的時候,線程會重新持有鎖
調(diào)動方法之前,必須要持有鎖,調(diào)用notify()方法本身不會釋放鎖的

6 守護線程

在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)

用個比較通俗的比如,任何一個守護線程都是整個JVM中所有非守護線程的保姆:

只要當前JVM實例中尚存在任何一個非守護線程沒有結(jié)束,守護線程就全部工作;只有當最后一個非守護線程結(jié)束時,守護線程隨著JVM一同結(jié)束工作。
Daemon的作用是為其他線程的運行提供便利服務(wù),守護線程最典型的應(yīng)用就是 GC (垃圾回收器),它就是一個很稱職的守護者。

User和Daemon兩者幾乎沒有區(qū)別,唯一的不同之處就在于虛擬機的離開:如果 User Thread已經(jīng)全部退出運行了,只剩下Daemon Thread存在了,虛擬機也就退出了。 因為沒有了被守護者,Daemon也就沒有工作可做了,也就沒有繼續(xù)運行程序的必要了。

值得一提的是,守護線程并非只有虛擬機內(nèi)部提供,用戶在編寫程序時也可以自己設(shè)置守護線程。下面的方法就是用來設(shè)置守護線程的。

Thread daemonTread = new Thread();  
   
  // 設(shè)定 daemonThread 為 守護線程,default false(非守護線程)  
 daemonThread.setDaemon(true);  
   
 // 驗證當前線程是否為守護線程,返回 true 則為守護線程  
 daemonThread.isDaemon();  

這里有幾點需要注意:

(1) thread.setDaemon(true)必須在thread.start()之前設(shè)置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規(guī)線程設(shè)置為守護線程。
(2) 在Daemon線程中產(chǎn)生的新線程也是Daemon的。
(3) 不要認為所有的應(yīng)用都可以分配給Daemon來進行服務(wù),比如讀寫操作或者計算邏輯。

因為你不可能知道在所有的User完成之前,Daemon是否已經(jīng)完成了預(yù)期的服務(wù)任務(wù)。一旦User退出了,可能大量數(shù)據(jù)還沒有來得及讀入或?qū)懗?,計算任?wù)也可能多次運行結(jié)果不一樣。這對程序是毀滅性的。造成這個結(jié)果理由已經(jīng)說過了:一旦所有User Thread離開了,虛擬機也就退出運行了。
注意:守護線程和主線程共死,finally也不能保證一定執(zhí)行

7 synchronized關(guān)鍵字

在java代碼中使用synchronized可是使用在代碼塊和方法中,根據(jù)Synchronized用的位置可以有這些使用場景:


image.png

如圖,synchronized可以用在方法上也可以使用在代碼塊中,其中方法是實例方法和靜態(tài)方法分別鎖的是該類的實例對象和該類的對象。而使用在代碼塊中也可以分為三種,具體的可以看上面的表格。這里的需要注意的是:如果鎖的是類對象的話,盡管new多個實例對象,但他們?nèi)匀皇菍儆谕粋€類依然會被鎖住,即線程之間保證同步關(guān)系。

8 volatile關(guān)鍵字,最輕量的同步機制

8.1 概述

volatile作為java中的關(guān)鍵詞之一,用以聲明變量的值可能隨時會別的線程修改,使用volatile修飾的變量會強制將修改的值立即寫入主存,主存中值的更新會使緩存中的值失效(非volatile變量不具備這樣的特性,非volatile變量的值會被緩存,線程A更新了這個值,線程B讀取這個變量的值時可能讀到的并不是是線程A更新后的值)。volatile會禁止指令重排。

8.2volatile特性

volatile具有可見性、有序性,不具備原子性。

注意,volatile不具備原子性,這是volatile與java中的synchronized、java.util.concurrent.locks.Lock最大的功能差異,這一點在面試中也是非常容易問到的點。

下面來分別看下可見性、有序性、原子性:

原子性:如果你了解事務(wù),那這個概念應(yīng)該好理解。原子性通常指多個操作不存在只執(zhí)行一部分的情況,如果全部執(zhí)行完成那沒毛病,如果只執(zhí)行了一部分,那對不起,你得撤銷(即事務(wù)中的回滾)已經(jīng)執(zhí)行的部分。
可見性:當多個線程訪問同一個變量x時,線程1修改了變量x的值,線程1、線程2...線程n能夠立即讀取到線程1修改后的值。
有序性:即程序執(zhí)行時按照代碼書寫的先后順序執(zhí)行。在Java內(nèi)存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。(本文不對指令重排作介紹,但不代表它不重要,它是理解JAVA并發(fā)原理時非常重要的一個概念)。

8.3 volatile適用場景

適用于對變量的寫操作不依賴于當前值,對變量的讀取操作不依賴于非volatile變量。
適用于讀多寫少的場景。
可用作狀態(tài)標志。
JDK中volatie應(yīng)用:JDK中ConcurrentHashMap的Entry的value和next被聲明為volatile,AtomicLong中的value被聲明為volatile。AtomicLong通過CAS原理(也可以理解為樂觀鎖)保證了原子性。

8.4 volatile VS synchronized

volatile本質(zhì)是在告訴jvm當前變量在寄存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住.

volatile僅能使用在變量級別,synchronized則可以使用在變量,方法.

volatile僅能實現(xiàn)變量的修改可見性,但不具備原子特性,而synchronized則可以保證變量的修改可見性和原子性.

volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞.

volatile標記的變量不會被編譯器優(yōu)化,而synchronized標記的變量可以被編譯器優(yōu)化.

9 ThreadLocal的使用

9.1 ThreadLocal是什么

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優(yōu)美的多線程程序。

當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應(yīng)的副本。

從線程的角度看,目標變量就象是線程的本地變量,這也是類名中“Local”所要表達的意思。

所以,在Java中編寫線程局部變量的代碼相對來說要笨拙一些,因此造成線程局部變量沒有在Java開發(fā)者中得到很好的普及。

ThreadLocal的接口方法

ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:

void set(Object value)設(shè)置當前線程的線程局部變量的值。
public Object get()該方法返回當前線程所對應(yīng)的線程局部變量。
public void remove()將當前線程局部變量的值刪除,目的是為了減少內(nèi)存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結(jié)束后,對應(yīng)該線程的局部變量將自動被垃圾回收,所以顯式調(diào)用該方法清除線程的局部變量并不是必須的操作,但它可以加快內(nèi)存回收的速度。
protected Object initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設(shè)計的。這個方法是一個延遲調(diào)用方法,在線程第1次調(diào)用get()或set(Object)時才執(zhí)行,并且僅執(zhí)行1次。ThreadLocal中的缺省實現(xiàn)直接返回一個null。
  值得一提的是,在JDK5.0中,ThreadLocal已經(jīng)支持泛型,該類的類名已經(jīng)變?yōu)門hreadLocal<T>。API方法也相應(yīng)進行了調(diào)整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。

ThreadLocal是如何做到為每一個線程維護變量的副本的呢?其實實現(xiàn)的思路很簡單:在ThreadLocal類中有一個Map,用于存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應(yīng)線程的變量副本。

9.2 Thread同步機制的比較

ThreadLocal和線程同步機制相比有什么優(yōu)勢呢?ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。

在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什么時候?qū)ψ兞窟M行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設(shè)計和編寫難度相對較大。

而ThreadLocal則從另一個角度來解決多線程的并發(fā)訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數(shù)據(jù)的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。

由于ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉(zhuǎn)換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。

概括起來說,對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

Spring使用ThreadLocal解決線程安全問題我們知道在一般情況下,只有無狀態(tài)的Bean才可以在多線程環(huán)境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態(tài)采用ThreadLocal進行處理,讓它們也成為線程安全的狀態(tài),因為有狀態(tài)的Bean就可以在多線程中共享了。

一般的Web應(yīng)用劃分為展現(xiàn)層、服務(wù)層和持久層三個層次,在不同的層中編寫對應(yīng)的邏輯,下層通過接口向上層開放功能調(diào)用。在一般情況下,從接收請求到返回響應(yīng)所經(jīng)過的所有程序調(diào)用都同屬于一個線程


image.png

10 什么是線程間的協(xié)作(待補充)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 九種基本數(shù)據(jù)類型的大小,以及他們的封裝類。(1)九種基本數(shù)據(jù)類型和封裝類 (2)自動裝箱和自動拆箱 什么是自動裝箱...
    關(guān)瑋琳linSir閱讀 2,054評論 0 47
  • 線程池ThreadPoolExecutor corepoolsize:核心池的大小,默認情況下,在創(chuàng)建了線程池之后...
    irckwk1閱讀 863評論 0 0
  • Java-Review-Note——4.多線程 標簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,761評論 2 17
  • Java SE 基礎(chǔ): 封裝、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務(wù))結(jié)合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,234評論 0 8
  • layout: posttitle: 《Java并發(fā)編程的藝術(shù)》筆記categories: Javaexcerpt...
    xiaogmail閱讀 6,015評論 1 19

友情鏈接更多精彩內(nèi)容