現(xiàn)代操作系統(tǒng)在運行一個程序時,會為其創(chuàng)建一個進程。例如,啟動一個Java程序,操作系統(tǒng)就會創(chuàng)建一個Java進程。線程是現(xiàn)代操作系統(tǒng)調(diào)度的最小單元,也叫輕量級進程,在一個進程里可以創(chuàng)建多個線程,這些線程都擁有各自的計算器、堆棧和局部變量等屬性,并且能夠訪問共享的內(nèi)存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執(zhí)行。今天主要以兩個方面讓大家更快的了解并發(fā)編程!
一、基本概念與方法
二、線程安全問題與解決
(一)、線程與進程

進程是CPU分配資源的最小單位,由一個或多個線程組成。
線程是CPU進行調(diào)度的最小單位,被稱為輕量級線程。
一個程序至少一個進程,一個進程至少一個線程
(二)、Java中線程的三種創(chuàng)建方式

(1)繼承Thread類,并重寫run()方法
(2)實現(xiàn)Runnable接口,并重寫run()方法
(3)實現(xiàn)Callable接口,并重寫call()方法;此種方法有返回值,且需要使用FutureTask類進行封裝
實現(xiàn)接口與繼承Thread類的比較:
Java中只能單繼承,但是可以實現(xiàn)多個接口;使用接口的方法更適合擴展
繼承整個Thread類的方法開銷過大
若想在線程執(zhí)行體中(即run方法體中)訪問當(dāng)前線程,繼承方式可以直接通過this;而接口方法要通過Thread.currrentThread()
此外實現(xiàn)Runnable接口創(chuàng)建的線程可以處理同一資源,從而實現(xiàn)資源的共享

(三)、線程的狀態(tài)

(1)新建狀態(tài):創(chuàng)建后未啟動
(2)就緒狀態(tài):調(diào)用start()方法后進入該狀態(tài),與其他就緒狀態(tài)線程一起競爭CPU,等待CPU的調(diào)度。
(3)運行狀態(tài):就緒狀態(tài)的線程獲得CPU時間片,真正的執(zhí)行run()方法。線程只能從就緒狀態(tài)進入運行狀態(tài)
(4)阻塞狀態(tài):線程由于如下所示的各種原因進入阻塞,線程掛起
該線程調(diào)用Thread.sleep()方法
等待阻塞,線程中的共享變量調(diào)用了wait()方法
I/O流方式,如read()方法,receive()方法等待數(shù)據(jù)
同步阻塞,線程因無法獲得目標(biāo)資源的鎖而被掛起
(四)、sleep()方法和wait()方法

sleep()是Thread類中的靜態(tài)方法,調(diào)用Thread.sleep(time)后線程休眠time毫秒,休眠過程中線程不會釋放擁有的對象鎖。如果該線程睡眠期間其他線程調(diào)用了該線程的interrupt()方法中斷了該線程,該線程會在調(diào)用sleep()方法的地方拋出InterruptedException。
wait()是Object類中的方法,當(dāng)線程調(diào)用一個共享變量的wait()方法是,該線程會被掛起并且釋放該對象鎖,進入等待此對象的等待鎖定池,直到其他線程調(diào)用了該共享對象的notify()或者notifyAll()方法。其中,notify()是在等待鎖定池中隨機喚醒一個線程,notifyAll()是喚醒所有因該對象的wait()方法而掛起的線程。
注意:調(diào)用共享變量的wait()、notify()、notifyAll()方法,需要先獲得共享變量的對象鎖。被喚醒的線程不會立即執(zhí)行,需要和其他線程一起競爭對象鎖(由調(diào)用notify()方法的線程所釋放的對象鎖)。


(五)、join()方法和yield()方法

join()方法,Thread類的成員方法,插隊方法,線程A的執(zhí)行體中調(diào)用 B.join(),B代表線程B,則線程A會阻塞,讓B線程插隊。參數(shù)可以傳入時間(毫秒),表示允許插隊運行的時間長度。
yield()方法,Thread類的靜態(tài)方法,禮讓方法,線程A調(diào)用Thread.yield()方法后會讓出CPU使用權(quán),進入就緒狀態(tài),與其他處于就緒狀態(tài)的線程一起競爭CPU。(實際上,調(diào)用yield()方法之后,線程調(diào)度器會從線程就緒隊列中獲取一個線程優(yōu)先級最高的線程,而該線程的優(yōu)先級會變?yōu)?)
(六)、線程中斷

