多線程操作共享資源保證原子,線程不共享的資源有哪些
- 夕逆IT
- 數(shù)據(jù)庫
- 2023-08-13
- 303
大家好,今天來為大家解答多線程操作共享資源保證原子這個問題的一些問題點,包括線程不共享的資源有哪些也一樣很多人還不知道,因此呢,今天就來為大家分析分析,現(xiàn)在讓我們一起來...
大家好,今天來為大家解答多線程操作共享資源保證原子這個問題的一些問題點,包括線程不共享的資源有哪些也一樣很多人還不知道,因此呢,今天就來為大家分析分析,現(xiàn)在讓我們一起來看看吧!如果解決了您的問題,還望您關(guān)注下本站哦,謝謝~
setifabsent是原子操作嗎
setifabsent是原子操作,
"原子操作(atomicoperation)是不需要synchronized",這是多線程編程的老生常談了。所謂原子操作是指不會被線程調(diào)度機制打斷的操作;這種操作一旦開始,就一直運行到結(jié)束,中間不會有任何contextswitch(切換到另一個線程)。
Java多線程同步內(nèi)部如何實現(xiàn)的
提示
請帶著這些問題繼續(xù)后文,會很大程度上幫助你更好的理解相關(guān)知識點。@pdai
為什么要有線程池?Java是實現(xiàn)和管理線程池有哪些方式?請簡單舉例如何使用。為什么很多公司不允許使用Executors去創(chuàng)建線程池?那么推薦怎么使用呢?ThreadPoolExecutor有哪些核心的配置參數(shù)?請簡要說明ThreadPoolExecutor可以創(chuàng)建哪是哪三種線程池呢?當隊列滿了并且worker的數(shù)量達到maxSize的時候,會怎么樣?說說ThreadPoolExecutor有哪些RejectedExecutionHandler策略?默認是什么策略?簡要說下線程池的任務(wù)執(zhí)行機制?execute–>addWorker–>runworker(getTask)線程池中任務(wù)是如何提交的?線程池中任務(wù)是如何關(guān)閉的?在配置線程池的時候需要考慮哪些配置因素?如何監(jiān)控線程池的狀態(tài)?為什么要有線程池線程池能夠?qū)€程進行統(tǒng)一分配,調(diào)優(yōu)和監(jiān)控:
降低資源消耗(線程無限制地創(chuàng)建,然后使用完畢后銷毀)提高響應(yīng)速度(無須創(chuàng)建線程)提高線程的可管理性ThreadPoolExecutor例子Java是如何實現(xiàn)和管理線程池的?
從JDK5開始,把工作單元與執(zhí)行機制分離開來,工作單元包括Runnable和Callable,而執(zhí)行機制由Executor框架提供。
WorkerThreadSimpleThreadPool
程序中我們創(chuàng)建了固定大小為五個工作線程的線程池。然后分配給線程池十個工作,因為線程池大小為五,它將啟動五個工作線程先處理五個工作,其他的工作則處于等待狀態(tài),一旦有工作完成,空閑下來工作線程就會撿取等待隊列里的其他工作進行執(zhí)行。
這里是以上程序的輸出。
輸出表明線程池中至始至終只有五個名為"pool-1-thread-1"到"pool-1-thread-5"的五個線程,這五個線程不隨著工作的完成而消亡,會一直存在,并負責執(zhí)行分配給線程池的任務(wù),直到線程池消亡。
Executors類提供了使用了ThreadPoolExecutor的簡單的ExecutorService實現(xiàn),但是ThreadPoolExecutor提供的功能遠不止于此。我們可以在創(chuàng)建ThreadPoolExecutor實例時指定活動線程的數(shù)量,我們也可以限制線程池的大小并且創(chuàng)建我們自己的RejectedExecutionHandler實現(xiàn)來處理不能適應(yīng)工作隊列的工作。
這里是我們自定義的RejectedExecutionHandler接口的實現(xiàn)。
RejectedExecutionHandlerImpl.javaThreadPoolExecutor提供了一些方法,我們可以使用這些方法來查詢executor的當前狀態(tài),線程池大小,活動線程數(shù)量以及任務(wù)數(shù)量。因此我是用來一個監(jiān)控線程在特定的時間間隔內(nèi)打印executor信息。
MyMonitorThread.java這里是使用ThreadPoolExecutor的線程池實現(xiàn)例子。
WorkerPool.java注意在初始化ThreadPoolExecutor時,我們保持初始池大小為2,最大池大小為4而工作隊列大小為2。因此如果已經(jīng)有四個正在執(zhí)行的任務(wù)而此時分配來更多任務(wù)的話,工作隊列將僅僅保留他們(新任務(wù))中的兩個,其他的將會被RejectedExecutionHandlerImpl處理。
上面程序的輸出可以證實以上觀點。
注意executor的活動任務(wù)、完成任務(wù)以及所有完成任務(wù),這些數(shù)量上的變化。我們可以調(diào)用shutdown()方法來結(jié)束所有提交的任務(wù)并終止線程池。
ThreadPoolExecutor使用詳解其實java線程池的實現(xiàn)原理很簡單,說白了就是一個線程集合workerSet和一個阻塞隊列workQueue。當用戶向線程池提交一個任務(wù)(也就是線程)時,線程池會先將任務(wù)放入workQueue中。workerSet中的線程會不斷的從workQueue中獲取線程然后執(zhí)行。當workQueue中沒有任務(wù)的時候,worker就會阻塞,直到隊列中有任務(wù)了就取出來繼續(xù)執(zhí)行。
Execute原理當一個任務(wù)提交至線程池之后:
線程池首先當前運行的線程數(shù)量是否少于corePoolSize。如果是,則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果都在執(zhí)行任務(wù),則進入2.判斷BlockingQueue是否已經(jīng)滿了,倘若還沒有滿,則將線程放入BlockingQueue。否則進入3.如果創(chuàng)建一個新的工作線程將使當前運行的線程數(shù)量超過maximumPoolSize,則交給RejectedExecutionHandler來處理任務(wù)。當ThreadPoolExecutor創(chuàng)建新線程時,通過CAS來更新線程池的狀態(tài)ctl.
參數(shù)corePoolSize線程池中的核心線程數(shù),當提交一個任務(wù)時,線程池創(chuàng)建一個新線程執(zhí)行任務(wù),直到當前線程數(shù)等于corePoolSize,即使有其他空閑線程能夠執(zhí)行新來的任務(wù),也會繼續(xù)創(chuàng)建線程;如果當前線程數(shù)為corePoolSize,繼續(xù)提交的任務(wù)被保存到阻塞隊列中,等待被執(zhí)行;如果執(zhí)行了線程池的prestartAllCoreThreads()方法,線程池會提前創(chuàng)建并啟動所有核心線程。workQueue用來保存等待被執(zhí)行的任務(wù)的阻塞隊列.在JDK中提供了如下阻塞隊列:具體可以參考JUC集合:BlockQueue詳解ArrayBlockingQueue:基于數(shù)組結(jié)構(gòu)的有界阻塞隊列,按FIFO排序任務(wù);LinkedBlockingQueue:基于鏈表結(jié)構(gòu)的阻塞隊列,按FIFO排序任務(wù),吞吐量通常要高于ArrayBlockingQueue;SynchronousQueue:一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài),吞吐量通常要高于LinkedBlockingQueue;PriorityBlockingQueue:具有優(yōu)先級的無界阻塞隊列;LinkedBlockingQueue比ArrayBlockingQueue在插入刪除節(jié)點性能方面更優(yōu),但是二者在put(),take()任務(wù)的時均需要加鎖,SynchronousQueue使用無鎖算法,根據(jù)節(jié)點的狀態(tài)判斷執(zhí)行,而不需要用到鎖,其核心是Transfer.transfer().
maximumPoolSize線程池中允許的最大線程數(shù)。如果當前阻塞隊列滿了,且繼續(xù)提交任務(wù),則創(chuàng)建新的線程執(zhí)行任務(wù),前提是當前線程數(shù)小于maximumPoolSize;當阻塞隊列是無界隊列,則maximumPoolSize則不起作用,因為無法提交至核心線程池的線程會一直持續(xù)地放入workQueue.keepAliveTime線程空閑時的存活時間,即當線程沒有任務(wù)執(zhí)行時,該線程繼續(xù)存活的時間;默認情況下,該參數(shù)只在線程數(shù)大于corePoolSize時才有用,超過這個時間的空閑線程將被終止;unitkeepAliveTime的單位threadFactory創(chuàng)建線程的工廠,通過自定義的線程工廠可以給每個新建的線程設(shè)置一個具有識別度的線程名。默認為DefaultThreadFactoryhandler線程池的飽和策略,當阻塞隊列滿了,且沒有空閑的工作線程,如果繼續(xù)提交任務(wù),必須采取一種策略處理該任務(wù),線程池提供了4種策略:AbortPolicy:直接拋出異常,默認策略;CallerRunsPolicy:用調(diào)用者所在的線程來執(zhí)行任務(wù);DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務(wù),并執(zhí)行當前任務(wù);DiscardPolicy:直接丟棄任務(wù);當然也可以根據(jù)應(yīng)用場景實現(xiàn)RejectedExecutionHandler接口,自定義飽和策略,如記錄日志或持久化存儲不能處理的任務(wù)。
三種類型newFixedThreadPool線程池的線程數(shù)量達corePoolSize后,即使線程池沒有可執(zhí)行任務(wù)時,也不會釋放線程。
FixedThreadPool的工作隊列為無界隊列LinkedBlockingQueue(隊列容量為Integer.MAX_VALUE),這會導(dǎo)致以下問題:
線程池里的線程數(shù)量不超過corePoolSize,這導(dǎo)致了maximumPoolSize和keepAliveTime將會是個無用參數(shù)由于使用了無界隊列,所以FixedThreadPool永遠不會拒絕,即飽和策略失效newSingleThreadExecutor初始化的線程池中只有一個線程,如果該線程異常結(jié)束,會重新創(chuàng)建一個新的線程繼續(xù)執(zhí)行任務(wù),唯一的線程可以保證所提交任務(wù)的順序執(zhí)行.
由于使用了無界隊列,所以SingleThreadPool永遠不會拒絕,即飽和策略失效
newCachedThreadPool線程池的線程數(shù)可達到Integer.MAX_VALUE,即2147483647,內(nèi)部使用SynchronousQueue作為阻塞隊列;和newFixedThreadPool創(chuàng)建的線程池不同,newCachedThreadPool在沒有任務(wù)執(zhí)行時,當線程的空閑時間超過keepAliveTime,會自動釋放線程資源,當提交新任務(wù)時,如果沒有空閑線程,則創(chuàng)建新線程執(zhí)行任務(wù),會導(dǎo)致一定的系統(tǒng)開銷;執(zhí)行過程與前兩種稍微不同:
主線程調(diào)用SynchronousQueue的offer()方法放入task,倘若此時線程池中有空閑的線程嘗試讀取SynchronousQueue的task,即調(diào)用了SynchronousQueue的poll(),那么主線程將該task交給空閑線程.否則執(zhí)行(2)當線程池為空或者沒有空閑的線程,則創(chuàng)建新的線程執(zhí)行任務(wù).執(zhí)行完任務(wù)的線程倘若在60s內(nèi)仍空閑,則會被終止.因此長時間空閑的CachedThreadPool不會持有任何線程資源.關(guān)閉線程池遍歷線程池中的所有線程,然后逐個調(diào)用線程的interrupt方法來中斷線程.
關(guān)閉方式-shutdown將線程池里的線程狀態(tài)設(shè)置成SHUTDOWN狀態(tài),然后中斷所有沒有正在執(zhí)行任務(wù)的線程.
關(guān)閉方式-shutdownNow將線程池里的線程狀態(tài)設(shè)置成STOP狀態(tài),然后停止所有正在執(zhí)行或暫停任務(wù)的線程.只要調(diào)用這兩個關(guān)閉方法中的任意一個,isShutDown()返回true.當所有任務(wù)都成功關(guān)閉了,isTerminated()返回true.
ThreadPoolExecutor源碼詳解幾個關(guān)鍵屬性內(nèi)部狀態(tài)其中AtomicInteger變量ctl的功能非常強大:利用低29位表示線程池中線程數(shù),通過高3位表示線程池的運行狀態(tài):
RUNNING:-1<<COUNT_BITS,即高3位為111,該狀態(tài)的線程池會接收新任務(wù),并處理阻塞隊列中的任務(wù);SHUTDOWN:0<<COUNT_BITS,即高3位為000,該狀態(tài)的線程池不會接收新任務(wù),但會處理阻塞隊列中的任務(wù);STOP:1<<COUNT_BITS,即高3位為001,該狀態(tài)的線程不會接收新任務(wù),也不會處理阻塞隊列中的任務(wù),而且會中斷正在運行的任務(wù);TIDYING:2<<COUNT_BITS,即高3位為010,所有的任務(wù)都已經(jīng)終止;TERMINATED:3<<COUNT_BITS,即高3位為011,terminated()方法已經(jīng)執(zhí)行完成任務(wù)的執(zhí)行execute–>addWorker–>runworker(getTask)
線程池的工作線程通過Woker類實現(xiàn),在ReentrantLock鎖的保證下,把Woker實例插入到HashSet后,并啟動Woker中的線程。從Woker類的構(gòu)造方法實現(xiàn)可以發(fā)現(xiàn):線程工廠在創(chuàng)建線程thread時,將Woker實例本身this作為參數(shù)傳入,當執(zhí)行start方法啟動線程thread時,本質(zhì)是執(zhí)行了Worker的runWorker方法。firstTask執(zhí)行完成之后,通過getTask方法從阻塞隊列中獲取等待的任務(wù),如果隊列中沒有任務(wù),getTask方法會被阻塞并掛起,不會占用cpu資源;
execute()方法ThreadPoolExecutor.execute(task)實現(xiàn)了Executor.execute(task)
為什么需要doublecheck線程池的狀態(tài)?在多線程環(huán)境下,線程池的狀態(tài)時刻在變化,而ctl.get()是非原子操作,很有可能剛獲取了線程池狀態(tài)后線程池狀態(tài)就改變了。判斷是否將command加入workque是線程池之前的狀態(tài)。倘若沒有doublecheck,萬一線程池處于非running狀態(tài)(在多線程環(huán)境下很有可能發(fā)生),那么command永遠不會執(zhí)行。
addWorker方法從方法execute的實現(xiàn)可以看出:addWorker主要負責創(chuàng)建新的線程并執(zhí)行任務(wù)線程池創(chuàng)建新線程執(zhí)行任務(wù)時,需要獲取全局鎖:
Worker類的runworker方法繼承了AQS類,可以方便的實現(xiàn)工作線程的中止操作;實現(xiàn)了Runnable接口,可以將自身作為一個任務(wù)在工作線程中執(zhí)行;當前提交的任務(wù)firstTask作為參數(shù)傳入Worker的構(gòu)造方法;一些屬性還有構(gòu)造方法:
runWorker方法是線程池的核心:
線程啟動之后,通過unlock方法釋放鎖,設(shè)置AQS的state為0,表示運行可中斷;Worker執(zhí)行firstTask或從workQueue中獲取任務(wù):進行加鎖操作,保證thread不被其他線程中斷(除非線程池被中斷)檢查線程池狀態(tài),倘若線程池處于中斷狀態(tài),當前線程將中斷。執(zhí)行beforeExecute執(zhí)行任務(wù)的run方法執(zhí)行afterExecute方法解鎖操作通過getTask方法從阻塞隊列中獲取等待的任務(wù),如果隊列中沒有任務(wù),getTask方法會被阻塞并掛起,不會占用cpu資源;
getTask方法
下面來看一下getTask()方法,這里面涉及到keepAliveTime的使用,從這個方法我們可以看出線程池是怎么讓超過corePoolSize的那部分worker銷毀的。
注意這里一段代碼是keepAliveTime起作用的關(guān)鍵:
allowCoreThreadTimeOut為false,線程即使空閑也不會被銷毀;倘若為ture,在keepAliveTime內(nèi)仍空閑則會被銷毀。
如果線程允許空閑等待而不被銷毀timed==false,workQueue.take任務(wù):如果阻塞隊列為空,當前線程會被掛起等待;當隊列中有任務(wù)加入時,線程被喚醒,take方法返回任務(wù),并執(zhí)行;
如果線程不允許無休止空閑timed==true,workQueue.poll任務(wù):如果在keepAliveTime時間內(nèi),阻塞隊列還是沒有任務(wù),則返回null;
任務(wù)的提交submit任務(wù),等待線程池execute執(zhí)行FutureTask類的get方法時,會把主線程封裝成WaitNode節(jié)點并保存在waiters鏈表中,并阻塞等待運行結(jié)果;FutureTask任務(wù)執(zhí)行完成后,通過UNSAFE設(shè)置waiters相應(yīng)的waitNode為null,并通過LockSupport類unpark方法喚醒主線程;在實際業(yè)務(wù)場景中,F(xiàn)uture和Callable基本是成對出現(xiàn)的,Callable負責產(chǎn)生結(jié)果,F(xiàn)uture負責獲取結(jié)果。
Callable接口類似于Runnable,只是Runnable沒有返回值。Callable任務(wù)除了返回正常結(jié)果之外,如果發(fā)生異常,該異常也會被返回,即Future可以拿到異步執(zhí)行任務(wù)各種結(jié)果;Future.get方法會導(dǎo)致主線程阻塞,直到Callable任務(wù)執(zhí)行完成;submit方法AbstractExecutorService.submit()實現(xiàn)了ExecutorService.submit()可以獲取執(zhí)行完的返回值,而ThreadPoolExecutor是AbstractExecutorService.submit()的子類,所以submit方法也是ThreadPoolExecutor`的方法。
通過submit方法提交的Callable任務(wù)會被封裝成了一個FutureTask對象。通過Executor.execute方法提交FutureTask到線程池中等待被執(zhí)行,最終執(zhí)行的是FutureTask的run方法;
FutureTask對象publicclassFutureTask<V>implementsRunnableFuture<V>可以將FutureTask提交至線程池中等待被執(zhí)行(通過FutureTask的run方法來執(zhí)行)
內(nèi)部狀態(tài)內(nèi)部狀態(tài)的修改通過sun.misc.Unsafe修改
get方法內(nèi)部通過awaitDone方法對主線程進行阻塞,具體實現(xiàn)如下:
如果主線程被中斷,則拋出中斷異常;
判斷FutureTask當前的state,如果大于COMPLETING,說明任務(wù)已經(jīng)執(zhí)行完成,則直接返回;如果當前state等于COMPLETING,說明任務(wù)已經(jīng)執(zhí)行完,這時主線程只需通過yield方法讓出cpu資源,等待state變成NORMAL;通過WaitNode類封裝當前線程,并通過UNSAFE添加到waiters鏈表;最終通過LockSupport的park或parkNanos掛起線程;run方法
FutureTask.run方法是在線程池中被執(zhí)行的,而非主線程
通過執(zhí)行Callable任務(wù)的call方法;如果call執(zhí)行成功,則通過set方法保存結(jié)果;如果call執(zhí)行有異常,則通過setException保存異常;任務(wù)的關(guān)閉shutdown方法會將線程池的狀態(tài)設(shè)置為SHUTDOWN,線程池進入這個狀態(tài)后,就拒絕再接受任務(wù),然后會將剩余的任務(wù)全部執(zhí)行完
shutdownNow做的比較絕,它先將線程池狀態(tài)設(shè)置為STOP,然后拒絕所有提交的任務(wù)。最后中斷左右正在運行中的worker,然后清空任務(wù)隊列。
更深入理解
為什么線程池不允許使用Executors去創(chuàng)建?推薦方式是什么?線程池不允許使用Executors去創(chuàng)建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風險。說明:Executors各個方法的弊端:
newFixedThreadPool和newSingleThreadExecutor:??主要問題是堆積的請求處理隊列可能會耗費非常大的內(nèi)存,甚至OOM。newCachedThreadPool和newScheduledThreadPool:??主要問題是線程數(shù)最大數(shù)是Integer.MAX_VALUE,可能會創(chuàng)建數(shù)量非常多的線程,甚至OOM。推薦方式1首先引入:commons-lang3包
推薦方式2首先引入:com.google.guava包
推薦方式3spring配置線程池方式:自定義線程工廠bean需要實現(xiàn)ThreadFactory,可參考該接口的其它默認實現(xiàn)類,使用方式直接注入bean調(diào)用execute(Runnabletask)方法即可
配置線程池需要考慮因素
從任務(wù)的優(yōu)先級,任務(wù)的執(zhí)行時間長短,任務(wù)的性質(zhì)(CPU密集/IO密集),任務(wù)的依賴關(guān)系這四個角度來分析。并且近可能地使用有界的工作隊列。
性質(zhì)不同的任務(wù)可用使用不同規(guī)模的線程池分開處理:
CPU密集型:盡可能少的線程,Ncpu+1IO密集型:盡可能多的線程,Ncpu*2,比如數(shù)據(jù)庫連接池混合型:CPU密集型的任務(wù)與IO密集型任務(wù)的執(zhí)行時間差別較小,拆分為兩個線程池;否則沒有必要拆分。監(jiān)控線程池的狀態(tài)可以使用ThreadPoolExecutor以下方法:
getTaskCount()Returnstheapproximatetotalnumberoftasksthathaveeverbeenscheduledforexecution.getCompletedTaskCount()Returnstheapproximatetotalnumberoftasksthathavecompletedexecution.返回結(jié)果少于getTaskCount()。getLargestPoolSize()Returnsthelargestnumberofthreadsthathaveeversimultaneouslybeeninthepool.返回結(jié)果小于等于maximumPoolSizegetPoolSize()Returnsthecurrentnumberofthreadsinthepool.getActiveCount()Returnstheapproximatenumberofthreadsthatareactivelyexecutingtasks.參考文章《Java并發(fā)編程藝術(shù)》https://www.jianshu.com/p/87bff5cc8d8chttps://blog.csdn.net/programmer_at/article/details/79799267https://blog.csdn.net/u013332124/article/details/79587436https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice由于問答代碼塊插入受限,部分代碼未完全展示,若有需要可閱讀原文:戳我閱讀原文
c++線程普通變量怎么添加線程
在C++中添加線程操作可以通過創(chuàng)建一個線程函數(shù)來實現(xiàn),線程函數(shù)內(nèi)部可以訪問普通變量并進行相關(guān)的操作。通過使用std::thread庫函數(shù)或者pthread_create函數(shù)可以創(chuàng)建線程。創(chuàng)建線程時需要傳入線程函數(shù),同時也可以傳入?yún)?shù)以便線程函數(shù)獲取相關(guān)的輸入數(shù)據(jù)。
在線程函數(shù)中可以使用普通變量來進行計算、修改或者讀取操作,并使用互斥鎖進行線程安全的控制。
線程結(jié)束后需要進行資源清理,例如調(diào)用pthread_join函數(shù)等待線程結(jié)束。
C++如何使用thread類多線程編程
大C++程序員可享受原生的多線程機制!淺析C++11多線程內(nèi)存模型
前言在C++11標準中,一個重大的更新就是引入了C++多線程內(nèi)存模型。本文的主要目的在于介紹C++多線程內(nèi)存模型涉及到的一些原理和概念,以幫助大家理解C++多線程內(nèi)存模型的作用和意義。(更多C/C++學習資料,請私信我“代碼”,即可獲取.)
順序一致性模型(SEQUENTIALCONSISTENCY)在介紹C++多線程模型之前,讓我們先介紹一下最基本的順序一致性模型。對多線程程序來說,最直觀,最容易被理解的執(zhí)行方式就是順序一致性模型。順序一致性的提出者Lamport給出的定義是:
“…theresultofanyexecutionisthesameasiftheoperationsofalltheprocessorswereexecutedinsomesequentialorder,andtheoperationsofeachindividualprocessorappearinthissequenceintheorderspecifiedbyitsprogram.”
從這個定義中我們可以看出,順序一致性主要約定了兩件事情:
(1)從單個線程的角度來看,每個線程內(nèi)部的指令都是按照程序規(guī)定的順序(programorder)來執(zhí)行的;
(2)從整個多線程程序的角度來看,整個多線程程序的執(zhí)行順序是按照某種交錯順序來執(zhí)行的,且是全局一致的;
下面我們通過一個例子來理解順序一致性。假設(shè)我們有兩個線程(線程1和線程2),它們分別運行在兩個CPU核上,有兩個初始值為0的全局共享變量x和y,兩個線程分別執(zhí)行下面兩條指令:
初始條件:x=y=0;
因為多線程程序交錯執(zhí)行的順序是不確定的,所以該程序可能有如下幾種執(zhí)行順序:順序一致性模型的第一個約定要求每個線程內(nèi)部的語句都是按照程序規(guī)定的順序執(zhí)行,例如,線程1里面的兩條語句在該線程中一定是x=1先執(zhí)行,r1=y后執(zhí)行。順序一致性的第二個約定要求多線程程序按照某種順序執(zhí)行,且所有線程看見的整體執(zhí)行順序必須一致,即該多線程程序可以按照順序1、順序2或者順序3(以及其他可能的執(zhí)行順序)執(zhí)行,且線程1和線程2所觀察到的整個程序的執(zhí)行順序是一致的(例如,如果線程1“看見”整個程序的執(zhí)行順序是順序1,那么線程2“看見”的整個程序的執(zhí)行順序也必須是順序1,而不能是順序2或者順序3)。依照順序一致性模型,雖然這個程序還可能按其他的交錯順序執(zhí)行,但是r1和r2的值卻只可能出現(xiàn)上面三種結(jié)果,而不可能出現(xiàn)r1和r2同時為0的情況。
然而,盡管順序一致性模型非常易于理解,但是它卻對CPU和編譯器的性能優(yōu)化做出了很大的限制,所以常見的多核CPU和編譯器大都沒有實現(xiàn)順序一致性模型。例如,編譯器可能會為了隱藏一部分讀操作的延遲而做如下優(yōu)化,把線程1中對y的讀操作(即r1=y)調(diào)換到x=1之前執(zhí)行:
初始條件:x=y=0;
在這種情況下,該程序如果按下面的順序執(zhí)行就可能就會出現(xiàn)r1和r2都為0這樣的違反順序一致性的結(jié)果:那么為什么編譯器會做這樣的亂序優(yōu)化呢?因為讀一個在內(nèi)存中而不是在cache中的共享變量需要較長的時鐘周期,所以編譯器就“自作聰明”的讓讀操作先執(zhí)行,從而隱藏掉一些指令執(zhí)行的延遲,從而提高程序的性能。實際上,這種優(yōu)化是串行時代非常普遍的,因為它對單線程程序的語義是沒有影響的。但是在進入多核時代后,編譯器缺少語言級的內(nèi)存模型的約束,導(dǎo)致其可能做出違法順序一致性規(guī)定的多線程語義的錯誤優(yōu)化。同樣的,多核CPU中的寫緩沖區(qū)(storebuffer)也可能實施亂序優(yōu)化:它會把要寫入內(nèi)存的值先在緩沖區(qū)中緩存起來,以便讓該寫操作之后的指令先執(zhí)行,進而出現(xiàn)違反順序一致性的執(zhí)行順序。
因為現(xiàn)有的多核CPU和編譯器都沒有遵守順序一致模型,而且C/C++的現(xiàn)有標準中都沒有把多線程考慮在內(nèi),所以給編寫多線程程序帶來了一些問題。例如,為了正確地用C++實現(xiàn)Double-CheckedLocking,我們需要使用非常底層的內(nèi)存柵欄(MemoryBarrier)指令來顯式地規(guī)定代碼的內(nèi)存順序性(memoryordering)[5]。然而,這種方案依賴于具體的硬件,因此可移植性很差;而且它過于底層,不方便使用。
C++多線程內(nèi)存模型為了更容易的進行多線程編程,程序員希望程序能按照順序一致性模型執(zhí)行;但是順序一致性對性能的損失太大了,CPU和編譯器為了提高性能就必須要做優(yōu)化。為了在易編程性和性能間取得一個平衡,一個新的模型出爐了:sequentialconsistencyfordataracefreeprograms,它就是即將到來的C++1x標準中多線程內(nèi)存模型的基礎(chǔ)。對C++程序員來說,隨著C++1x標準的到來,我們終于可以依賴高級語言內(nèi)建的多線程內(nèi)存模型來編寫正確的、高性能的多線程程序。
C++內(nèi)存模型可以被看作是C++程序和計算機系統(tǒng)(包括編譯器,多核CPU等可能對程序進行亂序優(yōu)化的軟硬件)之間的契約,它規(guī)定了多個線程訪問同一個內(nèi)存地址時的語義,以及某個線程對內(nèi)存地址的更新何時能被其它線程看見。這個模型約定:沒有數(shù)據(jù)競跑的程序是遵循順序一致性的。該模型的核心思想就是由程序員用同步原語(例如鎖或者C++1x中新引入的atomic類型的共享變量)來保證你程序是沒有數(shù)據(jù)競跑的,這樣CPU和編譯器就會保證程序是按程序員所想的那樣執(zhí)行的(即順序一致性)。換句話說,程序員只需要恰當?shù)厥褂镁哂型秸Z義的指令來標記那些真正需要同步的變量和操作,就相當于告訴CPU和編譯器不要對這些標記好的同步操作和變量做違反順序一致性的優(yōu)化,而其它未被標記的地方可以做原有的優(yōu)化。編譯器和CPU的大部分優(yōu)化手段都可以繼續(xù)實施,只是在同步原語處需要對優(yōu)化做出相應(yīng)的限制;而且程序員只需要保證正確地使用同步原語即可,因為它們最終表現(xiàn)出來的執(zhí)行效果與順序一致性模型一致。由此,C++多線程內(nèi)存模型幫助我們在易編程性和性能之間取得了一個平衡。
在C++11標準之前,C++是在建立在單線程語義上的。為了進行多線程編程,C++程序員通過使用諸如Pthreads,WindowsThread等C++語言標準之外的線程庫來完成代碼設(shè)計。以Pthreads為例,它提供了類似pthread_mutex_lock這樣的函數(shù)來保證對共享變量的互斥訪問,以防止數(shù)據(jù)競跑。人們不禁會問,Pthreads這樣的線程庫我用的好好的,干嘛需要C++引入的多線程,這不是多此一舉么?其實,以線程庫的形式進行多線程編程在絕大多數(shù)應(yīng)用場景下都是沒有問題的。然而,線程庫的解決方案也有其先天缺陷。第一,如果沒有在編程語言中定義內(nèi)存模型的話,我們就不能清楚的定義到底什么樣的編譯器/CPU優(yōu)化是合法的,而程序員也不能確定程序到底會怎么樣被優(yōu)化執(zhí)行。例如,Pthreads標準中并未對什么是數(shù)據(jù)競跑(DataRace)做出精確定義,因此C++編譯器可能會進行一些錯誤優(yōu)化從而導(dǎo)致數(shù)據(jù)競跑[3]。第二,絕大多數(shù)情況下線程庫能正確的完成任務(wù),而在極少數(shù)對性能有更高要求的情況下(尤其是需要利用底層的硬件特性來實現(xiàn)高性能LockFree算法時)需要更精確的內(nèi)存模型以規(guī)定好程序的行為。簡而言之,把內(nèi)存模型集成到編程語言中去是比線程庫更好的選擇。
C++11中引入的ATOMIC類型C++作為一種高性能的系統(tǒng)語言,其設(shè)計目標之一就在于提供足夠底層的操作,以滿足對高性能的需求。在這個前提之下,C++11除了提供傳統(tǒng)的鎖、條件變量等同步機制之外,還引入了新的atomic類型。相對于傳統(tǒng)的mutex鎖來說,atomic類型更底層,具備更好的性能,因此能用于實現(xiàn)諸如LockFree等高性能并行算法。有了atomic類型,C++程序員就不需要像原來一樣使用匯編代碼來實現(xiàn)高性能的多線程程序了。而且,把atomic類型集成到C++語言中之后,程序員就可以更容易地實現(xiàn)可移植的多線程程序,而不用再依賴那些平臺相關(guān)的匯編語句或者線程庫。對常見的數(shù)據(jù)類型,C++11都提供了與之相對應(yīng)的atomic類型。以bool類型舉例,與之相對應(yīng)的atomic_bool類型具備兩個新屬性:原子性與順序性。顧名思義,原子性的意思是說atomic_bool的操作都是不可分割的,原子的;而順序性則指定了對該變量的操作何時對其他線程可見。在C++11中,為了滿足對性能的追求,atomic類型提供了三種順序?qū)傩裕簊equentialconsistencyordering(即順序一致性),acquirereleaseordering以及relaxedordering。因為sequentialconsistency是最易理解的模型,所以默認情況下所有atomic類型的操作都會使sequentialconsistency順序。當然,順序一致性的性能相對來說比較差,所以程序員還可以使用對順序性要求稍弱一些的acquirereleaseordering與最弱的relaxedordering。
在下面這個例子中,atomic_bool類型的變量data_ready就被用來實現(xiàn)兩個線程間的同步操作。需要注意的是,對data_ready的寫操作仍然可以通過直接使用賦值操作符(即“=”)來進行,但是對其的讀操作就必須調(diào)用load()函數(shù)來進行。在默認的情況下,所有atomic類型變量的順序性都是順序一致性(即sequentialconsistency)。在這個例子中,因為data_ready的順序性被規(guī)定為順序一致性,所以線程1中對data_ready的寫操作會與線程2中對data_ready的讀操作構(gòu)建起synchronize-with的同步關(guān)系,即#2->#3。又因為writer_thread()中的代碼順序規(guī)定了#1在#2之前發(fā)生,即#1->#2;而且reader_thread中的代碼順序規(guī)定了#3->#4,所以就有了#1->#2->#3->#4這樣的順序關(guān)系,從而可以保證在#4中讀取data的值時,#1已經(jīng)執(zhí)行完畢,即#4一定能讀到#1寫入的值(10)。
相信很多朋友會納悶,這樣的執(zhí)行順序不是顯然的么?其實不然。如果我們把data_ready的順序性制定為relaxedordering的話,編譯器和CPU就可以自由地做違反順序一致性的亂序優(yōu)化,從而導(dǎo)致#1不一定在#2之前被執(zhí)行,最終導(dǎo)致#4中讀到的data的值不為10。
簡單的來說,在atomic類型提供的三種順序?qū)傩灾?,acquirereleaseordering對順序性的約束程度介于sequentialconsistency(順序一致性)和relaxedordering之間,因為它不要求全局一致性,但是具有synchronizedwith的關(guān)系。Relaxedordering最弱,因為它對順序性不做任何要求。由此可見,除非非常必要,我們一般不建議使用relaxedordering,因為這不能保證任何順序性。關(guān)于這三種屬性更詳細的信息大家可以參考[1]。
通過上面的例子我們可以看到,C++1x中的多線程內(nèi)存模型為了通過atomic類型提供足夠的靈活性和性能,最大限度地將底層細節(jié)(三種不同的順序?qū)傩裕┍┞督o了程序員。這樣的設(shè)計原則一方面給程序員提供了實現(xiàn)高性能多線程算法的可能,但卻也大大增加了使用上的難度。我個人的建議是,如果常規(guī)的mutex鎖、條件變量、future信號能滿足您的設(shè)計需求,那么您完全不需要使用atomic變量。如果您決定使用atomic變量,請盡量使用默認的順序一致性屬性。
總結(jié)本文對C++11標準中新引入的多線程內(nèi)存模型進行了簡要介紹。C++11多線程內(nèi)存模型的引入使得廣大C++程序員可以享受語言原生支持的多線程機制,并為實現(xiàn)高性能多線程算法提供了足夠豐富的工具(例如atomic類型)。但是,多線程內(nèi)存模型本身的復(fù)雜性,以及一些底層機制(例如不同的順序性屬性)的引入也給使用C++進行多線程編程帶來了不小的復(fù)雜度。如何高效、可靠的利用好這些新引入的多線程機制將會成為一個新的挑戰(zhàn)。
java線程間如何通信
謝邀!
Object的wait方法、notify方法和notifyAll方法可以實現(xiàn)線程間的通訊,wait方法讓當前線程等待,同時釋放持有的鎖,notify方法可以喚醒一個等待的線程,notifyAll方法可以喚醒所有等待的線程,線程間采用競爭的策略獲取執(zhí)行計劃,但是需要注意的是這三個方法需要配合synchronized關(guān)鍵字使用。
希望對你有所幫助!
關(guān)于多線程操作共享資源保證原子到此分享完畢,希望能幫助到您。
本文鏈接:http://xinin56.com/su/443.html