java併發編程的藝術(精華提煉)
通常我們在使用編髮編程時,主要目的是爲了程序能夠更快的處理,但是並不是說更多的線程就一定能夠讓程序變得足夠快,有時候太多的線程反而消耗了更多的資源,反而讓程序執行得更緩慢
一.CPU的上下文切換
就算是單核CPU是能夠處理多線程任務的,它只是不停的切換線程來執行,讓我們感覺是多線程執行
下圖是串行執行和併發執行的耗時對比
從圖中我們可以看到,在數量不達到千萬級的時候,串行和並行的耗時幾乎差不多,這是因爲線程在創建和上下文切換時需要一定的時間開銷.
如何減少上下文的切換似乎是我們在併發編程時需要注意的一點.
- 無鎖編髮編程,多線程併發強佔鎖時,會引起頻繁的上下文切換,我們可以使用分段式的方法來處理數據,不同的線程處理不同段的數據,避免使用鎖
- 使用最少的線程.避免創建大量無效線程,任務量很少的情況,會讓大量線程處於等待狀態,浪費資源
二.多線程在java中的使用
volatile和synchronize
當我們使用volatile變量修飾時會引發的兩件事:
1.當前處理器的數據會回寫到系統內存
2.這個回寫內存會使處理器中的這個內存地址無效化
這兩件事說明了什麼呢.在java中使用volatile可以讓多個線程共享一個變量,在多個線程獲取這個變量的時候,該線程會等待正在改變這個變量的線程釋放掉纔會去讀取,這讓我們多線程在共享這個變量時,才能保持唯一性
synchronized實現同步的基礎
1.對於普通方法,鎖的是當前實例對象
2.對於靜態方法.鎖的是當前class對象
3.對於方法塊,鎖的是作用域裏的所有對象
通俗一點說,一個普通方法鎖,其實是鎖的是實例對象來調用時,針對於這個實例對象來說他是同步的,如果我們有多個這個實例對象同時調用它內部同步方法時,實際上還是併發請求,對於靜態方法來說,他在java中只有一份,那麼無論調用幾次他都是一個同步調用,最後的方法塊類似於我們的普通方法,不同的地方就是鎖的作用域內的所有對象都被加鎖,屬於這個線程的私有變量
三.併發編程模型的兩個關鍵問題
1.線程之間如何通信
2.線程之間如何同步
大部分時間中,我們不會關心線程之間如何通信,我們在實際編程中會遇到線程之間如何同步一個變量的問題,使用volatile來修飾一個變量是一個非常好的方法,在jdk8之後的jvm內存模型中,對象的實例是存儲於本地棧,一個變量在使用volatile後,該對象實例的引用還是在本地內存中,而該對象的的變量則被拷貝到了主內存中用於共享,不在私有,簡單來說,就是各個線程任然保留有這個變量的內存地址只是不同的內存地址被指向了主內存中的同一個變量
三.Java併發容器和框架
ConcurrentHashMap的實現原理與使用
ConcurrentHashMap是線程安全且高效的HashMap
ConcurrentHashMap的結構圖
ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap裏扮演鎖的角色;HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組。Segment的結構和HashMap類似,是一種數組和鏈表結構。一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得與它對應的Segment鎖
Java裏的阻塞隊列
ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列。
- LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列。
- PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列。
- DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
- SynchronousQueue:一個不存儲元素的阻塞隊列。
- LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
- LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
四.線程池
線程池是我們使用得最多的線程框架,合理地使用它可以爲我們帶來很多好處
1.降低資源消耗,避免重複創建線程
2.提高響應速度,保持一定量的等待線程,不用每次都重新創建線程
3.提高線程的管理性
這裏不得不提一下阿里的java編碼規範中明確的指出了:線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險,
/**
* 配置線程池
*
* @return Executor線程池
*/
@Bean("threadPoolTaskExecutor")
public ThreadPoolTaskExecutor getExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//線程池維護線程的最少數量
executor.setCorePoolSize(10);
//線程池維護線程的最大數量
executor.setMaxPoolSize(50);
//緩存隊列
executor.setQueueCapacity(99999);
//對拒絕task的處理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//設置線程名稱
executor.setThreadNamePrefix(QINGDUO_UPMS_SERVICE);
//允許的空閒時間
executor.setKeepAliveSeconds(60);
//執行初始化
executor.initialize();
return executor;
}
五.併發編程實踐
生產者和消費者模式
在併發編程中使用生產者和消費者模式能夠解決絕大多數併發問題。該模式通過平衡生產線程和消費線程的工作能力來提高程序整體處理數據的速度。在線程世界裏,生產者就是生產數據的線程,消費者就是消費數據的線程。在多線程開發中,如果生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產數據。同樣的道理,如果消費者的處理能力大於生產者,那麼消費者就必須等待生產者。爲了解決這種生產消費能力不均衡的問題,便有了生產者和消費者模式。
我寫了一個簡單的生產消費,但是並沒有同步鎖也沒有同享變量,於是產生了消費者爲-1的情況
於是我讓這個變量在線程中共享,但是拋出了IllegalMonitorStateException異常,大致意思就是說拋出這個異常表明線程嘗試等待一個對象的監視器或者去通知其他正在等待這個對象監視器的線程時,但是沒有擁有這個監視器的所有權,這就證明了,其實對於共享變量操作時實際上只能有一個線程去操作
因爲我們對鎖的操作不正確,無法釋放鎖,所以我用ReentrantLock實現了
/**
* 生產者和消費者,ReentrantLock的實現
*
*/
public class Test1 {
private static Integer count = 0;
private static final Integer FULL = 10;
//創建一個鎖對象
private Lock lock = new ReentrantLock();
//創建兩個條件變量,一個爲緩衝區非滿,一個爲緩衝區非空
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public static void main(String[] args) {
Test1 test2 = new Test1();
new Thread(test2.new Producer()).start();
new Thread(test2.new Consumer()).start();
new Thread(test2.new Producer()).start();
new Thread(test2.new Consumer()).start();
new Thread(test2.new Producer()).start();
new Thread(test2.new Consumer()).start();
new Thread(test2.new Producer()).start();
new Thread(test2.new Consumer()).start();
}
class Producer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
//獲取鎖
lock.lock();
try {
while (count == FULL) {
try {
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName()
+ "生產者生產,目前總共有" + count);
//喚醒消費者
notEmpty.signal();
} finally {
//釋放鎖
lock.unlock();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
lock.lock();
try {
while (count == 0) {
try {
notEmpty.await();
} catch (Exception e) {
e.printStackTrace();
}
}
count--;
System.out.println(Thread.currentThread().getName()
+ "消費者消費,目前總共有" + count);
notFull.signal();
} finally {
lock.unlock();
}
}
}
}
}
關注公衆號,免費獲得一本<java併發編程的藝術>