一文詳解Java線程中的安全策略
一、不可變對象
不可變對象需要滿足的條件
(1)對象創建以後其狀態就不能修改
(2)對象所有域都是final類型
(3)對象是正確創建的(在對象創建期間,this引用沒有溢出)
對於不可變對象,可以參見JDK中的String類
final關鍵字:類、方法、變量
(1)修飾類:該類不能被繼承,String類,基礎類型的包裝類(比如Integer、Long等)都是final類型。final類中的成員變量可以根據需要設置為final類型,但是final類中的所有成員方法,都會被隱式的指定為final方法。
(2)修飾方法:鎖定方法不被繼承類修改;效率。註意:一個類的private方法會被隱式的指定為final方法
(3)修飾變量:基本數據類型變量(數值被初始化後不能再修改)、引用類型變量(初始化之後則不能再指向其他的對象)
在JDK中提供瞭一個Collections類,這個類中提供瞭很多以unmodifiable開頭的方法,如下:
Collections.unmodifiableXXX: Collection、List、Set、Map…
其中Collections.unmodifiableXXX方法中的XXX可以是Collection、List、Set、Map…
此時,將我們自己創建的Collection、List、Set、Map,傳遞到Collections.unmodifiableXXX方法中,就變為不可變的瞭。此時,如果修改Collection、List、Set、Map中的元素就會拋出java.lang.UnsupportedOperationException異常。
在Google的Guava中,包含瞭很多以Immutable開頭的類,如下:
ImmutableXXX,XXX可以是Collection、List、Set、Map…
註意:使用Google的Guava,需要在Maven中添加如下依賴包:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency>
二、線程封閉
(1)Ad-hoc線程封閉:程序控制實現,最糟糕,忽略
(2)堆棧封閉:局部變量,無並發問題
(3)ThreadLocal線程封閉:特別好的封閉方法
三、線程不安全類與寫法
1. StringBuilder -> StringBuffer
StringBuilder:線程不安全;
StringBuffer:線程不安全;
字符串拼接涉及到多線程操作時,使用StringBuffer實現
在一個具體的方法中,定義一個字符串拼接對象,此時可以使用StringBuilder實現。因為在一個方法內部定義局部變量進行使用時,屬於堆棧封閉,隻有一個線程會使用變量,不涉及多線程對變量的操作,使用StringBuilder即可。
2. SimpleDateFormat -> JodaTime
SimpleDateFormat:線程不安全,可以將其對象的實例化放入到具體的時間格式化方法中,實現線程安全
JodaTime:線程安全
SimpleDateFormat線程不安全的代碼示例如下:
package io.binghe.concurrency.example.commonunsafe; import lombok.extern.slf4j.Slf4j; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j public class DateFormatExample { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); //請求總數 public static int clientTotal = 5000; //同時並發執行的線程數 public static int threadTotal = 200; public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for(int i = 0; i < clientTotal; i++){ executorService.execute(() -> { try{ semaphore.acquire(); update(); semaphore.release(); }catch (Exception e){ log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); } public static void update(){ try { simpleDateFormat.parse("20191024"); } catch (ParseException e) { log.error("parse exception", e); } } }
修改成如下代碼即可。
package io.binghe.concurrency.example.commonunsafe; import lombok.extern.slf4j.Slf4j; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j public class DateFormatExample2 { //請求總數 public static int clientTotal = 5000; //同時並發執行的線程數 public static int threadTotal = 200; public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for(int i = 0; i < clientTotal; i++){ executorService.execute(() -> { try{ semaphore.acquire(); update(); semaphore.release(); }catch (Exception e){ log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); } public static void update(){ try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); simpleDateFormat.parse("20191024"); } catch (ParseException e) { log.error("parse exception", e); } } }
對於JodaTime需要在Maven中添加如下依賴包:
<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9</version> </dependency>
示例代碼如下:
package io.binghe.concurrency.example.commonunsafe; import lombok.extern.slf4j.Slf4j; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j public class DateFormatExample3 { //請求總數 public static int clientTotal = 5000; //同時並發執行的線程數 public static int threadTotal = 200; private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd"); public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for(int i = 0; i < clientTotal; i++){ final int count = i; executorService.execute(() -> { try{ semaphore.acquire(); update(count); semaphore.release(); }catch (Exception e){ log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); } public static void update(int i){ log.info("{} - {}", i, DateTime.parse("20191024", dateTimeFormatter)); } }
3. ArrayList、HashSet、HashMap等Collections集合類為線程不安全類
4. 先檢查再執行:if(condition(a)){handle(a);}
註意:這種寫法是線程不安全的!!!!!
兩個線程同時執行這種操作,同時對if條件進行判斷,並且a變量是線程共享的,如果兩個線程均滿足if條件,則兩個線程會同時執行handle(a)語句,此時,handle(a)語句就可能不是線程安全的。
不安全的點在於兩個操作中,即使前面的執行過程是線程安全的,後面的過程也是線程安全的,但是前後執行過程的間隙不是原子性的,因此,也會引發線程不安全的問題。
實際過程中,遇到if(condition(a)){handle(a);}類的處理時,考慮a是否是線程共享的,如果是線程共享的,則需要在整個執行方法上加鎖,或者保證if(condition(a)){handle(a);}的前後兩個操作(if判斷和代碼執行)是原子性的。
四、線程安全-同步容器
1. ArrayList -> Vector, Stack
ArrayList:線程不安全;
Vector:同步操作,但是可能會出現線程不安全的情況,線程不安全的代碼示例如下:
public class VectorExample { private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) throws InterruptedException { while (true){ for(int i = 0; i < 10; i++){ vector.add(i); } Thread thread1 = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < vector.size(); i++){ vector.remove(i); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < vector.size(); i++){ vector.get(i); } } }); thread1.start(); thread2.start(); } } }
Stack:繼承自Vector,先進後出。
2. HashMap -> HashTable(Key, Value都不能為null)
HashMap:線程不安全;
HashTable:線程安全,註意使用HashTable時,Key, Value都不能為null;
3. Collections.synchronizedXXX(List、Set、Map)
註意:在遍歷集合的時候,不要對集合進行更新操作。當需要對集合中的元素進行刪除操作時,可以遍歷集合,先對需要刪除的元素進行標記,集合遍歷結束後,再進行刪除操作。例如,下面的示例代碼:
public class VectorExample3 { //此方法拋出:java.util.ConcurrentModificationException private static void test1(Vector<Integer> v1){ for(Integer i : v1){ if(i == 3){ v1.remove(i); } } } //此方法拋出:java.util.ConcurrentModificationException private static void test2(Vector<Integer> v1){ Iterator<Integer> iterator = v1.iterator(); while (iterator.hasNext()){ Integer i = iterator.next(); if(i == 3){ v1.remove(i); } } } //正常 private static void test3(Vector<Integer> v1){ for(int i = 0; i < v1.size(); i++){ if(i == 3){ v1.remove(i); } } } public static void main(String[] args) throws InterruptedException { Vector<Integer> vector = new Vector<>(); vector.add(1); vector.add(2); vector.add(3); //test1(vector); //test2(vector); test3(vector); } }
五、線程安全-並發容器J.U.C
J.U.C表示的是java.util.concurrent報名的縮寫。
1. ArrayList -> CopyOnWriteArrayList
ArrayList:線程不安全;
CopyOnWriteArrayList:線程安全;
寫操作時復制,當有新元素添加到CopyOnWriteArrayList數組時,先從原有的數組中拷貝一份出來,然後在新的數組中進行寫操作,寫完之後再將原來的數組指向到新的數組。整個操作都是在鎖的保護下進行的。
CopyOnWriteArrayList缺點:
(1)每次寫操作都需要復制一份,消耗內存,如果元素特別多,可能導致GC;
(2)不能用於實時讀的場景,適合讀多寫少的場景;
CopyOnWriteArrayList設計思想:
(1)讀寫分離
(2)最終一致性
(3)使用時另外開辟空間,解決並發沖突
註意:CopyOnWriteArrayList讀操作時,都是在原數組上進行的,不需要加鎖,寫操作時復制,當有新元素添加到CopyOnWriteArrayList數組時,先從原有的集合中拷貝一份出來,然後在新的數組中進行寫操作,寫完之後再將原來的數組指向到新的數組。整個操作都是在鎖的保護下進行的。
2.HashSet、TreeSet -> CopyOnWriteArraySet、ConcurrentSkipListSet
CopyOnWriteArraySet:線程安全的,底層實現使用瞭CopyOnWriteArrayList。
ConcurrentSkipListSet:JDK6新增的類,支持排序。可以在構造時,自定義比較器,基於Map集合。在多線程環境下,ConcurrentSkipListSet中的contains()方法、add()、remove()、retain()等操作,都是線程安全的。但是,批量操作,比如:containsAll()、addAll()、removeAll()、retainAll()等操作,並不保證整體一定是原子操作,隻能保證批量操作中的每次操作是原子性的,因為批量操作中是以循環的形式調用的單步操作,比如removeAll()操作下以循環的方式調用remove()操作。如下代碼所示:
//ConcurrentSkipListSet類型中的removeAll()方法的源碼 public boolean removeAll(Collection<?> c) { // Override AbstractSet version to avoid unnecessary call to size() boolean modified = false; for (Object e : c) if (remove(e)) modified = true; return modified; }
所以,在執行ConcurrentSkipListSet中的批量操作時,需要考慮加鎖問題。
註意:ConcurrentSkipListSet類不允許使用空元素(null)。
3. HashMap、TreeMap -> ConcurrentHashMap、ConcurrentSkipListMap
ConcurrentHashMap:線程安全,不允許空值
ConcurrentSkipListMap:是TreeMap的線程安全版本,內部是使用SkipList跳表結構實現
4.ConcurrentSkipListMap與ConcurrentHashMap對比如下
(1)ConcurrentSkipListMap中的Key是有序的,ConcurrentHashMap中的Key是無序的;
(2)ConcurrentSkipListMap支持更高的並發,對數據的存取時間和線程數幾乎無關,也就是說,在數據量一定的情況下,並發的線程數越多,ConcurrentSkipListMap越能體現出它的優勢。
註意:在非對線程下盡量使用TreeMap,另外,對於並發數相對較低的並行程序,可以使用Collections.synchronizedSortedMap,將TreeMap進行包裝;對於高並發程序,使用ConcurrentSkipListMap提供更高的並發度;在多線程高並發環境中,需要對Map的鍵值對進行排序,盡量使用ConcurrentSkipListMap。
六、安全共享對象的策略-總結
(1)線程限制:一個被線程限制的對象,由線程獨占,並且隻能被占有它的線程修改
(2)共享隻讀:一個共享隻讀的對象,在沒有額外同步的情況下,可以被多個線程並發訪問,但是任何線程都不能修改它。
(3)線程安全對象:一個線程安全的對象或者容器,在內部通過同步機制來保證線程安全,所以其他線程無需額外的同步就可以通過公共接口隨意訪問它
(4)被守護對象:被守護對象隻能通過獲取特定的鎖來訪問
到此這篇關於一文詳解Java線程中的安全策略的文章就介紹到這瞭,更多相關Java線程安全策略內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- java並發包中CountDownLatch和線程池的使用詳解
- Java並發容器相關知識總結
- java多線程CountDownLatch與線程池ThreadPoolExecutor/ExecutorService案例
- Java多線程高並發中解決ArrayList與HashSet和HashMap不安全的方案
- 一文搞懂Java創建線程的五種方法