不可變物件需要滿足的條件
(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:線程安全
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判斷和程式碼執行)是原子性的。
四、線程安全-同步容器
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)
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表示的是java.util.concurrent报名的缩写。
ArrayList:线程不安全;
CopyOnWriteArrayList:线程安全;
写操作时复制,当有新元素添加到CopyOnWriteArrayList数组时,先从原有的数组中拷贝一份出来,然后在新的数组中进行写操作,写完之后再将原来的数组指向到新的数组。整个操作都是在锁的保护下进行的。
CopyOnWriteArrayList缺点:
(1)每次写操作都需要复制一份,消耗内存,如果元素特别多,可能导致GC;
(2)不能用于实时读的场景,适合读多写少的场景;
CopyOnWriteArrayList设计思想:
(1)读写分离
(2)最终一致性
(3)使用时另外开辟空间,解决并发冲突
注意:CopyOnWriteArrayList读操作时,都是在原数组上进行的,不需要加锁,写操作时复制,当有新元素添加到CopyOnWriteArrayList数组时,先从原有的集合中拷贝一份出来,然后在新的数组中进行写操作,写完之后再将原来的数组指向到新的数组。整个操作都是在锁的保护下进行的。
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)。
ConcurrentHashMap:线程安全,不允许空值
ConcurrentSkipListMap:是TreeMap的线程安全版本,内部是使用SkipList跳表结构实现
(1)ConcurrentSkipListMap中的Key是有序的,ConcurrentHashMap中的Key是无序的;
(2)ConcurrentSkipListMap支持更高的并发,对数据的存取时间和线程数几乎无关,也就是说,在数据量一定的情况下,并发的线程数越多,ConcurrentSkipListMap越能体现出它的优势。
注意:在非对线程下尽量使用TreeMap,另外,对于并发数相对较低的并行程序,可以使用Collections.synchronizedSortedMap,将TreeMap进行包装;对于高并发程序,使用ConcurrentSkipListMap提供更高的并发度;在多线程高并发环境中,需要对Map的键值对进行排序,尽量使用ConcurrentSkipListMap。
以上是Java線程安全性策略的實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!