【从入门到放弃-Java】并发编程-线程安全
概述
并發(fā)編程,即多條線程在同一時間段內(nèi)“同時”運行。
在多處理器系統(tǒng)已經(jīng)普及的今天,多線程能發(fā)揮出其優(yōu)勢,如:一個8核cpu的服務(wù)器,如果只使用單線程的話,將有7個處理器被閑置,只能發(fā)揮出服務(wù)器八分之一的能力(忽略其它資源占用情況)。
同時,使用多線程,可以簡化我們對復(fù)雜任務(wù)的處理邏輯,降低業(yè)務(wù)模型的復(fù)雜程度。
因此并發(fā)編程對于提高服務(wù)器的資源利用率、提高系統(tǒng)吞吐量、降低編碼難度等方面起著至關(guān)重要的作用。
以上是并發(fā)編程的優(yōu)點,但是它同樣引入了一個很重要的問題:線程安全。
什么是線程安全問題
線程在并發(fā)執(zhí)行時,因為cpu的調(diào)度等原因,線程會交替執(zhí)行。如下圖例子所示
public class SelfIncremental {private static int count;public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i< 10000; i++) {count++;System.out.println(count);}});Thread thread2 = new Thread(() -> {for (int i = 0; i< 10000; i++) {count++;System.out.println(count);}});thread1.start();thread2.start();} }執(zhí)行完畢后count的值并不是每次都能等于20000,會出現(xiàn)小于20000的情況,原因是thread1和thread2可能會交替執(zhí)行。
如圖所示:
- t1時刻: thread1 讀取到count=100
- t2時刻: thread2 讀取到count=100
- t3時刻: thread1 對count+1
- t4時刻: thread2 對count+1
- t5時刻: thread1 將101寫入count
- t5時刻: thread2 將101寫入count
因為count++ 不是一個原子操作,實際上會執(zhí)行三步:
- 1、獲取count的值
- 2、將count加1
- 3、將計算結(jié)果寫入count
因此在并發(fā)執(zhí)行時,兩個線程同時讀,可能會讀取到相同的值,對相同的值加一,導(dǎo)致結(jié)果不符合預(yù)期,這種情況就是線程不安全。
線程安全:當多個線程訪問某個類時,不管運行時環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,并且調(diào)用時不需要采用額外的同步操作,這個類都能表現(xiàn)出正確的行為,那么就稱這個類是線程安全的。
引發(fā)原因
引發(fā)線程安全性問題的原因主要是共享內(nèi)存可以被多個線程讀寫,因為讀取和修改時機存在不確定性,導(dǎo)致有線程讀到了過期數(shù)據(jù),并在臟數(shù)據(jù)的基礎(chǔ)上處理后寫回共享內(nèi)存,產(chǎn)生了錯誤的結(jié)果。
竟態(tài)條件
在并發(fā)編程中,因為不恰當?shù)膱?zhí)行時序而出現(xiàn)不正確的結(jié)果的情況被稱為竟態(tài)條件。
常見的靜態(tài)條件類型:
- 先檢查后執(zhí)行:首先觀察到某個條件為真。根據(jù)這個觀察結(jié)果采用相應(yīng)的動作,但實際上在你觀察到這個結(jié)果和采用相應(yīng)動作之間,觀察的結(jié)果可能發(fā)生改變變得無效,導(dǎo)致后續(xù)的所有操作都變得不可預(yù)期。(比如延遲初始化)
- 讀取-修改-寫入:基于對象之前的狀態(tài)來定義對象狀態(tài)的轉(zhuǎn)換。但在讀取到結(jié)果和修改之間,對象可能已被更改。這樣就會基于錯誤的數(shù)據(jù)修改得出錯誤的結(jié)果并被寫入。(比如遞增操作)
發(fā)布與逸出
發(fā)布:使對象能夠在當前作用域之外的代碼中使用。如將該對象的引用保存到其它代碼可以訪問的地方、在一個非私有的方法中返回該引用,將引用傳遞到其它類的方法中。如:
public static Student student;public void init() { student = new Student; }這里 student對象就被發(fā)布了。
逸出:當不該被發(fā)布的對象被發(fā)布了,就稱為逸出。如
private String name = "xxx";public String getString() {return name; }這里name原為private類型但是卻被getString方法發(fā)布了,就可以被視為逸出。
如何避免
線程封閉
線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,并且只有這個對象能修改。
線程封閉即不共享數(shù)據(jù),僅在單線程內(nèi)訪問數(shù)據(jù),這是實現(xiàn)線程安全最簡單的方式之一。
實現(xiàn)線程封閉可以通過:
- Ad-hoc線程封閉:即維護線程封閉性的職責完全由成熟實現(xiàn)承擔。
- 棧封閉:通過局部變量才能訪問對象,該局部變量被保存在執(zhí)行線程的棧中,其他線程無法訪問。
- ThreadLocal類:將共享的全局變量轉(zhuǎn)換為ThreadLocal對象,當線程終止后,這些值會被垃圾回收。
只讀共享
在沒有額外同步的情況下,共享的對象可以由多個線程并發(fā)訪問,但是任何線程都不能修改。共享的對象包括不可變對象和事實不可變對象。
不可變對象:如果某個對象在被創(chuàng)建后就不能修改,那么這個對象就是不可變對象。不可變對象一定是線程安全的。
線程安全共享
線程安全的對象在其內(nèi)部實現(xiàn)同步,因此多線程可以通過對象的公有接口來進行訪問而不需要自己做同步。
保護對象
被保護的對象只能通過持有特定的鎖來訪問。即通過加鎖機制,確保對象的可見性及原子性。
- 內(nèi)置鎖:即通過synchronized關(guān)鍵字同步代碼塊。線程在進入同步代碼塊之前會自動獲得鎖,并在退出同步代碼塊時自動釋放鎖。內(nèi)置鎖是一種互斥鎖。
- 重入鎖:當線程視圖獲取一個已經(jīng)持有的鎖時,就會給鎖的計數(shù)器加一,釋放鎖時計數(shù)器會減一。當計數(shù)器為0時,釋放鎖
- volatile:訪問volatile變量時,不會加鎖,也不會阻塞線程執(zhí)行。他只確保變量的可見性,是一種比synchronized更輕量級的同步機制。
總結(jié)
本文主要是記錄了學(xué)習(xí)《Java并發(fā)編程實站》前幾章中,并發(fā)編程相關(guān)的一些概念。簡單介紹了線程安全、鎖機制等,接下來 我們會深入JUC源碼,來深刻學(xué)習(xí)并發(fā)編程相關(guān)知識。
備注:本文主要源自對《Java并發(fā)編程實戰(zhàn)》的學(xué)習(xí)筆記。
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的【从入门到放弃-Java】并发编程-线程安全的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SLS机器学习最佳实战:批量时序异常检测
- 下一篇: 手淘促活那些事儿 | 智能投放算法框架助