這些線程安全的坑,你在工作中踩了么?
我們知道多線程能并發(fā)的處理多個(gè)任務(wù),有效地提高復(fù)雜應(yīng)用程序的性能,在實(shí)際開發(fā)中扮演著十分重要的角色
但是使用多線程也帶來了很多風(fēng)險(xiǎn),并且由線程引起的問題往往在測(cè)試中難以發(fā)現(xiàn),到了線上就會(huì)造成重大的故障和損失
下面我會(huì)結(jié)合幾個(gè)實(shí)際案例,幫助大家在工作做規(guī)避這些問題
多線程問題
首先介紹下使用的多線程會(huì)有哪些問題
使用多線程的問題很大程度上源于多個(gè)線程對(duì)同一變量的操作權(quán),以及不同線程之間執(zhí)行順序的不確定性
《Java并發(fā)編程實(shí)戰(zhàn)》這本書中提到了三種多線程的問題:安全性問題、活躍性問題和性能問題
安全性問題
例如有一段很簡(jiǎn)單的扣庫(kù)存功能操作,如下:
public int decrement(){
return --count;//count初始庫(kù)存為10
}
在單線程環(huán)境下,這個(gè)方法能正確工作,但在多線程環(huán)境下,就會(huì)導(dǎo)致錯(cuò)誤的結(jié)果
--count看上去是一個(gè)操作,但實(shí)際上它包含三步(讀取-修改-寫入):
讀取count的值
將值減一
最后把計(jì)算結(jié)果賦值給count
如下圖展示了一種錯(cuò)誤的執(zhí)行過程,當(dāng)有兩個(gè)線程1、2同時(shí)執(zhí)行該方法時(shí),它們讀取到count的值都是10,最后返回結(jié)果都是9;意味著可能有兩個(gè)人購(gòu)買了商品,但庫(kù)存卻只減了1,這對(duì)于真實(shí)的生產(chǎn)環(huán)境是不可接受的

像上面例子這樣由于不恰當(dāng)?shù)膱?zhí)行時(shí)序?qū)е虏徽_結(jié)果的情況,是一種很常見的并發(fā)安全問題,被稱為競(jìng)態(tài)條件
decrement()方法這個(gè)導(dǎo)致發(fā)生競(jìng)態(tài)條件的代碼區(qū)被稱為臨界區(qū)
避免這種問題,需要保證讀取-修改-寫入這樣復(fù)合操作的原子性
在Java中,有很多方式可以實(shí)現(xiàn),比如使用synchronize內(nèi)置鎖或ReentrantLock顯式鎖的加鎖機(jī)制、使用線程安全的原子類、以及采用CAS的方式等
活躍性問題
活躍性問題指的是,某個(gè)操作因?yàn)樽枞蜓h(huán),無法繼續(xù)執(zhí)行下去
最典型的有三種,分別為死鎖、活鎖和饑餓
死鎖
最常見的活躍性問題是死鎖
死鎖是指多個(gè)線程之間相互等待獲取對(duì)方的鎖,又不會(huì)釋放自己占有的鎖,而導(dǎo)致阻塞使得這些線程無法運(yùn)行下去就是死鎖,它往往是不正確的使用加鎖機(jī)制以及線程間執(zhí)行順序的不可預(yù)料性引起的

