文檔章節java
既然今天的主題是線程安全,那什麼是線程安全呢?編程
其實線程安全並無一個明確的定義,Doug Lea大師(不認識的去百度,java不認識的去面壁)給下的定義爲多線程下使用這個類,不過多線程如何使用和調度這個類,這個類老是表示出正確的行爲,這個類就是線程安全的。緩存
類的線程安全表現爲:安全
不作正確的同步,在多個線程之間共享狀態的時候,就會出現線程不安全。服務器
原子性在以前的篇章都有講過,就不細說了。主要就是表示一個操做是不可中斷的,或者不可在分割的,要麼所有執行成功要麼所有執行失敗。微信
多個線程對同一個變量(稱爲:共享變量)進行操做,可是這多個線程有可能被分配到多個處理器中運行,那麼編譯器會對代碼進行優化,當線程要處理該變量時,多個處理器會將變量從主存複製一份分別存儲在本身的存儲器中,等到進行完操做後,再賦值回主存。多線程
這樣作的好處是提升了運行的速度,一樣優化帶來的問題之一是變量可見性——若是線程t1與線程t2分別被安排在了不一樣的處理器上面,那麼t1與t2對於變量A的修改時相互不可見,若是t1給A賦值,而後t2又賦新值,那麼t2的操做就將t1的操做覆蓋掉了,這樣會產生不可預料的結果。所以,須要保證變量的可見性(一個線程對共享變量值的修改,可以及時地被其它線程看到)。併發
多線程操做共享變量實現可見性過程JVM的內存模型以下:dom
所以,若是要保證線程安全,那就要保證,咱們全部的線程讀取到的共享變量都是正確的值,也就是保證內存的可見性。ide
若是瞭解JVM的同窗應該都清楚,java中定義的每一個方法,存儲在java的方法棧中,每一個方法是一個棧楨,所謂的棧封閉,就是變量在方法內部進行聲明。那麼這些變量都是處於棧封閉狀態的。(說簡單點就是定義局部變量)
code:
/** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description:棧封閉 */ public class WorkTask { //不安全 private String name; public Integer call() { //方法內的成員變量是安全的, String name ="safe"; this.name = name; int sleepTime = new Random().nextInt(1000); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } return sleepTime; } }
就是沒有實例變量的對象 ,沒有具體字段,不能保存數據,是不變類。例如咱們MVC模式中的DAO層。只定義方法,沒有定義字段變量。
/** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description:沒有任何變量,線程安全 */ public class WorkTask { public Integer call() { String name ="safe"; int sleepTime = new Random().nextInt(1000); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } return sleepTime; } }
除了將類定義成無狀態以外,但大多數狀況下,咱們的類都是有字段的,有狀態的,那就須要經過讓類不可變的方式,讓類變得安全。方式有如下兩種
(1),加final關鍵字,可是加上final,要注意若是成員變量又是一個對象時,這個對象所對應的類也要是不可變,才能保證整個類是不可變的。
code:
package com.xiangxue.ch7.safeclass; /** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description: 不可變的類 */ public class ImmutableFinalRef { //由於a,b都是final變量,不可變,顧屬於安全的。 private final int a; private final int b; //這裏,就不能保證線程安全啦,由於user是一個引用類型,user對象裏的字段有可能進行改變, //除非user對象裏全部字段也都是final類型,不然User實際上是可變的。 private final User user; public ImmutableFinalRef(int a, int b) { super(); this.a = a; this.b = b; this.user = new User(2); } public int getA() { return a; } public int getB() { return b; } public User getUser() { return user; } //將User內部全部字段改成final,則能保證改類爲線程安全。 public static class User{ private int age; public User(int age) { super(); this.age = age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } public static void main(String[] args) { ImmutableFinalRef ref = new ImmutableFinalRef(12,23); User u = ref.getUser(); } }
(2)、根本就不提供任何可供修改爲員變量的地方,同時成員變量也不做爲方法的返回值
code:
/** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description:不可變的類 */ public class ImmutetableToo { //改類也屬於線程安全的,由於沒有提供任何獲取和修改的地方。 private List<Integer> list = new ArrayList<>(3); public ImmutetableToo() { list.add(1); list.add(2); list.add(3); } public boolean isContains(int i) { return list.contains(i); } }
java開發建議:對於一個類,全部的成員變量應該是私有的,若是有可能,全部的成員變量應該加上final關鍵字.能夠保證線程安全。
volatile關鍵字能夠保證保證類的可見性,最適合一個線程寫,多個線程讀的情景,
恩,這個沒啥好解釋的了。。。。
類中持有的成員變量,特別是對象的引用,若是這個成員對象不是線程安全的,經過get等方法發佈出去,會形成這個成員對象自己持有的數據在多線程下不正確的修改,從而形成整個類線程不安全的問題。
/** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description:存在不安全的發佈 */ public class UnsafePublish { //要麼用線程安全的容器替換 //要麼發佈出去的時候,提供副本,深度拷貝 private List<Integer> list = new ArrayList<>(3); public UnsafePublish() { list.add(1); list.add(2); list.add(3); } //將list不安全的發佈出去了,線程不安全。 public List<Integer> getList() { return list; } //也是安全的,加了鎖-------------------------------- public synchronized int getList(int index) { return list.get(index); } public synchronized void set(int index,int val) { list.set(index,val); } }
ThreadLocal的實例表明了一個線程局部的變量,每條線程都只能看到本身的值,並不會意識到其它的線程中也存在該變量。
死鎖是指兩個或兩個以上的進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。
發生死鎖的條件是,競爭資源必定是多於1個,同時小於等於競爭的線程數,當資源只有一個,只會產生激烈的競爭,不會死鎖。
死鎖的根本成因:獲取鎖的順序不一致致使。
code:
/** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description:死鎖的例子 */ public class NormalDeadLock { private static Object lockFirst = new Object();//第一個鎖 private static Object lockSecond = new Object();//第二個鎖 //先拿第一個鎖,再拿第二個鎖 private static void fisrtToSecond() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (lockFirst) { System.out.println(threadName+" get first"); SleepTools.ms(100); synchronized (lockSecond) { System.out.println(threadName+" get second"); } } } //先拿第二個鎖,再拿第一個鎖 private static void SecondToFisrt() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (lockFirst) { System.out.println(threadName+" get first"); SleepTools.ms(100); synchronized (lockSecond) { System.out.println(threadName+" get second"); } } } //執行先拿第二個鎖,再拿第一個鎖 private static class TestThread extends Thread{ private String name; public TestThread(String name) { this.name = name; } public void run(){ Thread.currentThread().setName(name); try { SecondToFisrt(); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread.currentThread().setName("TestDeadLock"); TestThread testThread = new TestThread("SubTestThread"); testThread.start(); try { fisrtToSecond();//先拿第一個鎖,再拿第二個鎖 } catch (InterruptedException e) { e.printStackTrace(); } } }
一、代碼邏輯查看
二、經過經過jps 查詢應用的 id,再經過jstack id 查看應用的鎖的持有狀況,高版本的JDK能夠直接檢測到簡單地死鎖
解決辦法:保證加鎖的順序性
動態順序死鎖,在實現時按照某種順序加鎖了,可是由於外部調用的問題,致使沒法保證加鎖順序而產生的。
code:
/** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description:不安全的轉帳動做的實現 */ public class TrasnferAccount implements ITransfer { //當根據傳參不一致,有可能會致使死鎖。 //一個線程先鎖了from,在鎖了to同時另外一個線程將to當作from傳參, // 將from當作to傳參。那麼第二個線程則先鎖了to,又鎖了from。致使死鎖 @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { synchronized (from){//先鎖轉出 System.out.println(Thread.currentThread().getName() +" get"+from.getName()); Thread.sleep(100); synchronized (to){//再鎖轉入 System.out.println(Thread.currentThread().getName() +" get"+to.getName()); from.flyMoney(amount); to.addMoney(amount); } } } }
解決:
例如咱們生活中的例子,兩我的在窄路相遇,同時向一個方向避讓,而後又向另外一個方向避讓,如此反覆。致使兩我的都過不去。
活鎖便是在嘗試拿鎖的機制中,發生多個線程之間互相謙讓,不斷髮生拿鎖,釋放鎖的過程。
解決辦法:每一個線程休眠隨機數,錯開拿鎖的時間。
低優先級的線程,老是拿不到執行時間。
使用併發的目標是爲了提升性能,引入多線程後,其實會引入額外的開銷,如線程之間的協調、增長的上下文切換,線程的建立和銷燬,線程的調度等等。過分的使用和不恰當的使用,會致使多線程程序甚至比單線程還要低。
衡量應用的程序的性能:服務時間,延遲時間,吞吐量,可伸縮性等等,其中服務時間,延遲時間(多快),吞吐量(處理能力的指標,完成工做的多少)。多快和多少,徹底獨立,甚至是相互矛盾的。
對服務器應用來講:多少(可伸縮性,吞吐量)這個方面比多快更受重視,先保證能夠橫向擴展,在保證垂直擴展。
咱們作應用的時候:
一個應用程序裏,串行的部分是永遠都有的。
Amdahl定律 : 1/(F+(1-N)/N) F:必須被串行部分,程序最好的結果爲 1/F。
是指CPU 從一個進程或線程切換到另外一個進程或線程。一次上下文切換花費5000~10000個時鐘週期,幾微秒。在上下文切換過程當中,CPU會中止處理當前運行的程序,並保存當前程序運行的具體位置以便以後繼續運行。從這個角度來看,上下文切換有點像咱們同時閱讀幾本書,在來回切換書本的同時咱們須要記住每本書當前讀到的頁碼。
上下文切換一般是計算密集型的。也就是說,它須要至關可觀的處理器時間。因此,上下文切換對系統來講意味着消耗大量的 CPU 時間,事實上,多是操做系統中時間消耗最大的操做。
通常指加鎖,對加鎖來講,須要增長額外的指令,這些指令都須要刷新緩存等等操做。
會致使線程掛起【掛起:掛起進程在操做系統中能夠定義爲暫時被淘汰出內存的進程,機器的資源是有限的,在資源不足的狀況下,操做系統對在內存中的程序進行合理的安排,其中有的進程被暫時調離出內存,當條件容許的時候,會被操做系統再次調回內存,從新進入等待被執行的狀態即就緒態,系統在超過必定的時間沒有任何動做】。很明顯這個操做包括兩次額外的上下文切換。
使用鎖的時候,鎖所保護的對象是多個,當這些多個對象實際上是獨立變化的時候,不如用多個鎖來一一保護這些對象。可是若是有同時要持有多個鎖的業務方法,要注意避免發生死鎖
code
/** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description: */ public class FinenessLock { public final Set<String> users = new HashSet<String>(); // 鎖的粒度過大,在執行方法體是就進行加鎖 // public void synchronized addUser(String u) { // System.out.println("test"); // users.add(u); // } //只針對對操做的對象進行加鎖,若是方法體重還有其餘方法,則不影響其餘業務進行。 public void addUser(String u) { System.out.println("test"); synchronized (users) { users.add(u); } } }
加鎖原則,能加鎖對象別鎖方法,能鎖方法不要鎖類。鎖類儘可能不要使用
對鎖的持有實現快進快出,儘可能縮短持由鎖的的時間。將一些與鎖無關的代碼移出鎖的範圍,特別是一些耗時,可能阻塞的操做
code:
/** * @Auther: DarkKing * @Date: 2019/5/12 12:09 * @Description:縮小鎖的範圍 */ public class ReduceLock { private Map<String,String> matchMap = new HashMap<>(); //加鎖體力過多無關線程安全的操做 public boolean isMatch(String name,String regexp) { synchronized(this){ String key = "user."+name; String job = matchMap.get(key); if(job == null) { return false; }else { return Pattern.matches(regexp, job);//很耗費時間 } } } //只對有可能發生線程安全問題的操做進行加鎖 public boolean isMatchReduce(String name,String regexp) { String key = "user."+name; String job ; synchronized(this) { job = matchMap.get(key); } if(job == null) { return false; }else { return Pattern.matches(regexp, job); } } }
兩次加鎖之間的語句很是簡單,致使加鎖的時間比執行這些語句還長,這個時候應該進行鎖粗化—擴大鎖的範圍。
ConcurrrentHashMap就是典型的鎖分段。下一篇會進行ConcurrrentHashMap源碼進行分析。
在業務容許的狀況下:
本章重點,瞭解什麼是線程安全,實現類線程安全的幾種方式,線程安全可能會引起什問題以及性能瓶頸如何去解決和優化。下一章將會對java裏的一些併發容器介紹,以及源碼進行分析。
今天送波福利電子書,你們有興趣能夠下載哦。
連接:https://pan.baidu.com/s/1oTHGDw-7_A8Yhn5MzpuDNA
提取碼:g5h1
其餘閱讀 併發編程專題
有什麼問題能夠留言或者掃下方二維碼,加我微信進行溝通哦。