線程中斷是線程間的一種協(xié)作模式,通過設(shè)置線程的中斷標(biāo)志并不能直接終止該線程的執(zhí)行,而是被中斷的線程根據(jù)中斷狀態(tài)自行處理。
interrupt()方法,中斷線程,將線程的中斷標(biāo)志設(shè)置為true。當(dāng)線程因調(diào)用wait()、join()、sleep()等方法進入阻塞時,其他線程調(diào)用該線程的interrupt()方法,該線程會拋出InterruptedException并返回。如果調(diào)用線程的interrupt()方法后未拋出InterruptedException,則應(yīng)通過interrupted()方法判斷當(dāng)前線程是否被中斷來返回線程(如在執(zhí)行體中使用該方法作為線程執(zhí)行前提條件)
(七)、守護線程與用戶線程


守護線程是服務(wù)于用戶線程的,可以通過調(diào)用setDaemon(true)方法將用戶線程設(shè)置為守護線程
兩者可以通過JVM是否等待線程結(jié)束來區(qū)分,JVM只會等待用戶線程結(jié)束;守護線程不會影響JVM的退出,不管其是否運行結(jié)束都會隨著JVM的結(jié)束而結(jié)束。即用戶線程全部結(jié)束時,程序終止,并殺死所有守護線程。如main函數(shù)就是一個用戶線程,而垃圾回收線程就是一個守護線程。
(八)、ThreadLocal的使用

ThreadLocal由JDK包提供,它提供了線程本地變量,即每個訪問ThreadLocal變量的線程都會有一個該變量的隨機副本。線程對該變量進行操作時,實際上是對自己的本地內(nèi)存里的變量進行操作,從而避免了多線程共享一個變量時的安全問題。如在封裝MyBatisUtil工具包時,其中就用到了將SqlSession的實例對象存儲在ThreadLocal的實例對象中,每次通過
get獲取,使用完后關(guān)閉SqlSession實例對象,并set(null)將ThreadLocal清空;tl是ThreadLocal的實例對象。
二。線程安全問題與解決
(一)、Java中的線程安全問題

當(dāng)多個線程對共享資源進行訪問時,只有當(dāng)至少有一個線程修改共享資源時才會存在線程安全問題。典型的如計數(shù)器類實現(xiàn)中的丟失修改問題。
(二)、共享變量的內(nèi)存可見性問題

