如何保證線程安全,看這里

上篇文章我們簡單聊了什么是多線程,我想大家對多線程已經(jīng)有了一個初步的了解,沒看的沒有放下文章鏈接?什么是線程安全,你真的了解嗎?

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

public?void?threadMethod(int?j)?{

?int?i =?1;

j = j + i;

}

大家覺得這段代碼是線程安全的嗎?

毫無疑問,它絕對是線程安全的,我們來分析一下為什么它是線程安全的?

我們可以看到這段代碼是沒有任何狀態(tài)的,什么意思,就是說我們這段代碼不包含任何的作用域,也沒有去引用其他類中的域進行引用,它所執(zhí)行的作用范圍與執(zhí)行結(jié)果只存在它這條線程的局部變量中,并且只能由正在執(zhí)行的線程進行訪問。當前線程的訪問不會對另一個訪問同一個方法的線程造成任何的影響。

兩個線程同時訪問這個方法,因為沒有共享的數(shù)據(jù),所以他們之間的行為并不會影響其他線程的操作和結(jié)果,所以說無狀態(tài)的對象也是線程安全的。

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 添加一個狀態(tài)呢?

如果我們給這段代碼添加一個狀態(tài),添加一個count,來記錄這個方法并命中的次數(shù),每請求一次count+1,那么這個時候這個線程還是安全的嗎?

public?class?ThreadDemo?{

int?count =?0;?// 記錄方法的命中次數(shù)

public?void?threadMethod(int?j)?{

count++ ;

int?i =?1;

j = j + i;

}

}

很明顯已經(jīng)不是了,單線程運行起來確實是沒有任何問題的,但是當出現(xiàn)多條線程并發(fā)訪問這個方法的時候,問題就出現(xiàn)了,我們先來分析下count+1這個操作。

進入這個方法之后首先要讀取count的值,然后修改count的值,最后才把這把值賦值給count,總共包含了三步過程:“讀取”一>“修改”一>“賦值”,既然這個過程是分步的,那么我們先來看下面這張圖,看看你能不能看出問題:

如何確保線程安全?

可以發(fā)現(xiàn),count的值并不是正確的結(jié)果,當線程A讀取到count的值,但是還沒有進行修改的時候,線程B已經(jīng)進來了,然后線程B讀取到的還是count為1的值,正因為如此所以我們的count值已經(jīng)出現(xiàn)了偏差,那么這樣的程序放在我們的代碼中是存在很多的隱患的。

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

1、synchronized

synchronized關(guān)鍵字就是用來控制線程同步的,保證我們的線程在多線程環(huán)境下,不被多個線程同時執(zhí)行,確保我們數(shù)據(jù)的完整性,使用方法一般是加在方法上。

public?class?ThreadDemo?{

int?count =?0;?// 記錄方法的命中次數(shù)

public?synchronized?void?threadMethod(int?j)?{

count++ ;

int?i =?1;

j = j + i;

}

}

這樣就可以確保我們的線程同步了,同時這里需要注意一個大家平時忽略的問題,首先synchronized鎖的是括號里的對象,而不是代碼,其次,對于非靜態(tài)的synchronized方法,鎖的是對象本身也就是this

當synchronized鎖住一個對象之后,別的線程如果想要獲取鎖對象,那么就必須等這個線程執(zhí)行完釋放鎖對象之后才可以,否則一直處于等待狀態(tài)。

注意點:雖然加synchronized關(guān)鍵字可以讓我們的線程變的安全,但是我們在用的時候也要注意縮小synchronized的使用范圍,如果隨意使用時很影響程序的性能,別的對象想拿到鎖,結(jié)果你沒用鎖還一直把鎖占用,這樣就應(yīng)了一句話:占著茅坑不拉屎,屬實有點浪費資源。

2、Lock

先來說說它跟synchronized有什么區(qū)別吧,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();?// 釋放鎖對象

}

}

進入方法我們首先要獲取到鎖,然后去執(zhí)行我們業(yè)務(wù)代碼,這里跟synchronized不同的是,Lock獲取的所對象需要我們親自去進行釋放,為了防止我們代碼出現(xiàn)異常,所以我們的釋放鎖操作放在finally中,因為finally中的代碼無論如何都是會執(zhí)行的。

寫個主方法,開啟兩個線程測試一下我們的程序是否正常:

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();

}

結(jié)果:

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

其實在Lock還有幾種獲取鎖的方式,我們這里再說一種就是tryLock()這個方法跟Lock()是有區(qū)別的,Lock在獲取鎖的時候如果拿不到鎖就一直處于等待狀態(tài),直到拿到鎖,但是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();?// 釋放鎖對象

}

}

}

結(jié)果:我們繼續(xù)使用剛才的兩個線程進行測試可以發(fā)現(xiàn),在線程t1獲取到鎖之后,線程t2立馬進來,然后發(fā)現(xiàn)鎖已經(jīng)被占用,那么這個時候它也不在繼續(xù)等待。

似乎這種方法感覺不是很完美,如果我第一個線程拿到鎖的時間比第二個線程進來的時間還要長,是不是也拿不到鎖對象,那我能不能用一中方式來控制一下,讓后面等待的線程可以需要等待5秒,如果5秒之后還獲取不到鎖,那么就停止等,其實tryLock()是可以進行設(shè)置等待的相應(yīng)時間的。

private?void?method(Thread thread) throws InterruptedException?{

// lock.lock(); // 獲取鎖對象

// 如果2秒內(nèi)獲取不到鎖對象,那就不再等待

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();?// 釋放鎖對象

}

}

}

結(jié)果:看上面的代碼我們可以發(fā)現(xiàn),雖然我們獲取鎖對象的時候可以等待2秒,但是我們線程t1在獲取鎖對象之后執(zhí)行任務(wù)缺花費了3秒,那么這個時候線程t2是不在等待的。

我們再來改一下這個等待時間,改為5秒,再來看下結(jié)果:

private?void?method(Thread thread) throws InterruptedException?{

? ? ? ?// lock.lock(); // 獲取鎖對象

? ? ? ?// 如果5秒內(nèi)獲取不到鎖對象,那就不再等待

? ? ? ?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();?// 釋放鎖對象

? ? ? ? ? ?}

? ? ? ?}

? ?}

結(jié)果:這個時候我們可以看到,線程t2等到5秒獲取到了鎖對象,執(zhí)行了任務(wù)代碼。

這就是使用Lock來保證我們線程安全的方式,其實Lock還有好多的方法來操作我們的鎖對象,這里我們就不多說了,大家有興趣可以看一下API。

PS:現(xiàn)在你能做到如何確保一個方法是線程安全的嗎?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.理清文章各章節(jié)脈絡(luò)和邏輯; 2.帶走文章的亮點; 3.含大量自己的感想、心得; 4.與讀過的書、已有的知識建立...
    王新印閱讀 1,998評論 0 0
  • 爸媽在家吃好的用好的,女兒們在外面省吃儉用拼死拼活掙來的死工資都被父母每次的“裝可憐”壓榨的干干凈凈! ...
    辣媽不怕辣閱讀 895評論 12 7
  • 日漸扭曲變形的社會 逐漸消磨殆盡的人性 圣潔無暇的天使成為傳說 平靜不起波瀾的現(xiàn)世里 那些鋼筋銅鑄的叢林里 一個一...
    趙金祥閱讀 584評論 0 2

友情鏈接更多精彩內(nèi)容