Java中確保執行緒安全最常用的兩種方式

一個程式設計師的成長發表於2018-09-02

上篇文章我們簡單聊了什麼是多執行緒,我想大家對多執行緒已經有了一個初步的瞭解,沒看的沒有放下文章連結 什麼是執行緒安全,你真的瞭解嗎?


上篇我們搞清楚了什麼樣的執行緒是安全的,我們今天先來看段程式碼:

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,總共包含了三步過程:“讀取”一>“修改”一>“賦值”,既然這個過程是分步的,那麼我們先來看下面這張圖,看看你能不能看出問題:

Java中確保執行緒安全最常用的兩種方式

可以發現,count的值並不是正確的結果,當執行緒A讀取到count的值,但是還沒有進行修改的時候,執行緒B已經進來了,然後執行緒B讀取到的還是count為1的值,正因為如此所以我們的count值已經出現了偏差,那麼這樣的程式放在我們的程式碼中是存在很多的隱患的。


2、如何確保執行緒安全?

既然存線上程安全的問題,那麼肯定得想辦法解決這個問題,怎麼解決?我們說說常見的幾種方式。


2.1、synchronized

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的使用範圍,如果隨意使用時很影響程式的效能,別的物件想拿到鎖,結果你沒用鎖還一直把鎖佔用,這樣就應了一句話:佔著茅坑不拉屎,屬實有點浪費資源。


2.2、Lock

先來說說它跟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();
   }複製程式碼


結果:

Java中確保執行緒安全最常用的兩種方式

可以看出我們的執行是沒有任何問題的。


其實在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立馬進來,然後發現鎖已經被佔用,那麼這個時候它也不在繼續等待。


Java中確保執行緒安全最常用的兩種方式


似乎這種方法感覺不是很完美,如果我第一個執行緒拿到鎖的時間比第二個執行緒進來的時間還要長,是不是也拿不到鎖物件,那我能不能用一中方式來控制一下,讓後面等待的執行緒可以需要等待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是不在等待的。

Java中確保執行緒安全最常用的兩種方式


我們再來改一下這個等待時間,改為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秒獲取到了鎖物件,執行了任務程式碼。

Java中確保執行緒安全最常用的兩種方式

這就是使用Lock來保證我們執行緒安全的方式,其實Lock還有好多的方法來操作我們的鎖物件,這裡我們就不多說了,大家有興趣可以看一下API。

PS:現在你能做到如何確保一個方法是執行緒安全的嗎?


相關文章