Java中所有的變量存放在主存中,而線程使用變量時會把主內(nèi)存里面的變量復(fù)制到自己的工作內(nèi)存中,線程讀寫變量時操作的是自己工作變量中的內(nèi)存,然后將自己工作內(nèi)存中的變量刷新到主內(nèi)存中。因此,當(dāng)線程A和線程B同時處理一個共享變量時,會存在內(nèi)存不可見的問題。
(三)、鎖的概念
(1)樂觀鎖與悲觀鎖:是從數(shù)據(jù)庫概念中引入的詞。悲觀鎖指認(rèn)為數(shù)據(jù)很容易被其他線程修改,因此會在數(shù)據(jù)被處理前對數(shù)據(jù)進行加鎖,使得整個處理過程中數(shù)據(jù)處于鎖定狀態(tài)。樂觀鎖則是認(rèn)為數(shù)據(jù)在一般情況下不會造成沖突,因此在訪問數(shù)據(jù)前不會加排它鎖,只有在數(shù)據(jù)提交更新時,才會正式的對數(shù)據(jù)沖突與否進行檢測。
(2)獨占鎖與共享鎖:根據(jù)鎖只能被單個線程持有還是能被多個線程持有,分為獨占鎖(排它鎖)和共享鎖。獨占鎖是一種悲觀鎖,每次訪問資源前都先加上互斥鎖,只允許同一時間由一個線程讀取數(shù)據(jù)。而共享鎖是一種樂觀鎖,允許多個線程同時進行讀操作。
(3)公平鎖與非公平鎖:根據(jù)線程獲取鎖的搶占機制,可以分為公平鎖與非公平鎖。公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,即早到早得。而非公平鎖則不一定先到先得。ReentrantLock提供的鎖默認(rèn)是非公平鎖。一般來說,在沒有公平性需求的前提下,盡量使用非公平鎖,因為公平鎖會帶來性能開銷。
(4)可重入鎖:一個線程再次獲取它自己已經(jīng)獲得的鎖時,則稱為可重入鎖。可重入的原理是在鎖內(nèi)部維護一個線程表示,線程表示來指示該鎖目前被哪個線程占有,然后關(guān)聯(lián)一個計數(shù)器來表示該鎖是否被線程占用,0為未被占用,1為已占用,此后每次重入則計數(shù)器+1.
(5)自旋鎖:自旋鎖是指線程在獲取鎖失敗時不會馬上掛起,而是在不放棄CPU使用權(quán)的情況下,多次嘗試獲取該鎖(默認(rèn)10次)。一般而言,當(dāng)線程獲取鎖失敗后,會切換到內(nèi)核狀態(tài)而被掛起;當(dāng)該線程獲取鎖后又需要將其切換到內(nèi)核狀態(tài)而喚醒該線程,而用戶狀態(tài)切換到內(nèi)核狀態(tài)的開銷是比較大的,即自旋鎖是使用CPU時間換取線程阻塞與調(diào)度的開銷。
(四、)synchronized的使用
synchronized是Java提供的一種原子性內(nèi)置鎖。是一種排它鎖,同時也是非公平的。synchronized可以解決共享變量的內(nèi)存可見性問題。
進入synchronized塊的語義是,把塊內(nèi)使用的變量從線程的工作內(nèi)存中清除,這樣線程就會直接從主內(nèi)存中去獲取塊內(nèi)需要使用的變量。
退出synchronized塊的語義是,將synchronized塊內(nèi)對共享變量的修改刷新到主內(nèi)存中。
(五)、volatile的使用
使用鎖的方式解決共享變量內(nèi)存可見性的問題太過繁瑣,開銷太大,因此Java提供了一種弱形式的同步,即volatile關(guān)鍵字。
類成員變量或者類靜態(tài)成員變量被volatile修飾后主要有兩個特性
(1)解決不同線程對該變量進行的操作時的可見性問題。因為線程在操作volatile修飾的變量時,不會把值緩存到寄存器或者其他地方,而是直接把值刷新會主內(nèi);當(dāng)其他線程獲取該變量時,會從主內(nèi)存中重新獲取最新值,而不是使用當(dāng)前線程工作內(nèi)存中的值。
(2)禁止指令重排,一定程度上能保證有序性。具體情況是,寫volatile變量時,寫之前的操作不會被編譯器重排序到volatile寫之后。讀volatile變量時,讀之后的操作不會被編譯器重排序到volatile讀之前。
(六)、Java中的CAS操作
Java中使用鎖來處理并發(fā)會產(chǎn)生線程上下文切換和重新調(diào)度的開銷。而非阻塞的volatile關(guān)鍵字只能保證共享變量的可見性,不能解決讀-改-寫等原子性問題。因此JDK提供了非阻塞原子性操作,即CAS(Compare
and Swap)操作,它通過硬件保證了比較-更新操作的原子性。
CAS操作有個經(jīng)典的ABA問題,大概意思是
線程1獲取變量X的值(A),然后修改變量X的值為B,這種情況下即使使用CAS操作成,程序也不一定運行正確。因為可能存在線程2在1獲取變量X后,使用CAS操作修改了X的值為B,然后又使用CAS操作修改X的值為A,這樣線程1修改變量X的值是,已經(jīng)是此A非彼A了。
ABA問題大概流程:1.CASget(X-A) --->2.CASset(X-B)--->2.CASset(X-A)--->1.CASset(X-B)。
ABA問題的產(chǎn)生是因為變量的狀態(tài)值產(chǎn)生了環(huán)形轉(zhuǎn)換,即變量值從A到B,然后再從B到A。如果規(guī)定變量的值只能朝著一個方向轉(zhuǎn)換,則不會出現(xiàn)該問題。因此JDK中的AtomicStampedReference類給每個變量的狀態(tài)值都配置了一個時間戳死,以避免ABA問題發(fā)生。