南寧網(wǎng)站空間深圳網(wǎng)絡(luò)推廣推薦
競態(tài)
對于同樣的輸入,程序的輸出有時候正確而有時候卻是錯誤的。這種一個計算結(jié)果的正確性與時間有關(guān)的現(xiàn)象就被稱為競態(tài)(RaceCondition)
導(dǎo)致競態(tài)的常見原因是多個線程在沒有采取任何措施的情況下并發(fā)更新、讀取同一個共享變量。
競態(tài)往往伴隨著數(shù)據(jù)的臟讀問題,即線程讀取到一個過時的數(shù)據(jù);丟失更新問題,即一個線程丟失數(shù)據(jù)所做的更新沒有體現(xiàn)在后續(xù)其他線程對該數(shù)據(jù)的讀取上。
競態(tài)實例:
模擬RequestID生成器,RequestID是一個固定長度的編碼字符串,其中最后三位是在0~999循環(huán)遞增的序列號。
public final class RequestIDGenerator implements CircularSeqGenerator {/*** 保存該類的唯一實例*/private final static RequestIDGenerator INSTANCE = new RequestIDGenerator();private final static short SEQ_UPPER_LIMIT = 999;private short sequence = -1;// 私有構(gòu)造器private RequestIDGenerator() {// 什么也不做}/*** 生成循環(huán)遞增序列號** @return*/@Overridepublic short nextSequence() {if (sequence >= SEQ_UPPER_LIMIT) {sequence = 0;} else {sequence++;}return sequence;}/*** 生成一個新的Request ID** @return*/public String nextID() {SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmss");String timestamp = sdf.format(new Date());DecimalFormat df = new DecimalFormat("000");// 生成請求序列號short sequenceNo = nextSequence();return "0049" + timestamp + df.format(sequenceNo);}/*** 返回該類的唯一實例** @return*/public static RequestIDGenerator getInstance() {return INSTANCE;}
}
競態(tài)demo:
public class RaceConditionDemo {public static void main(String[] args) throws Exception {// 客戶端線程數(shù)
// args = new String[] {"4"};//Runtime.getRuntime().availableProcessors()-- 返回可用處理器的Java虛擬機(jī)的數(shù)量int numberOfThreads = args.length > 0 ? Short.valueOf(args[0]) : Runtime.getRuntime().availableProcessors();Thread[] workerThreads = new Thread[numberOfThreads];for (int i = 0; i < numberOfThreads; i++) {workerThreads[i] = new WorkerThread(i, 10);}// 待所有線程創(chuàng)建完畢后,再一次性將其啟動,以便這些線程能夠盡可能地在同一時間內(nèi)運(yùn)行for (Thread ct : workerThreads) {ct.start();}}// 模擬業(yè)務(wù)線程static class WorkerThread extends Thread {private final int requestCount;public WorkerThread(int id, int requestCount) {super("worker-" + id);this.requestCount = requestCount;}@Overridepublic void run() {int i = requestCount;String requestID;RequestIDGenerator requestIDGen = RequestIDGenerator.getInstance();while (i-- > 0) {// 生成Request IDrequestID = requestIDGen.nextID();processRequest(requestID);}}// 模擬請求處理private void processRequest(String requestID) {// 模擬請求處理耗時Tools.randomPause(50);System.out.printf("%s got requestID: %s %n",Thread.currentThread().getName(), requestID);}}
}
當(dāng)args = new String[] {"4"}; 時,理論上序列號最后三位是:000-039,但是多次運(yùn)行結(jié)果有時正確有時返回000-038,有時000-037。
截取部分運(yùn)行結(jié)果,其中work-0和work-2線程返回的值是一樣的。該程序在運(yùn)行過程中出現(xiàn)了競態(tài)。
worker-0 got requestID: 0049190620170236002
worker-3 got requestID: 0049190620170236000
worker-0 got requestID: 0049190620170236004
worker-1 got requestID: 0049190620170236003
worker-2 got requestID: 0049190620170236002
nextSequence()中的 sequence++ 實際上相當(dāng)于如下偽代碼:
load(sequence,r1); //指令①:從內(nèi)存將sequence的值讀取到寄存器r1(讀取共享變量)
increment(r1); //指令②:將寄存器的r1值增加1(共享變量做計算)
store(sequence,r1); //指令③:將寄存器r1的內(nèi)容寫入sequence對應(yīng)的內(nèi)存空間(更新變量)
發(fā)生原因:
一個線程在執(zhí)行完指令①之后到開始執(zhí)行指令②的這段時間內(nèi)其他線程可能已經(jīng)更新了共享變量的值,這就使得該線程在執(zhí)行指令②的時候使用的是共享變量的舊值,即臟讀數(shù)據(jù)。接著,該線程把根據(jù)這個舊值算出來的結(jié)果更新到共享變量,而這又使得其他線程對該變量所做的更新被覆蓋,造成更新丟失。
競態(tài): 一個線程讀取共享變量并以該共享變量為基礎(chǔ)進(jìn)行計算的期間另外的一個線程更新了該共享變量的值而導(dǎo)致的干擾(讀取臟數(shù)據(jù))或沖突(丟失更新)的結(jié)果。
競態(tài)防止
共享變量修改為局部變量
public class NoRaceCondition {public int nextSequence(int sequence) {// 以下語句使用的是局部變量而非狀態(tài)變量,并不會產(chǎn)生競態(tài)if (sequence >= 999) {sequence = 0;} else {sequence++;}return sequence;}}
由于不同線程各自訪問各自的那一部分局部變量,所以局部變量不會導(dǎo)致競態(tài)。、
添加synchronized關(guān)鍵字
public class SafeCircularSeqGenerator implements CircularSeqGenerator {private short sequence = -1;@Overridepublic synchronized short nextSequence() {if (sequence >= 999) {sequence = 0;} else {sequence++;}return sequence;}
}
限制只能被一個線程執(zhí)行
線程安全和非線程安全
線程安全 如果一個類在單線程環(huán)境下運(yùn)行正常,并且在多線程環(huán)境下,不做任何改變的情況下也能正常運(yùn)行,那我們就稱其是線程安全的,相應(yīng)的我們稱這個類具有線程安全性。
非線程安全 反之我們則為非線程安全。
線程安全概述:
原子性
原子的意思是不可再分。對于設(shè)計共享變量訪問的操作,若該操作從其執(zhí)行線程以外的任意線程來看是不可分割的,那么該操作就是原子操作,相應(yīng)的我們稱該操作具有原子性。
原子操作是多線程環(huán)境下的一個概念,他是針對訪問共享變量的操作而言的。原子操作的不可分割包括以下兩層含義:
- 訪問(讀寫)某個共享變量的操作從其執(zhí)行線程以外的任何線程來看,
該操作要么已經(jīng)執(zhí)行結(jié)束要么尚未發(fā)生,即其他線程不會‘看到’該操作執(zhí)行了部分的中間效果。 - 訪問一組共享變量的原子操作是不能夠被交錯的。
java如何實現(xiàn)原子性:
- 使用鎖(lock)。鎖具有排他性,它能保證一個共享變量在任意一個時刻只能夠被一個線程訪問。
- 另一種是利用處理器提供的專門CAS指令。CAS指令實現(xiàn)原子性的方式和鎖實現(xiàn)原子性的方式實質(zhì)上是相同的,差別在于鎖是在軟件層次實現(xiàn),而CAS是直接在硬件(處理器和內(nèi)存)層次實現(xiàn),可以看做“硬件鎖”。
可見性
可見性:在多線程環(huán)境下,一個線程對共享變量做了更新,而其它線程在后續(xù)的訪問過程中無法立刻讀取到更新后的內(nèi)容,甚至永遠(yuǎn)無法讀取到更新后的內(nèi)容。
后續(xù)訪問該變量的線程可以讀取到更新后的結(jié)果,我們稱這線程對共享變量的更新對其他線程可見。反之,則稱為不可見。
不可見產(chǎn)生的原因:
- 代碼沒有給編譯器足夠的提示,使其認(rèn)為該狀態(tài)變量只有一個線程訪問,從而使編譯器為了避免重復(fù)讀取該變量而對代碼做了優(yōu)化。
- 可見性問題與與計算機(jī)的儲存系統(tǒng)也有關(guān)。
如何保證可見性?
對實例變量添加關(guān)鍵字:volatile
- 一個作用是提示JIT編譯器被修飾的變量可能被多個線程共享,以阻止編譯器做出可能導(dǎo)致程序不正常的優(yōu)化。
- 另一個作用是讀取一個volatile關(guān)鍵字修飾的變量會使相應(yīng)的處理器執(zhí)行刷新處理器緩存的動作,寫一個volatile修飾的變量會使相應(yīng)的處理器執(zhí)行沖刷處理器緩存的動作,從而保障了可見性。
轉(zhuǎn)載于:https://www.cnblogs.com/sanzashu/p/11065294.html