記得今年3月份剛來杭州面試的時候,有一家公司的技術總監問了我這樣一個問題:你來講說有哪些線程安全的類?我內心一想,這我早都背好了,稀里嘩啦說了一大堆。java
他又接着問:那你再來講說什麼是線程安全?——而後我就GG了。說真的,咱們成天說線程安全,可是對於什麼是線程安全咱們真的瞭解嗎?以前的我真的是瞭解甚微,那麼咱們今天就來聊聊這個問題。程序員
在探討線程安全以前,咱們先來聊聊什麼是進程。面試
電腦中時會有不少單獨運行的程序,每一個程序有一個獨立的進程,而進程之間是相互獨立存在的。好比下圖中的QQ、酷狗播放器、電腦管家等等。安全
進程想要執行任務就須要依賴線程。換句話說,就是進程中的最小執行單位就是線程,而且一個進程中至少有一個線程。多線程
那什麼是多線程?提到多線程這裏要說兩個概念,就是串行和並行,搞清楚這個,咱們才能更好地理解多線程。併發
所謂串行,實際上是相對於單條線程來執行多個任務來講的,咱們就拿下載文件來舉個例子:當咱們下載多個文件時,在串行中它是按照必定的順序去進行下載的,也就是說,必須等下載完A以後才能開始下載B,它們在時間上是不可能發生重疊的。ide
並行:下載多個文件,開啓多條線程,多個文件同時進行下載,這裏是嚴格意義上的,在同一時刻發生的,並行在時間上是重疊的。性能
瞭解了這兩個概念以後,咱們再來講說什麼是多線程。舉個例子,咱們打開騰訊管家,騰訊管家自己就是一個程序,也就是說它就是一個進程,它裏面有不少的功能,咱們能夠看下圖,能查殺病毒、清理垃圾、電腦加速等衆多功能。測試
按照單線程來講,不管你想要清理垃圾、仍是要病毒查殺,那麼你必須先作完其中的一件事,才能作下一件事,這裏面是有一個執行順序的。this
若是是多線程的話,咱們其實在清理垃圾的時候,還能夠進行查殺病毒、電腦加速等等其餘的操做,這個是嚴格意義上的同一時刻發生的,沒有執行上的前後順序。
以上就是,一個進程運行時產生了多個線程。
在瞭解完這個問題後,咱們又須要去了解一個使用多線程不得不考慮的問題——線程安全。
今天咱們不說如何保證一個線程的安全,咱們聊聊什麼是線程安全?由於我以前面試被問到了,說真的,我以前真的不是特別瞭解這個問題,咱們好像只學瞭如何確保一個線程安全,殊不知道所謂的安全究竟是什麼!
既然是線程安全問題,那麼毫無疑問,全部的隱患都是在多個線程訪問的狀況下產生的,也就是咱們要確保在多條線程訪問的時候,咱們的程序還能按照咱們預期的行爲去執行,咱們看一下下面的代碼。
Integer count = 0; public void getCount() { count ++; System.out.println(count); }
很簡單的一段代碼,下面咱們就來統計一下這個方法的訪問次數,多個線程同時訪問會不會出現什麼問題,我開啓的3條線程,每一個線程循環10次,獲得如下結果:
咱們能夠看到,這裏出現了兩個26,出現這種狀況顯然代表這個方法根本就不是線程安全的,出現這種問題的緣由有不少。
最多見的一種,就是咱們A線程在進入方法後,拿到了count的值,剛把這個值讀取出來,尚未改變count的值的時候,結果線程B也進來的,那麼致使線程A和線程B拿到的count值是同樣的。
那麼由此咱們能夠了解到,這確實不是一個線程安全的類,由於他們都須要操做這個共享的變量。其實要對線程安全問題給出一個明確的定義,仍是蠻複雜的,咱們根據咱們這個程序來總結下什麼是線程安全。
當多個線程訪問某個方法時,無論你經過怎樣的調用方式、或者說這些線程如何交替地執行,咱們在主程序中不須要去作任何的同步,這個類的結果行爲都是咱們設想的正確行爲,那麼咱們就能夠說這個類是線程安全的。
搞清楚了什麼是線程安全,接下來咱們看看Java中確保線程安全最經常使用的兩種方式。先來看段代碼。
public void threadMethod(int j) { int i = 1; j = j + i; }
你們以爲這段代碼是線程安全的嗎?
毫無疑問,它絕對是線程安全的,咱們來分析一下,爲何它是線程安全的?
咱們能夠看到這段代碼是沒有任何狀態的,就是說咱們這段代碼,不包含任何的做用域,也沒有去引用其餘類中的域進行引用,它所執行的做用範圍與執行結果只存在它這條線程的局部變量中,而且只能由正在執行的線程進行訪問。當前線程的訪問,不會對另外一個訪問同一個方法的線程形成任何的影響。
兩個線程同時訪問這個方法,由於沒有共享的數據,因此他們之間的行爲,並不會影響其餘線程的操做和結果,因此說無狀態的對象,也是線程安全的。
若是咱們給這段代碼添加一個狀態,添加一個count,來記錄這個方法並命中的次數,每請求一次count+1,那麼這個時候這個線程仍是安全的嗎?
public class ThreadDemo { int count = 0; // 記錄方法的命中次數 public void threadMethod(int j) { count++ ; int i = 1; j = j + i; } }
很明顯已經不是了,單線程運行起來確實是沒有任何問題的,可是當出現多條線程併發訪問這個方法的時候,問題就出現了,咱們先來分析下count+1這個操做。
進入這個方法以後首先要讀取count的值,而後修改count的值,最後才把這把值賦值給count,總共包含了三步過程:「讀取」一>「修改」一>「賦值」,既然這個過程是分步的,那麼咱們先來看下面這張圖,看看你能不能看出問題:
能夠發現,count的值並非正確的結果,當線程A讀取到count的值,可是尚未進行修改的時候,線程B已經進來了,而後線程B讀取到的仍是count爲1的值,正由於如此因此咱們的count值已經出現了誤差,那麼這樣的程序放在咱們的代碼中,是存在不少的隱患的。
既然存在線程安全的問題,那麼確定得想辦法解決這個問題,怎麼解決?咱們說說常見的幾種方式。
synchronized關鍵字,就是用來控制線程同步的,保證咱們的線程在多線程環境下,不被多個線程同時執行,確保咱們數據的完整性,使用方法通常是加在方法上。
public class ThreadDemo { int count = 0; // 記錄方法的命中次數 public synchronized void threadMethod(int j) { count++ ; int i = 1; j = j + i; } }
這樣就能夠確保咱們的線程同步了,同時這裏須要注意一個你們平時忽略的問題,首先synchronized鎖的是括號裏的對象,而不是代碼,其次,對於非靜態的synchronized方法,鎖的是對象自己也就是this。
當synchronized鎖住一個對象以後,別的線程若是想要獲取鎖對象,那麼就必須等這個線程執行完釋放鎖對象以後才能夠,不然一直處於等待狀態。
注意點:雖然加synchronized關鍵字,可讓咱們的線程變得安全,可是咱們在用的時候,也要注意縮小synchronized的使用範圍,若是隨意使用時很影響程序的性能,別的對象想拿到鎖,結果你沒用鎖還一直把鎖佔用,這樣就有點浪費資源。
先來講說它跟synchronized有什麼區別吧,Lock是在Java1.6被引入進來的,Lock的引入讓鎖有了可操做性,什麼意思?就是咱們在須要的時候去手動的獲取鎖和釋放鎖,甚至咱們還能夠中斷獲取以及超時獲取的同步特性,可是從使用上說Lock明顯沒有synchronized使用起來方便快捷。咱們先來看下通常是如何使用的:
private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子類 private void method(Thread thread){ lock.lock(); // 獲取鎖對象 try { System.out.println("線程名:"+thread.getName() + "得到了鎖"); // Thread.sleep(2000); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名:"+thread.getName() + "釋放了鎖"); lock.unlock(); // 釋放鎖對象 } }
進入方法咱們首先要獲取到鎖,而後去執行咱們業務代碼,這裏跟synchronized不一樣的是,Lock獲取的所對象須要咱們親自去進行釋放,爲了防止咱們代碼出現異常,因此咱們的釋放鎖操做放在finally中,由於finally中的代碼不管如何都是會執行的。
寫個主方法,開啓兩個線程測試一下咱們的程序是否正常:
public static void main(String[] args) { LockTest lockTest = new LockTest(); // 線程1 Thread t1 = new Thread(new Runnable() { @Override public void run() { // Thread.currentThread() 返回當前線程的引用 lockTest.method(Thread.currentThread()); } }, "t1"); // 線程2 Thread t2 = new Thread(new Runnable() { @Override public void run() { lockTest.method(Thread.currentThread()); } }, "t2"); t1.start(); t2.start(); }
結果:
能夠看出咱們的執行,是沒有任何問題的。
其實在Lock還有幾種獲取鎖的方式,咱們這裏再說一種,就是tryLock()這個方法跟Lock()是有區別的,Lock在獲取鎖的時候,若是拿不到鎖,就一直處於等待狀態,直到拿到鎖,可是tryLock()卻不是這樣的,tryLock是有一個Boolean的返回值的,若是沒有拿到鎖,直接返回false,中止等待,它不會像Lock()那樣去一直等待獲取鎖。
咱們來看下代碼:
private void method(Thread thread){ // lock.lock(); // 獲取鎖對象 if (lock.tryLock()) { try { System.out.println("線程名:"+thread.getName() + "得到了鎖"); // Thread.sleep(2000); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名:"+thread.getName() + "釋放了鎖"); lock.unlock(); // 釋放鎖對象 } } }
結果:咱們繼續使用剛纔的兩個線程進行測試能夠發現,在線程t1獲取到鎖以後,線程t2立馬進來,而後發現鎖已經被佔用,那麼這個時候它也不在繼續等待。
彷佛這種方法,感受不是很完美,若是我第一個線程,拿到鎖的時間,比第二個線程進來的時間還要長,是否是也拿不到鎖對象?
那我能不能,用一中方式來控制一下,讓後面等待的線程,能夠等待5秒,若是5秒以後,還獲取不到鎖,那麼就中止等,其實tryLock()是能夠進行設置等待的相應時間的。
private void method(Thread thread) throws InterruptedException { // lock.lock(); // 獲取鎖對象 // 若是2秒內獲取不到鎖對象,那就再也不等待 if (lock.tryLock(2,TimeUnit.SECONDS)) { try { System.out.println("線程名:"+thread.getName() + "得到了鎖"); // 這裏睡眠3秒 Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名:"+thread.getName() + "釋放了鎖"); lock.unlock(); // 釋放鎖對象 } } }
結果:看上面的代碼,咱們能夠發現,雖然咱們獲取鎖對象的時候,能夠等待2秒,可是咱們線程t1在獲取鎖對象以後,執行任務缺花費了3秒,那麼這個時候線程t2是不在等待的。
咱們再來改一下這個等待時間,改成5秒,再來看下結果:
private void method(Thread thread) throws InterruptedException { // lock.lock(); // 獲取鎖對象 // 若是5秒內獲取不到鎖對象,那就再也不等待 if (lock.tryLock(5,TimeUnit.SECONDS)) { try { System.out.println("線程名:"+thread.getName() + "得到了鎖"); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名:"+thread.getName() + "釋放了鎖"); lock.unlock(); // 釋放鎖對象 } } }
結果:這個時候咱們能夠看到,線程t2等到5秒獲取到了鎖對象,執行了任務代碼。
以上就是使用Lock,來保證咱們線程安全的方式。
做者:一個非科班出身的Diao絲男,自學半年多,找到了一份還不錯的工做,我但願作一個專一於Java領域與思惟認知的公衆號,但願能夠帶領更多的初學者和入門選手,經過本身努力,獲得更多的技術上的提高和思惟認知上的拓展。
聲明:本文爲公衆號:一個程序員的成長,版權歸對方全部。