一。基本概念與方法java
1.線程與進程數據庫
進程是CPU分配資源的最小單位,由一個或多個線程組成。緩存
線程是CPU進行調度的最小單位,被稱爲輕量級線程。安全
一個程序至少一個進程,一個進程至少一個線程多線程
2.Java中線程的三種建立方式併發
(1)繼承Thread類,並重寫run()方法ide
(2)實現Runnable接口,並重寫run()方法函數
(3)實現Callable接口,並重寫call()方法;此種方法有返回值,且須要使用FutureTask類進行封裝工具
實現接口與繼承Thread類的比較:性能
1 import java.util.concurrent.Callable; 2 import java.util.concurrent.ExecutionException; 3 import java.util.concurrent.FutureTask; 4 5 public class TestThread { 6 7 public static void main(String[] args) throws InterruptedException { 8 9 ThreadA threadA = new ThreadA(); 10 11 ThreadB threadB = new ThreadB(); 12 13 FutureTask<String> futureTask = new FutureTask<>(new ThreadC()); 14 15 //依次啓動三個線程 16 threadA.start(); 17 Thread.sleep(1000);//主線程休眠1000ms 18 19 //需傳入實現接口的類以建立線程 20 new Thread(threadB).start(); 21 Thread.sleep(1000); 22 23 new Thread(futureTask).start(); 24 try { 25 //獲取第三種方法建立線程中設置的返回值 26 String result = futureTask.get(); 27 System.out.println(result); 28 } catch (ExecutionException e) { 29 e.printStackTrace(); 30 } 31 } 32 33 } 34 35 /** 36 * 繼承Thread類並重寫run()方法 37 */ 38 class ThreadA extends Thread { 39 @Override 40 public void run() { 41 System.out.println("我是線程A"); 42 } 43 } 44 45 /** 46 * 實現Runnable接口並重寫run()方法 47 */ 48 class ThreadB implements Runnable { 49 50 @Override 51 public void run() { 52 System.out.println("我是線程B"); 53 } 54 55 } 56 57 /** 58 *實現Callable接口並重寫call()方法 59 */ 60 class ThreadC implements Callable<String> { 61 62 @Override 63 public String call() throws Exception { 64 return "我是線程C"; 65 } 66 67 }
3.線程的狀態
(1)新建狀態:建立後未啓動
(2)就緒狀態:調用start()方法後進入該狀態,與其餘就緒狀態線程一塊兒競爭CPU,等待CPU的調度。
(3)運行狀態:就緒狀態的線程得到CPU時間片,真正的執行run()方法。線程只能從就緒狀態進入運行狀態
(4)阻塞狀態:線程因爲以下所示的各類緣由進入阻塞,線程掛起
4.sleep()方法和wait()方法
sleep()是Thread類中的靜態方法,調用Thread.sleep(time)後線程休眠time毫秒,休眠過程當中線程不會釋放擁有的對象鎖。若是該線程睡眠期間其餘線程調用了該線程的interrupt()方法中斷了該線程,該線程會在調用sleep()方法的地方拋出InterruptedException。
wait()是Object類中的方法,當線程調用一個共享變量的wait()方法是,該線程會被掛起而且釋放該對象鎖,進入等待此對象的等待鎖定池,直到其餘線程調用了該共享對象的notify()或者notifyAll()方法。其中,notify()是在等待鎖定池中隨機喚醒一個線程,notifyAll()是喚醒全部因該對象的wait()方法而掛起的線程。
注意:調用共享變量的wait()、notify()、notifyAll()方法,須要先得到共享變量的對象鎖。被喚醒的線程不會當即執行,須要和其餘線程一塊兒競爭對象鎖(由調用notify()方法的線程所釋放的對象鎖)。
public class TestNotify { private static volatile Object resourceA = new Object(); public static void main(String[] args) throws InterruptedException { //建立線程A Thread threadA = new Thread(new Runnable() { @Override public void run() { //先獲取共享資源resourceA的對象鎖 synchronized(resourceA){ System.out.println("TA get RA lock"); try { System.out.println("TA begin wait"); //調用共享對象的wait()方法 resourceA.wait(); System.out.println("TA end wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //建立線程B Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized(resourceA) { System.out.println("TB get RA lock"); try { System.out.println("TB begin wait"); resourceA.wait(); System.out.println("TB end wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //建立線程C Thread threadC = new Thread(new Runnable() { @Override public void run() { //只有先得到共享對象的鎖才能調用notify()等方法,不然會拋出IllegalMonitorStateException synchronized(resourceA) { System.out.println("TC begin notify"); resourceA.notify();//隨機喚醒一個因resourceA.wait()掛起的線程 } } }); //啓動線程,線程進入就緒狀態,具體何時運行由CPU調度決定 threadA.start(); threadB.start(); Thread.sleep(1000);//主線程休眠1000ms threadC.start();//啓動線程C //等待線程結束 threadA.join(); threadB.join(); threadC.join(); System.out.println("main over"); } }
5.join()方法和yield()方法
join()方法,Thread類的成員方法,插隊方法,線程A的執行體中調用 B.join(),B表明線程B,則線程A會阻塞,讓B線程插隊。參數能夠傳入時間(毫秒),表示容許插隊運行的時間長度。
yield()方法,Thread類的靜態方法,禮讓方法,線程A調用Thread.yield()方法後會讓出CPU使用權,進入就緒狀態,與其餘處於就緒狀態的線程一塊兒競爭CPU。(實際上,調用yield()方法以後,線程調度器會從線程就緒隊列中獲取一個線程優先級最高的線程,而該線程的優先級會變爲1)
6.線程中斷
線程中斷是線程間的一種協做模式,經過設置線程的中斷標誌並不能直接終止該線程的執行,而是被中斷的線程根據中斷狀態自行處理。
interrupt()方法,中斷線程,將線程的中斷標誌設置爲true。當線程因調用wait()、join()、sleep()等方法進入阻塞時,其餘線程調用該線程的interrupt()方法,該線程會拋出InterruptedException並返回。若是調用線程的interrupt()方法後未拋出InterruptedException,則應經過interrupted()方法判斷當前線程是否被中斷來返回線程(如在執行體中使用該方法做爲線程執行前提條件)
7.守護線程與用戶線程
守護線程是服務於用戶線程的,能夠經過調用setDaemon(true)方法將用戶線程設置爲守護線程
二者能夠經過JVM是否等待線程結束來區分,JVM只會等待用戶線程結束;守護線程不會影響JVM的退出,無論其是否運行結束都會隨着JVM的結束而結束。即用戶線程所有結束時,程序終止,並殺死全部守護線程。如main函數就是一個用戶線程,而垃圾回收線程就是一個守護線程。
8.ThreadLocal的使用
ThreadLocal由JDK包提供,它提供了線程本地變量,即每一個訪問ThreadLocal變量的線程都會有一個該變量的隨機副本。線程對該變量進行操做時,其實是對本身的本地內存裏的變量進行操做,從而避免了多線程共享一個變量時的安全問題。如在封裝MyBatisUtil工具包時,其中就用到了將SqlSession的實例對象存儲在ThreadLocal的實例對象中,每次經過 tl.get()獲取,使用完後關閉SqlSession實例對象,並 tl.set(null)將ThreadLocal清空;tl是ThreadLocal的實例對象。
二。線程安全問題與解決
1.Java中的線程安全問題
當多個線程對共享資源進行訪問時,只有當至少有一個線程修改共享資源時纔會存在線程安全問題。典型的如計數器類實現中的丟失修改問題。
2.共享變量的內存可見性問題
Java中全部的變量存放在主存中,而線程使用變量時會把主內存裏面的變量複製到本身的工做內存中,線程讀寫變量時操做的是本身工做變量中的內存,而後將本身工做內存中的變量刷新到主內存中。所以,當線程A和線程B同時處理一個共享變量時,會存在內存不可見的問題。
3.鎖的概念
(1)樂觀鎖與悲觀鎖:是從數據庫概念中引入的詞。悲觀鎖指認爲數據很容易被其餘線程修改,所以會在數據被處理前對數據進行加鎖,使得整個處理過程當中數據處於鎖定狀態。樂觀鎖則是認爲數據在通常狀況下不會形成衝突,所以在訪問數據前不會加排它鎖,只有在數據提交更新時,纔會正式的對數據衝突與否進行檢測。
(2)獨佔鎖與共享鎖:根據鎖只能被單個線程持有仍是能被多個線程持有,分爲獨佔鎖(排它鎖)和共享鎖。獨佔鎖是一種悲觀鎖,每次訪問資源前都先加上互斥鎖,只容許同一時間由一個線程讀取數據。而共享鎖是一種樂觀鎖,容許多個線程同時進行讀操做。
(3)公平鎖與非公平鎖:根據線程獲取鎖的搶佔機制,能夠分爲公平鎖與非公平鎖。公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間遲早來決定的,即早到早得。而非公平鎖則不必定先到先得。ReentrantLock提供的鎖默認是非公平鎖。通常來講,在沒有公平性需求的前提下,儘可能使用非公平鎖,由於公平鎖會帶來性能開銷。
(4)可重入鎖:一個線程再次獲取它本身已經得到的鎖時,則稱爲可重入鎖。可重入的原理是在鎖內部維護一個線程表示,線程表示來指示該鎖目前被哪一個線程佔有,而後關聯一個計數器來表示該鎖是否被線程佔用,0爲未被佔用,1爲已佔用,此後每次重入則計數器+1.
(5)自旋鎖:自旋鎖是指線程在獲取鎖失敗時不會立刻掛起,而是在不放棄CPU使用權的狀況下,屢次嘗試獲取該鎖(默認10次)。通常而言,當線程獲取鎖失敗後,會切換到內核狀態而被掛起;當該線程獲取鎖後又須要將其切換到內核狀態而喚醒該線程,而用戶狀態切換到內核狀態的開銷是比較大的,即自旋鎖是使用CPU時間換取線程阻塞與調度的開銷。
4.synchronized的使用
synchronized是Java提供的一種原子性內置鎖。是一種排它鎖,同時也是非公平的。synchronized能夠解決共享變量的內存可見性問題。
進入synchronized塊的語義是,把塊內使用的變量從線程的工做內存中清除,這樣線程就會直接從主內存中去獲取塊內須要使用的變量。
退出synchronized塊的語義是,將synchronized塊內對共享變量的修改刷新到主內存中。
5.volatile的使用
使用鎖的方式解決共享變量內存可見性的問題太過繁瑣,開銷太大,所以Java提供了一種弱形式的同步,即volatile關鍵字。
類成員變量或者類靜態成員變量被volatile修飾後主要有兩個特性
(1)解決不一樣線程對該變量進行的操做時的可見性問題。由於線程在操做volatile修飾的變量時,不會把值緩存到寄存器或者其餘地方,而是直接把值刷新會主內;當其餘線程獲取該變量時,會從主內存中從新獲取最新值,而不是使用當前線程工做內存中的值。
(2)禁止指令重排,必定程度上能保證有序性。具體狀況是,寫volatile變量時,寫以前的操做不會被編譯器重排序到volatile寫以後。讀volatile變量時,讀以後的操做不會被編譯器重排序到volatile讀以前。
6.Java中的CAS操做
Java中使用鎖來處理併發會產生線程上下文切換和從新調度的開銷。而非阻塞的volatile關鍵字只能保證共享變量的可見性,不能解決讀-改-寫等原子性問題。所以JDK提供了非阻塞原子性操做,即CAS(Compare and Swap)操做,它經過硬件保證了比較-更新操做的原子性。
CAS操做有個經典的ABA問題,大概意思是 線程1獲取變量X的值(A),而後修改變量X的值爲B,這種狀況下即便使用CAS操做成,程序也不必定運行正確。由於可能存在線程2在1獲取變量X後,使用CAS操做修改了X的值爲B,而後又使用CAS操做修改X的值爲A,這樣線程1修改變量X的值是,已是此A非彼A了。
ABA問題大概流程:1.CASget(X-A) --->2.CASset(X-B)--->2.CASset(X-A)--->1.CASset(X-B)。
ABA問題的產生是由於變量的狀態值產生了環形轉換,即變量值從A到B,而後再從B到A。若是規定變量的值只能朝着一個方向轉換,則不會出現該問題。所以JDK中的AtomicStampedReference類給每一個變量的狀態值都配置了一個時間戳死,以免ABA問題發生。