Java中確保線程安全最經常使用的兩種方式

上篇文章咱們簡單聊了什麼是多線程,我想你們對多線程已經有了一個初步的瞭解,沒看的沒有放下文章連接 什麼是線程安全,你真的瞭解嗎?安全


上篇咱們搞清楚了什麼樣的線程是安全的,咱們今天先來看段代碼:bash

public void threadMethod(int j) {

    int i = 1;

    j = j + i;
}複製代碼


你們以爲這段代碼是線程安全的嗎?多線程


毫無疑問,它絕對是線程安全的,咱們來分析一下爲何它是線程安全的?併發


咱們能夠看到這段代碼是沒有任何狀態的,什麼意思,就是說咱們這段代碼不包含任何的做用域,也沒有去引用其餘類中的域進行引用,它所執行的做用範圍與執行結果只存在它這條線程的局部變量中,而且只能由正在執行的線程進行訪問。當前線程的訪問不會對另外一個訪問同一個方法的線程形成任何的影響。ide


兩個線程同時訪問這個方法,由於沒有共享的數據,因此他們之間的行爲並不會影響其餘線程的操做和結果,因此說無狀態的對象也是線程安全的。性能


                                                        添加一個狀態呢?

若是咱們給這段代碼添加一個狀態,添加一個count,來記錄這個方法並命中的次數,每請求一次count+1,那麼這個時候這個線程仍是安全的嗎?測試


public class ThreadDemo {

   int count = 0; // 記錄方法的命中次數

   public void threadMethod(int j) {
       
       count++ ;

       int i = 1;

       j = j + i;
   }
}複製代碼


很明顯已經不是了,單線程運行起來確實是沒有任何問題的,可是當出現多條線程併發訪問這個方法的時候,問題就出現了,咱們先來分析下count+1這個操做。this


進入這個方法以後首先要讀取count的值,而後修改count的值,最後才把這把值賦值給count,總共包含了三步過程:「讀取」一>「修改」一>「賦值」,既然這個過程是分步的,那麼咱們先來看下面這張圖,看看你能不能看出問題:spa

能夠發現,count的值並非正確的結果,當線程A讀取到count的值,可是尚未進行修改的時候,線程B已經進來了,而後線程B讀取到的仍是count爲1的值,正由於如此因此咱們的count值已經出現了誤差,那麼這樣的程序放在咱們的代碼中是存在不少的隱患的。線程


二、如何確保線程安全?

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


2.一、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.二、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();
   }複製代碼


結果:

能夠看出咱們的執行是沒有任何問題的。


其實在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來保證咱們線程安全的方式,其實Lock還有好多的方法來操做咱們的鎖對象,這裏咱們就很少說了,你們有興趣能夠看一下API。

PS:如今你能作到如何確保一個方法是線程安全的嗎?

相關文章
相關標籤/搜索