線程安全一直是多線程開發(fā)中需要注意的地方,可以說,并發(fā)安全保證了所有的數(shù)據(jù)都安全。
1 線程不安全示例
線程安全其實是多線程編程里面的一個核心點,所有的設計和代碼都是為了實現(xiàn)線程的高效與安全。
多線程中有幾個比較核心概念,即原子性,可見性,順序性。那么線程安全也會圍繞著這三個核心來展開嘍。
下面我們看一兩個簡單的問題多線程。
簡單買票的線程安全問題
public class ThreadSafeDemo3 {
public static void main(String[] args) throws InterruptedException {
TicketStation station = new TicketStation();
new Thread(station,"軟軟").start();
new Thread(station,"冰冰").start();
new Thread(station,"指北君").start();
}
}
class TicketStation implements Runnable{
int ticketCount = 10;
boolean hasTicket = true;
@Override
public void run() {
while(hasTicket){buyTicket();}
}
private void buyTicket(){
if (ticketCount < 1) {
hasTicket = false;
return;
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " get the ticket"+ ticketCount--);
}
}
運行幾遍就有可能會出現(xiàn)下面的錯誤不預期的結果。一個線程賣完了票,但是另外兩個線程都還不知道。
image-20210926221216385
多線程操作非線程安全對象問題
public class ThreadSafeDemo2 {
public static void main(String[] args) throws InterruptedException {
List< String > list = new ArrayList< >();
for (int i =0 ;i< 20 ;i++) {
new Thread(() - > {
for (int j = 0; j < 5; j++) {
list.add(Thread.currentThread().getName() + j);
}
}, "thread" + i).start();
}
Thread.sleep(1000*3);
System.out.println(list.size());
}
}
以上代碼多執(zhí)行幾次之后會,所得list的size不會等于100。問題就在于多線程操作同一個線程不安全的List的時候,會是結果與預期不符,出現(xiàn)線程安全問題。
以上是兩個線程不安全的示例,而對于線程安全,應該要做到如下:
當多個線程訪問某個方法的時候,不管你通過怎樣的調用方法,或者說這些線程如何交替執(zhí)行,我們在主程序中不需要去做任何的同步,這個類的行為都是我們設想的正確行為,那么我們可以說這個類是線程安全的。即可以保證原子性,可見性,順序性。
2 并發(fā)安全的問題根源
線程不安全指的是多線程并發(fā)執(zhí)行某個代碼時,產(chǎn)生了邏輯上的錯誤,結果和預期值不相同。
其原因可以總結如下:
- Java線程是搶占執(zhí)行的。
- 有些操作不是原子的,cpu在處理某一個線程的時候,有可能被其他線程搶去做工。
- 內(nèi)存共享可變。
- 指令重排序:Java編譯器在編譯代碼時,會對最終執(zhí)行的指令重排序,它會保證原有邏輯不變的情況下,提高程序的運行效率。
3 線程安全不是絕對的
《深入理解JVM》中有講到如果要保證絕對線程安全,在大多數(shù)的應用場景下是難以做到的,或者說很難做到,即使做到,也會付出很大的代價。而且在Java中標注的某些線程安全的類也不是絕對的線程安全,也需要在調用時使用一些額外操作。
我們大多時候都是盡量保證線程的相對安全,對一個對象單獨操作的時候保證線程安全,而對于一些特殊的調用情況,我們則需要采取一些同步操作付諸。
4 線程安全的實現(xiàn)方法
我們在寫代碼的時候,保證線程安全的方法有多種,下面我們介紹幾種方式。
4.1 互斥同步
互斥的特點是在同一時刻只有一個線程獲得執(zhí)行權利,其余線程則會等待。( 同一時刻,只有一個線程在操作共享數(shù)據(jù) ) 互斥是實現(xiàn)同步的一種手段, 臨界區(qū)、互斥量、信號量都是主要的互斥實現(xiàn)方式。即通過實現(xiàn)互斥來最終完成同步的目的。
4.1.1 Synchronized
synchronized是同步鎖,主要用來控制線程同步,保證某個鎖住的內(nèi)容不被多個線程同步執(zhí)行。上述買票的例子中,在buyTicket方法上加上synchronized關鍵字,就可以使線程同步執(zhí)行了。
private synchronized void buyTicket(){}
其中synchronized 使用有幾點注意:
- 加到非靜態(tài)方法前,表示鎖this,即當前對象
- 加到靜態(tài)方法前,表示鎖當前類的所有類對象
4.1.2 Lock
Lock 是Java1.6之后引入的。使用Lock可以對鎖進行多種操作,可以手動的獲取鎖,釋放鎖。
我們用ReentrantLock (ReentrantLock傳送門。。。)改寫上述購票行為。
class TicketLockStation implements Runnable{
private Lock lock = new ReentrantLock();
int ticketCount = 10;
boolean hasTicket = true;
@Override
public void run() {
while(hasTicket){buyTicket();}
}
private void buyTicket(){
lock.lock();
try {
if (ticketCount < 1) {
hasTicket = false;
return;
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " get the ticket"+ ticketCount--);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
以上為簡單Lock示例,其中Lock中還有tryLock()(如果獲取不到鎖立即返回), tryLock(long time, TimeUnit unit)(一段時間后獲取不到鎖則返回)等方法。
上邊就是Lock的簡單示例。
4.2 非阻塞同步
非阻塞同步可以描述為 基于沖突檢測的樂觀并發(fā)策略 ,關鍵點就是沖突檢測以及樂觀的并發(fā)策略。
沖突檢測是指當發(fā)生共享數(shù)據(jù)搶奪的話,我們會進行重試檢測,直到成功為止。而樂觀的并發(fā)策略的實現(xiàn)大多時候都不需要掛起線程。
4.2.1 CAS
CAS(Compare And Swap)是非阻塞的一個實現(xiàn),其核心指令有3個操作數(shù),分別為內(nèi)存地址V, 舊值A,新值B。當CAS執(zhí)行時當 V的內(nèi)存地址對應的值與A匹配時,本操作就會用B來更新V對應的值,否則不執(zhí)行更新。但無論是否更新了V對應的值,都會返回V處對應的舊值。而且此操作為原子操作。
CAS有一個缺點就是ABA問題,V處的值原來是A,后來變成了B,然后又變成了A。使用CAS檢查的時候發(fā)現(xiàn)其值沒變化,然而實際上已經(jīng)發(fā)生了變化。對于解決ABA問題,可以使用版本號的思路來解決,在更新變量的時候把版本號加一。然后對比的時候也對比版本號,版本號與值全都相等則執(zhí)行更新。
4.3 無同步方案
保證線程安全的方法中,也并不是一定要使用同步。同步只是在保證共享數(shù)據(jù)在有競爭條件的時候使用。如果有方法可以避免共享數(shù)據(jù)的競爭,那么自然就不需要任何同步操作去保證數(shù)據(jù)的正確。所以在有一些場景下,代碼自身就已經(jīng)保證了線程安全,而無須使用同步方法。
4.3.1 棧封閉
其實主要就是盡量保證數(shù)據(jù)的操作在一個棧幀中,也就是局部變量,避免過多的去操作共享內(nèi)存數(shù)據(jù)。
4.3.2 線程本地存儲
如果代碼中所需的數(shù)據(jù)必須與其他代碼共享,那么就可以看看這些共享數(shù)據(jù)的代碼是否能保證在同一個線程中執(zhí)行。如果可以保證共享數(shù)據(jù)在同一個線程之內(nèi)是可見的,那么線程之間也就不會出現(xiàn)數(shù)據(jù)的競爭。
ThreadLocal就是一個最典型的例子,Web應用中的Request也是這樣的思路。
4.3.3 可重入代碼 (Reentrant Code)
可重入代碼指在代碼執(zhí)行的任何時刻中斷它,轉而去執(zhí)行另外一段代碼,而控制權返回后,原來的程序不會出現(xiàn)任何錯誤。所有的可重入代碼都是線程安全的。一般而言可重入代碼不依賴存儲在堆上的數(shù)據(jù)以及公共的系統(tǒng)資源,用到的狀態(tài)量都是有參數(shù)傳入,或者說不調用,非可重入的方法等。
總結
關于線程安全,我們可以總結以下的一些思路。
- 使用互斥同步的方法。
- 使用Synchronized
- 使用Lock
- 使用非阻塞同步方案。
- CAS 等。
- 無同步方案
其實就是在設計上盡量避免共享變量的使用,這樣也就可以避免線程安全問題的發(fā)生。
-
數(shù)據(jù)
+關注
關注
8文章
7223瀏覽量
90194 -
程序
+關注
關注
117文章
3807瀏覽量
81724 -
多線程
+關注
關注
0文章
278瀏覽量
20111 -
代碼
+關注
關注
30文章
4858瀏覽量
69550 -
線程
+關注
關注
0文章
507瀏覽量
19864
發(fā)布評論請先 登錄
相關推薦
評論