一呛讲、定義
線程安全指的是多個線程并發(fā)訪問共享資源時禾怠,不會出現(xiàn)數(shù)據(jù)不一致或其他意外情況的情況。
二贝搁、如何實現(xiàn)線程安全
不可變對象
不可變對象是指一旦創(chuàng)建吗氏,其狀態(tài)不能被修改的對象。
因為不可變對象的狀態(tài)不可變雷逆,所以它們可以安全地在多個線程之間共享弦讽。
ThreadLocal
ThreadLocal是一種特殊的變量,它為每個線程提供了一個獨立的副本膀哲。
volatile
只能用于修飾變量往产。
優(yōu)點:變量對于所有線程的可見性、禁止指令重排某宪。
缺點:不能保證原子性仿村、頻繁讀寫volatile變量的開銷大。
同步(synchronized)
通過synchronized 關鍵字給方法或代碼塊加上內(nèi)置鎖來實現(xiàn)線程安全
優(yōu)點:簡單易用兴喂、支持可重用鎖蔼囊。
缺點:性能問題包颁、只能保護代碼塊或方法。
Lock
使用Lock鎖機制可以實現(xiàn)更加靈活的鎖操作压真,比synchronized關鍵字更加高效。
優(yōu)點:可以重復獲取鎖避免死鎖蘑险,性能好滴肿,可擴展(公平鎖非公平鎖、可重入讀寫鎖等)
缺點:代碼復雜
原子操作(CAS)
原子操作是一種無需加鎖的線程安全操作方式佃迄,可以保證多個線程同時訪問同一個變量時泼差,仍能保證數(shù)據(jù)的一致性。Java中通過使用原子操作類中的原子操作方法呵俏,實現(xiàn)線程安全堆缘。
三、使用線程安全類
synchronized
Vector普碎,Stack吼肥,StringBuffer,HashTable麻车,Timer,TimerTask
Java提供了java.util.concurrent.*
CopyOnWriteArrayList:線程安全的ArrayList,將原有的數(shù)組復制一份斤寇,然后在新數(shù)組上進行修改操作棉饶,最后再賦給原的數(shù)組引用。相對Vector更加高效赁咙。
ConcurrentHashMap等
還提供了更多的線程安全類和工具钮莲,
如Semaphore、CountDownLatch彼水、CyclicBarrier崔拥、ReadWriteLock等
Java提供了java.util.concurrent.atomic包
包括AtomicInteger、AtomicLong猿涨、AtomicBoolean在內(nèi)7種握童。
Java提供的工具類Collections.synchronized*
用于將一個非線程安全的集合包裝成一個線程安全的集合。
它通過對集合的操作加上同步鎖(使用synchronized關鍵字)的方式來實現(xiàn)線程安全叛赚。
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
四澡绩、拓展
StringBuilder 和 StringBuffer
特性對比:
- String : 不可變字符序列,效率低俺附,但是復用率高肥卡。
- StringBuffer : 可變字符序列,效率較高事镣,且線程安全步鉴。
- StringBuilder : 可變字符序列,效率最高,但線程不安全氛琢。
使用場景對比:
- String : 適用于字符串很少被修改喊递,且被多個對象引用的情況,比如定義數(shù)據(jù)庫的IP信息阳似,配置信息等骚勘。
- StringBuffer : 適用于存在大量修改字符串的情況,且滿足 多線程條件撮奏。
- StringBuilder : 適用于存在大量修改字符串的情況俏讹,且滿足 單線程條件。
import org.springframework.util.StopWatch;
public class StringTest {
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch("StringTest");
stopWatch.start("String");
String s = "";
for (int i = 0; i < 100000; i++) {
s += i;
}
stopWatch.stop();
stopWatch.start("StringBuilder");
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 500000; i++) {
stringBuilder.append(i);
}
stopWatch.stop();
stopWatch.start("StringBuffer");
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 500000; i++) {
stringBuffer.append(i);
}
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
}
/*
StopWatch 'StringTest': running time (millis) = 2521
-----------------------------------------
ms % Task name
-----------------------------------------
02503 099% String
00007 000% StringBuilder
00011 000% StringBuffer
*/
HashMap畜吊、HashTable泽疆、ConcurrentHashMap區(qū)別
HashMap
線程不安全,可以存儲null鍵和null值
Hashtable
Hashtable是線程安全的,但鎖的是整個Hashtable對象玲献,無論key還是value都不能為null
缺點:
(1) 多個線程訪問同一個Hashtable對象時殉疼,無論做什么操作,都會產(chǎn)生鎖沖突捌年;
(2) 如果某個線程觸發(fā)了擴容機制株依,那么就會由這個線程完成整個擴容過程,如果元素過多延窜,效率則非常低恋腕,其它線程阻塞等待的時間也會更長。
Java官方已經(jīng)不推薦使用Hashtable了
ConcurrentHashMap
ConcurrentHashMap相較于Hashtable做了許多的優(yōu)化逆瑞,核心思路就是降低鎖沖突概率~
1荠藤、鎖粒度的控制ConcurrentHashMap不是鎖整個對象,而是使用多把鎖获高,對每個鏈表(哈希桶)都進行加鎖哈肖,只有當兩個線程同時訪問同一個鏈表時,才會產(chǎn)生鎖競爭念秧,因此極大地降低了鎖沖突的概率淤井。
2、讀操作不加鎖
ConcurrentHashMap只對寫操作進行加鎖摊趾,讀操作沒有加鎖币狠,此時會有三種情況:
(1) 兩個線程同時修改一個哈希桶時才會產(chǎn)生鎖沖突;
(2) 兩個線程同時讀數(shù)據(jù)砾层,不會有鎖沖突漩绵;
(3) 一個線程修改,一個線程讀肛炮,也沒有鎖沖突止吐。
第三種情況可能會有線程不安全問題宝踪,這和我們寫的代碼有關,但是ConcurrentHashMap中的讀操作使用了Volatile碍扔,來保證讀到的數(shù)據(jù)不是修改了一半的數(shù)據(jù)瘩燥。
3、利用了CAS的特性
ConcurrentHashMap充分利用了CAS的特性不同,避免出現(xiàn)重量級鎖的情況颤芬。
比如維護元素個數(shù)(size)時,就是通過CAS來更新~
4套鹅、優(yōu)化擴容操作
Hashtable的擴容機制是,創(chuàng)建一個更大的新數(shù)組汰具,然后由一個線程一次性把舊數(shù)組中的元素搬運到新數(shù)組卓鹿。
而ConcurrentHashMap則是讓新數(shù)組和舊數(shù)組同時存在一段時間,在這期間留荔,后續(xù)每個操作ConcurrentHashMap的線程都會負責搬運舊數(shù)組的一部分元素到新數(shù)組吟孙,直到搬運完舊數(shù)組的最后一個元素時,再把舊數(shù)組刪除~
搬運期間聚蝶,插入元素時直接插入到新數(shù)組中杰妓;查詢元素時,新數(shù)組和舊數(shù)組一起查碘勉。