一、併發容器
ConcurrentHashMap
爲什麼使用ConcurrentHashMap
在多線程環境下,使用HashMap進行put操作會引起死循環,導致CPU利用率接近100%,HashMap在併發執行put操作時,出發rehash時,可能會引起鏈表成環的現象,一旦形成環形數據結構,Entry的next結點永遠不爲空,就會產生死循環獲取Entry。
HshTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。因爲當一個線程訪問HashTable的同步方法,其他的線程也訪問H啊是Table的同步方法時,會進入阻塞或者輪詢狀態。如線程1使用put進行元素添加,線程2不但不能使用put方法添加元素,也不能使用get方法來獲取元素,所以競爭越激烈效率越低。
常見的方法
putIfAbsent(K key,V value)
如果key對應的value不存在,則put進去,返回null。否則不put,返回已存在的value.
boolean remove(Object key , Object value)
如果key對應的值是value,則移除k-v,返回true。否則不移除,返回false
boolean replace(K key, V oldValue, V newValue)
如果key對應的當前值是oldValue,則替換爲newValue,返回true。否則不替換,返回false。
Hash的解釋
散列,任意長度的輸入,通過一種算法,變換成固定長度的輸出。屬於壓縮的映射。Md5,Sha,取餘都是散列算法,ConcurrentHashMap中是wang/jenkins算法
ConcurrentHashMap在1.7下的實現
分段鎖的設計思想。
ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment實際是一種可重入鎖(ReentrantLock),HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組。Segment的結構和HashMap類似,是一種數組和鏈表結構。一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得與它對應的Segment鎖。
ConcurrentHashMap初始化方法是通過initialCapacity、loadFactor和concurrencyLevel(參數concurrencyLevel是用戶估計的併發級別,就是說你覺得最多有多少線程共同修改這個map,根據這個來確定Segment數組的大小concurrencyLevel默認是DEFAULT_CONCURRENCY_LEVEL = 16;)。
ConcurrentHashMap完全允許多個讀操作併發進行,讀操作並不需要加鎖。ConcurrentHashMap實現技術是保證HashEntry幾乎是不可變的。HashEntry代表每個hash鏈中的一個節點,可以看到其中的對象屬性要麼是final的,要麼是volatile的。
ConcurrentHashMap1.8版本實現
改進一:取消segments字段,直接採用transient volatile HashEntry<K,V>[] table保存數據,採用table數組元素作爲鎖,從而實現了對每一行數據進行加鎖,進一步減少併發衝突的概率。
改進二:將原先table數組+單向鏈表的數據結構,變更爲table數組+單向鏈表+紅黑樹的結構。對於個數超過8(默認值)的列表,jdk1.8中採用了紅黑樹的結構,那麼查詢的時間複雜度可以降低到O(logN),可以改進性能。
ConcurrentSkipListMap 和ConcurrentSkipListSet
ConcurrentSkipListMap TreeMap的併發實現
ConcurrentSkipListSet TreeSet的併發實現
瞭解什麼是SkipList(跳錶)?
二分查找和AVL樹查找
二分查找要求元素可以隨機訪問,所以決定了需要把元素存儲在連續內存。這樣查找確實很快,但是插入和刪除元素的時候,爲了保證元素的有序性,就需要大量的移動元素了。
如果需要的是一個能夠進行二分查找,又能快速添加和刪除元素的數據結構,首先就是二叉查找樹,二叉查找樹在最壞情況下可能變成一個鏈表。
於是,就出現了平衡二叉樹,根據平衡算法的不同有AVL樹,B-Tree,B+Tree,紅黑樹等,但是AVL樹實現起來比較複雜,平衡操作較難理解,這時候就可以用SkipList跳躍表結構。
傳統意義的單鏈表是一個線性結構,向有序的鏈表中插入一個節點需要O(n)的時間,查找操作需要O(n)的時間。
如果我們使用上圖所示的跳躍表,就可以減少查找所需時間爲O(n/2),因爲我們可以先通過每個節點的最上面的指針先進行查找,這樣子就能跳過一半的節點。
比如我們想查找19,首先和6比較,大於6之後,在和9進行比較,然後在和17進行比較......最後比較到21的時候,發現21大於19,說明查找的點在17和21之間,從這個過程中,我們可以看出,查找的時候跳過了3、7、12等點,因此查找的複雜度爲O(n/2)。
跳躍表其實也是一種通過“空間來換取時間”的一個算法,通過在每個節點中增加了向前的指針,從而提升查找的效率。
跳躍表又被稱爲概率,或者說是隨機化的數據結構,目前開源軟件 Redis 和 lucence都有用到它。
ConcurrentLinkedQueue 無界非阻塞隊列
可以看作是LinkedList的併發版本
add,offer:添加元素
Peek:get頭元素並不把元素拿走
poll():get頭元素把元素拿走
CopyOnWriteArrayList和CopyOnWriteArraySet
寫的時候進行復制,可以進行併發的讀。
適用讀多寫少的場景:比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜索網站,用戶在這個網站的搜索框中,輸入關鍵字搜索內容,但是某些關鍵字不允許被搜索。這些不能被搜索的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜索時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜索。
弱點:內存佔用高,數據一致性弱
什麼是阻塞隊列
取數據和讀數據不滿足要求時,會對線程進行阻塞
方法 |
拋出異常 |
返回值 |
一直阻塞 |
超時退出 |
插入 |
Add |
offer |
put |
offer |
移除 |
Remove |
poll |
take |
poll |
檢查 |
element |
peek |
沒有 |
沒有 |
常用阻塞隊列
ArrayBlockingQueue: 數組結構組成有界阻塞隊列。
先進先出原則,初始化必須傳大小,take和put時候用的同一把鎖
LinkedBlockingQueue:鏈表結構組成的有界阻塞隊列
先進先出原則,初始化可以不傳大小,put,take鎖分離
PriorityBlockingQueue:支持優先級排序的無界阻塞隊列,
排序,自然順序升序排列,更改順序:類自己實現compareTo()方法,初始化PriorityBlockingQueue指定一個比較器Comparator
DelayQueue: 使用了優先級隊列的無界阻塞隊列
支持延時獲取,隊列裏的元素要實現Delay接口。DelayQueue非常有用,可以將DelayQueue運用在以下應用場景。
緩存系統的設計:可以用DelayQueue保存緩存元素的有效期,使用一個線程循環查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示緩存有效期到了。
還有訂單到期,限時支付等等。
假如對User進行緩存:
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(String name) {
super();
this.name = name;
}
}
CacheBean:
因爲DelayQueue中存放的是Delayed接口,所以CacheBean要實現Delayed接
public class CacheBean<T> implements Delayed {
private String id;
private String name;
private T data;
//數據的到期時間
private Long activeTime;
//要求傳入的activeTime爲毫秒,在構造函數中會自動轉換成納秒
public CacheBean(String id, String name, T data, Long activeTime) {
super();
this.id = id;
this.name = name;
this.data = data;
this.activeTime = TimeUnit.NANOSECONDS.convert(activeTime, TimeUnit.MILLISECONDS)+System.nanoTime();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Long getActiveTime() {
return activeTime;
}
public void setActiveTime(Long activeTime) {
this.activeTime = activeTime;
}
@Override
public int compareTo(Delayed o) {
long d = this.getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return (d == 0) ? 0 : (d >0 ? 1 : -1);
}
//返回還有多少納秒的剩餘時間
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.activeTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
}
定義兩個任務類,分別是向DelayQueue中存數據和取數據。
public class PutUserWork implements Runnable{
private DelayQueue<CacheBean<User>> delayQueue;
private List<CacheBean<User>> list;
public PutUserWork(DelayQueue<CacheBean<User>> delayQueue, List<CacheBean<User>> list) {
super();
this.delayQueue = delayQueue;
this.list = list;
}
@Override
public void run() {
list.forEach(cacheBean->{
delayQueue.put(cacheBean);
System.out.println("放入:"+cacheBean.getData());
});
}
}
public class GetUserWork implements Runnable{
private DelayQueue<CacheBean<User>> delayque;
public GetUserWork(DelayQueue<CacheBean<User>> delayque) {
super();
this.delayque = delayque;
}
@Override
public void run() {
while(true)
{
try {
CacheBean<User> element = delayque.take();
System.out.println("get element:"+element+" id:"+element.getId()+", name:"+element.getName()
+" data:"+element.getName());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
測試類:
public class MainTest {
public static void main(String[] args) throws InterruptedException {
User u1 = new User("張三");
CacheBean<User> c1 = new CacheBean<User>("1", "張三", u1, 5000L);
User u2 = new User("李四");
CacheBean<User> c2 = new CacheBean<User>("2", "李四", u2, 3000L);
List<CacheBean<User>> list = new ArrayList<>();
list.add(c1);
list.add(c2);
DelayQueue<CacheBean<User>> delayQueue = new DelayQueue<>();
new Thread(new PutUserWork(delayQueue,list)).start();
new Thread(new GetUserWork(delayQueue)).start();
CountDownLatch countDownLatch = new CountDownLatch(1);
countDownLatch.await();
}
}
生產者消費者模式
在併發編程中使用生產者和消費者模式能夠解決絕大多數併發問題。該模式通過平衡生產線程和消費線程的工作能力來提高程序整體處理數據的速度。在線程世界裏,生產者就是生產數據的線程,消費者就是消費數據的線程。在多線程開發中,如果生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產數據。同樣的道理,如果消費者的處理能力大於生產者,那麼消費者就必須等待生產者。爲了解決這種生產消費能力不均衡的問題,便有了生產者和消費者模式。生產者和消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通信,而是通過阻塞隊列來進行通信,所以生產者生產完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。
Fork/Join框架
並行執行任務的框架,把大任務拆分成很多的小任務,彙總每個小任務的結果得到大任務的結果。
工作竊取算法
工作竊取(work-stealing)算法是指某個線程從其他隊列裏竊取任務來執行。那麼,爲什麼需要使用工作竊取算法呢?假如我們需要做一個比較大的任務,可以把這個任務分割爲若干互不依賴的子任務,爲了減少線程間的競爭,把這些子任務分別放到不同的隊列裏,併爲每個隊列創建一個單獨的線程來執行隊列裏的任務,線程和隊列一一對應。比如A線程負責處理A隊列裏的任務。但是,有的線程會先把自己隊列裏的任務幹完,而其他線程對應的隊列裏還有任務等待處理。幹完活的線程與其等着,不如去幫其他線程幹活,於是它就去其他線程的隊列裏竊取一個任務來執行。而在這時它們會訪問同一個隊列,所以爲了減少竊取任務線程和被竊取任務線程之間的競爭,通常會使用雙端隊列,被竊取任務線程永遠從雙端隊列的頭部拿任務執行,而竊取任務的線程永遠從雙端隊列的尾部拿任務執行。
Fork/Join框架的使用
Fork/Join使用兩個類來完成以上兩件事情。
①ForkJoinTask:我們要使用ForkJoin框架,必須首先創建一個ForkJoin任務。它提供在任務
中執行fork()和join()操作的機制。通常情況下,我們不需要直接繼承ForkJoinTask類,只需要繼承它的子類,Fork/Join框架提供了以下兩個子類。
·RecursiveAction:用於沒有返回結果的任務。
·RecursiveTask:用於有返回結果的任務。
②ForkJoinPool:ForkJoinTask需要通過ForkJoinPool來執行。
Fork/Join有同步和異步兩種方式。
例子:計算硬盤中.txt文件的個數。。。。
public class MyRunnable2 {
static class CountWork extends RecursiveTask<Integer>
{
private String filePath;
public CountWork(String filePath) {
super();
this.filePath = filePath;
}
@Override
protected Integer compute() {
File file = new File(filePath);
int count = 0;
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files == null || files.length == 0)
return 0;
List<CountWork> taskList = new ArrayList<CountWork>();
for (File f : files) {
if (f.isDirectory()) {
String lastName = f.getName();
String newFilePath = filePath + File.separator + lastName;
// count+= count(newFilePath);
CountWork countWork = new CountWork(newFilePath);
taskList.add(countWork);
// invokeAll(countWork);
// count += countWork.join();
} else {
try {
Thread.currentThread().sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
count++;
}
}
if (!taskList.isEmpty()) {
//方式1
for (CountWork mtask : invokeAll(taskList)) {
count += mtask.join();
}
//方式2,
// for (CountWork mtask : taskList) {
// invokeAll(mtask);
//
// //Returns the result of the computation when it {@link #isDone is 一直等待到計算完成才返回結果
// count += mtask.join();
// }
}
}else {
count=1;
}
//System.out.println(Thread.currentThread().getName());
return count;
}
}
//監控Fork/Join池相關方法
private static void showLog(ForkJoinPool pool) {
System.out.printf("**********************\n");
System.out.printf("線程池的worker線程們的數量:%d\n",
pool.getPoolSize());
System.out.printf("當前執行任務的線程的數量:%d\n",
pool.getActiveThreadCount());
System.out.printf("沒有被阻塞的正在工作的線程:%d\n",
pool.getRunningThreadCount());
System.out.printf("已經提交給池還沒有開始執行的任務數:%d\n",
pool.getQueuedSubmissionCount());
System.out.printf("已經提交給池已經開始執行的任務數:%d\n",
pool.getQueuedTaskCount());
System.out.printf("線程偷取任務數:%d\n",
pool.getStealCount());
System.out.printf("池是否已經終止 :%s\n",
pool.isTerminated());
System.out.printf("**********************\n");
}
public static void main(String[] args) throws InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool(15);
File [] roots = File.listRoots();
Long start = System.currentTimeMillis();
//File e=new File("D:\\");
for(File e:roots)
{
CountWork countWork = new CountWork(e.getAbsolutePath());
forkJoinPool.invoke(countWork);
// Thread.sleep(1000);
// forkJoinPool.execute(countWork);
// showLog(forkJoinPool);
System.out.println(countWork.join());
}
Long end = System.currentTimeMillis();
System.out.println("用時:"+(end-start)/1000);
}
}
二、CountDownLatch、CyclicBarrier、Semaphore(控制線程併發數)、Exchanger
CountDownLatch
允許一個或多個線程等待其他線程完成操作。CountDownLatch的構造函數接收一個int類型的參數作爲計數器,如果你想等待N個點完成,這裏就傳入N。當我們調用CountDownLatch的countDown方法時,N就會減1,CountDownLatch的await方法會阻塞當前線程,直到N變成零。由於countDown方法可以用在任何地方,所以這裏說的N個點,可以是N個線程,也可以是1個線程裏的N個執行步驟。用在多個線程時,只需要把這個CountDownLatch的引用傳遞到線程裏即可。
CyclicBarrier
CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續運行。CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞。CyclicBarrier還提供一個更高級的構造函數CyclicBarrier(int parties,Runnable barrierAction),用於在線程到達屏障時,優先執行barrierAction,方便處理更復雜的業務場景。CyclicBarrier可以用於多線程計算數據,最後合併計算結果的場景。
CyclicBarrier和CountDownLatch的區別
CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,CountDownLatch.await一般阻塞主線程,所有的工作線程執行countDown,而CyclicBarrierton通過工作線程調用await從而阻塞工作線程,直到所有工作線程達到屏障。
Semaphore(控制線程併發數)
Semaphore(信號量)是用來控制同時訪問特定資源的線程數量,它通過協調各個線程,以保證合理的使用公共資源。應用場景Semaphore可以用於做流量控制,特別是公用資源有限的應用場景,比如數據庫連接。假如有一個需求,要讀取幾萬個文件的數據,因爲都是IO密集型任務,我們可以啓動幾十個線程併發地讀取,但是如果讀到內存後,還需要存儲到數據庫中,而數據庫的連接數只有10個,這時我們必須控制只有10個線程同時獲取數據庫連接保存數據,否則會報錯無法獲取數據庫連接。這個時候,就可以使用Semaphore來做流量控制。。Semaphore的構造方法Semaphore(int permits)接受一個整型的數字,表示可用的許可證數量。Semaphore的用法也很簡單,首先線程使用Semaphore的acquire()方法獲取一個許可證,使用完之後調用release()方法歸還許可證。還可以用tryAcquire()方法嘗試獲取許可證。
Semaphore還提供一些其他方法,具體如下。
·int availablePermits():返回此信號量中當前可用的許可證數。
·int getQueueLength():返回正在等待獲取許可證的線程數。
·boolean hasQueuedThreads():是否有線程正在等待獲取許可證。
·void reducePermits(int reduction):減少reduction個許可證,是個protected方法。
·Collection getQueuedThreads():返回所有等待獲取許可證的線程集合,是個protected方法。
用法:用用信號量實現有界緩存
public class SemaphporeCase<T> {
private final Semaphore items;//有多少元素可拿
private final Semaphore space;//有多少空位可放元素
private List queue = new LinkedList<>();
public SemaphporeCase(int itemCounts){
this.items = new Semaphore(0);
this.space = new Semaphore(itemCounts);
}
//放入數據
public void put(T x) throws InterruptedException {
space.acquire();//拿空位的許可,沒有空位線程會在這個方法上阻塞
synchronized (queue){
queue.add(x);
}
items.release();//有元素了,可以釋放一個拿元素的許可
}
//取數據
public T take() throws InterruptedException {
items.acquire();//拿元素的許可,沒有元素線程會在這個方法上阻塞
T t;
synchronized (queue){
t = (T)queue.remove(0);
}
space.release();//有空位了,可以釋放一個存在空位的許可
return t;
}
}
Exchanger
Exchanger(交換者)是一個用於線程間協作的工具類。Exchanger用於進行線程間的數據交換。它提供一個同步點,在這個同步點,兩個線程可以交換彼此的數據。這兩個線程通過exchange方法交換數據,如果第一個線程先執行exchange()方法,它會一直等待第二個線程也執行exchange方法,當兩個線程都到達同步點時,這兩個線程就可以交換數據,將本線程生產出來的數據傳遞給對方。
例子:
public class ExchangeCase {
static final Exchanger<List<String>> exgr = new Exchanger<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
List<String> list = new ArrayList<>();
list.add(Thread.currentThread().getId()+" insert A1");
list.add(Thread.currentThread().getId()+" insert A2");
list = exgr.exchange(list);//交換數據
for(String item:list){
System.out.println(Thread.currentThread().getId()+":"+item);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
List<String> list = new ArrayList<>();
list.add(Thread.currentThread().getId()+" insert B1");
list.add(Thread.currentThread().getId()+" insert B2");
list.add(Thread.currentThread().getId()+" insert B3");
System.out.println(Thread.currentThread().getId()+" will sleep");
Thread.sleep(1500);
list = exgr.exchange(list);//交換數據
for(String item:list){
System.out.println(Thread.currentThread().getId()+":"+item);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
結果:
14 will sleep
14:13 insert A1
13:14 insert B1
13:14 insert B2
13:14 insert B3
14:13 insert A2