如何預(yù)防死鎖
1.盡量保證加鎖順序是一樣的
例如有A,B,C三把鎖。
Thread 1的加鎖順序?yàn)锳、B、C這樣的。 Thread 2的加鎖順序?yàn)锳、C,這樣就不會(huì)死鎖。
tryLock(long time, TimeUnit unit)方法,該方法可以按照固定時(shí)長(zhǎng)等待鎖,因此線程可以在獲取鎖超時(shí)以后,主動(dòng)釋放之前已經(jīng)獲得的所有的鎖??梢员苊馑梨i問題活鎖
饑餓
舉例說明
線程不安全類
案例1
ConcurrentModificationException的異常,也就是常說的fail-fast機(jī)制List<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
list.add(2); //list: [0,1,2]
System.out.println(list);
//線程t1遍歷打印list
Thread t1 = new Thread(() -> {
for(int i : list){
System.out.println(i);
}
});
//線程t2向list添加元素
Thread t2 = new Thread(() -> {
for(int i = 3; i < 6; i++){
list.add(i);
}
});
t1.start();
t2.start();
進(jìn)到拋異常的ArrayList源碼中,可以看到遍歷ArrayList是通過內(nèi)部實(shí)現(xiàn)的迭代器完成的checkForComodification()方法檢查modCount和expectedModCount是否相等,若不相等則拋出ConcurrentModificationException
modCount是ArrayList的屬性,表示集合結(jié)構(gòu)被修改的次數(shù)(列表長(zhǎng)度發(fā)生變化的次數(shù)),每次調(diào)用add或remove等方法都會(huì)使modCount加1expectedModCount=modCount)
List<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
list.add(2);
System.out.println(list);
//線程t1遍歷打印list
Thread t1 = new Thread(() -> {
synchronized (list){ //使用synchronized關(guān)鍵字
for(int i : list){
System.out.println(i);
}
}
});
//線程t2向list添加元素
Thread t2 = new Thread(() -> {
synchronized (list){
for(int i = 3; i < 6; i++){
list.add(i);
System.out.println(list);
}
}
});
t1.start();
t2.start();
案例2
public static final SimpleDateFormat SDF_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
//兩個(gè)線程同時(shí)調(diào)用SimpleDateFormat.parse方法
Thread t1 = new Thread(() -> {
try {
Date date1 = SDF_FORMAT.parse("2019-12-09 17:04:32");
} catch (ParseException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
Date date2 = SDF_FORMAT.parse("2019-12-09 17:43:32");
} catch (ParseException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
}
建議將SimpleDateFormat作為局部變量使用,或者配合ThreadLocal使用//初始化
public static final ThreadLocal<SimpleDateFormat> SDF_FORMAT = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
//調(diào)用
Date date = SDF_FORMAT.get().parse(wedDate);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime time = LocalDateTime.now();
System.out.println(formatter.format(time));
鎖的正確釋放
Lock lock = new ReentrantLock();
...
try{
lock.tryLock(timeout, TimeUnit.MILLISECONDS)
//業(yè)務(wù)邏輯
}
catch (Exception e){
//錯(cuò)誤日志
//拋出異?;蛑苯臃祷?/span>
}
finally {
//業(yè)務(wù)邏輯
lock.unlock();
}
...
正確使用線程池
案例1
public void request(List<Id> ids) {
for (int i = 0; i < ids.size(); i++) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
}}
案例2
Executors.newFixedThreadPool(int); //創(chuàng)建固定容量大小的線程池
Executors.newSingleThreadExecutor(); //創(chuàng)建容量為1的線程池
Executors.newCachedThreadPool(); //創(chuàng)建一個(gè)線程池,線程池容量大小為Integer.MAX_VALUE
Integer.MAX_VALUE=2147483647,對(duì)于真正的機(jī)器來說,可以被認(rèn)為是無界隊(duì)列newFixedThreadPool和newSingleThreadExecutor在運(yùn)行的線程數(shù)超過corePoolSize時(shí),后來的請(qǐng)求會(huì)都被放到阻塞隊(duì)列中等待,因?yàn)樽枞?duì)列設(shè)置的過大,后來請(qǐng)求不能快速失敗而長(zhǎng)時(shí)間阻塞,就可能造成請(qǐng)求端的線程池被打滿,拖垮整個(gè)服務(wù)。
Integer.MAX_VALUE,阻塞隊(duì)列使用的SynchronousQueue,SynchronousQueue不會(huì)保存等待執(zhí)行的任務(wù)所以newCachedThreadPool是來了任務(wù)就創(chuàng)建線程運(yùn)行,而maximumPoolSize相當(dāng)于無限的設(shè)置,使得創(chuàng)建的線程數(shù)可能會(huì)將機(jī)器內(nèi)存占滿。
線程數(shù)建議
1.CPU密集型應(yīng)用
corePoolSize=CPU核數(shù)+1個(gè)線程。JVM可運(yùn)行的CPU核數(shù)可以通過Runtime.getRuntime().availableProcessors()查看。2.IO密集型應(yīng)用
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
