併發編程專題
第一章 線程入門
基本概念
串行
Ø 一個任務執行完再去執行另一個任務
並行
Ø 用齊頭並進的方式去完成任務,其實只是時間片的切換,離開時間片,無意義
併發
Ø 一段時間內以交替的方式去完成任務
進程
Ø 資源分配的最小單位
線程
Ø CPU調度的最小單位,共享進程中的資源,必須依附於進程,不能獨立存在
任務
Ø 線程要完成特定計算的任務
併發編程意義
Ø 充分利用CPU資源
Ø 加快用戶響應的時候
Ø 模塊化,異步化
創建線程
Ø Extends Thread : 重寫run方法
Ø Implement Runable: 實現run方法,可以共享變量
Ø Implement Callable:帶返回值
線程啓動
Ø Run方法 是對象本身方法
Ø Start方法 是啓動線程的方法
線程生命週期
Ø 新建:new的時候
Ø 就緒:調用start方法等待CPU的調度
Ø 運行:獲得CPU調度的權利
Ø 阻塞:主要有被動的或者主動的放棄了CPU的調度權
>> 線程調用sleep方法主動放棄所佔有的CPU資源
>> 調用IO阻塞方法,在該方法返回之前,該線程被阻塞
>> 線程試圖獲取一個同步監視器,但該監視器正被其它線程鎖持有
>> 線程在等待某個通知,比如notify,signal等
>> 程序調用了線程的suspend,使該線程掛起
Ø 死亡
>> 線程調用run方法正常運行且結束、
>> 線程拋出一個未捕獲的異常或者錯誤,被迫結束
>> 直接調用了線程的stop方法
線程上下文切換
Ø Wait:等待
線程會放棄對象鎖
Ø Yield:暫停
是讓當前線程讓出cpu時間,os依然可以選中當前線程
Ø Sleep:休眠
讓出cpu該其他線程,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態,線程不會釋放對象鎖
Ø Join:等待
當前線程A等待thread線程終止之後才從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(longmillis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示,如果線程thread在給定的超時時間裏沒有終止,那麼將會從該超時方法中返回
Ø Stw: GC出現的stop the word
線程故障
死鎖
Ø 有兩個以上的線程或者進程發生了資源的爭搶
活鎖
Ø X線程之間學會了謙讓,誰都不願意去爭搶資源
飢餓鎖
Ø 線程因爲CPU時間全部被其它線程搶走而得不到CPU運行時間
線程調度
Ø 公平調度
Ø 非公平調度
線程中斷
停止一個正在運行的線程,目前不推薦使用stop方法或者supend方法,而是使用interupt方法,看一下代碼,針對runable接口和thread類是有區別的,下面看
實現runable接口
public class EndRunnable {
private static class UseRunnable implements Runnable{
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()) {//改爲true,線程不會中斷;改爲Thread.interrupted(),標誌位被複位
System.out.println(Thread.currentThread().getName()
+ " I am implements Runnable.");
}
System.out.println(Thread.currentThread().getName()
+" interrupt flag is "+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseRunnable useRunnable = new UseRunnable();
Thread endThread = new Thread(useRunnable,"endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt();
}
繼承thread
public class HasInterrputException {
private static class UseThread extends Thread{
public UseThread(String name) {
super(name);
}
@Override
public void run() {
while(!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+" in InterruptedException interrupt flag is "
+isInterrupted());
interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()
+" interrupt flag is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("HasInterrputEx");
endThread.start();
Thread.sleep(500);
endThread.interrupt();
}
}
守護線程
Daemon線程是一種支持型線程,因爲它主要被用作程序中後臺調度以及支持性工作。這意味着,當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。可以通過調用Thread.setDaemon(true)將線程設置爲Daemon線程。我們一般用不上,比如垃圾回收線程就是Daemon線程。
Daemon線程被用作完成支持性工作,但是在Java虛擬機退出時Daemon線程中的finally塊並不一定會執行。在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。
看一下這段代碼
Finally代碼不一定會執行,setDaemon方法一定要放在start方法調用之前設置
線程特性
Ø 原子性:
java對變量的操作都是原子性
保證讀取到的值要不是初始化的,要不就是最新更新的值
Ø 可見性
修改變量對其它線程是可見的
Ø 有序性
指令重排序
第二章 線程安全
安全定義
類,多線程下訪問,不管調用者調用這個類,這個類總是表現出個正確的行爲
不安全原因
主要是JMM模型導致,總結如下
1、 數據不一致,緩存不一致
2、 指令的重排序
不安全表現
數據不一致
僞共享
多個線程不同的變量正位於同一個緩存行
CPU緩存
CPU緩存系統中是以緩存行(cache line)爲單位存儲的。目前主流的CPU Cache的Cache Line大小都是64Bytes
CPU Cache分成了三個級別:L1,L2,L3。級別越小越接近CPU, 所以速度也更快, 同時也代表着容量越小
CPU獲取數據回依次從L1,L2,L3中查找,如果都找不到則會直接向內存查找
解決方案
jdk8以前需要自己實現:緩存填充行
jdk8以後需要自己實現:添加註解
@sun.misc.Contended, 此註解默認是無效的,需要在jvm啓動時設置-XX:-RestrictContended纔會生效
死鎖
通常來說是發生在系統有好幾個進程或者線程同時爭搶某個資源而發生了死鎖,
它嚴重的時候會消耗整個CPU資源,而是程序癱瘓,所以平時編程的時候一定要注意死鎖的發生
public class NormalDeadLock {
private static Object valueFirst = new Object();//第一個鎖
private static Object valueSecond = new Object();//第二個鎖
//先拿第一個鎖,再拿第二個鎖
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueFirst){
System.out.println(threadName+" get valueSecond");
Thread.sleep(100);
synchronized (valueSecond){
System.out.println(threadName+" get valueFirst");
}
}
}
//先拿第二個鎖,再拿第一個鎖
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueSecond){
System.out.println(threadName+" get valueSecond");
Thread.sleep(100);
synchronized (valueFirst){
System.out.println(threadName+" get valueFirst");
}
}
}
private static class TestThread extends Thread{
private String name;
public TestThread(String name) {
this.name = name;
}
public void run(){
Thread.currentThread().setName(name);
try {
SecondToFisrt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("TestDeadLock");
TestThread testThread = new TestThread("SubTestThread");
testThread.start();
try {
fisrtToSecond();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
安全編程
Synchroized
基於JVM級別的,可以用來同步方法塊,指定同步快,在高併發的情況下,性能能比Lock稍微差點
模擬兩個線程分別對一個數字做同樣的加法和減法,最終打印的結果應該是一致的
public class AddSubstractionTest {
private int age=100;
private static class AddSubThread extends Thread{
private AddSubstractionTest synTest;
public AddSubThread(AddSubstractionTest synTest) {
this.synTest = synTest;
}
@Override
public void run() {
for(int i=0;i<1000;i++) {//遞增100000
synTest.add();
}
System.out.println(Thread.currentThread().getName()
+" age = "+synTest.getAge());
}
}
/**
* 加法
*/
public synchronized void add() {
age++;
}
/**
* 減法
* @return
*/
public synchronized void muls() {
age--;
}
public int getAge() {
return age;
}
public static void main(String[] args) {
AddSubstractionTest test = new AddSubstractionTest();
AddSubThread thread = new AddSubThread(test);
thread.start();
for (int i=0;i<1000;i++){
test.muls();
}
}
}
如果去掉synchroized關鍵字,會發現結果每次都是不正確且還是不一樣的,這就說明了數據出現了不安全,也就是我們所說的線程不安全,加上這個關鍵字線程就安全了,但是加這個關鍵字要考慮實際業務的需求,加到不同的位置.
Ø 直接加方法上,粒度比較大,性能也比較差
Ø 加指定需要的同步塊,粒度變小,性能也較好
但這裏需要注意的,這裏同步的對象不能是基本數值類型,比如int,而是要換成引用對象,Integer
Lock
鎖是基於類級別的,正常來說性能會比synchroized關鍵字要好點,但是它編程會相對麻煩點,從使用的角度來說又可以分爲專門的普通鎖和讀寫鎖
ReetrantLock
普通的鎖,有三個常用方法
方法 | 說明 |
---|---|
lock | 鎖住當前對象 |
unLock | 釋放鎖,這個方法一定是會執行的 |
tryLock | 嘗試獲取鎖,可以指定時間 |
lockInterruptibly | 獲取鎖時候,響應中斷,直接拋出異常 |
看一個簡單實用的例子 做加減法的功能
public class LockCase {
private Lock lock = new ReentrantLock();
private int age = 100000;//初始100000
private static class TestThread extends Thread{
private LockCase lockCase;
public TestThread(LockCase lockCase,String name) {
super(name);
this.lockCase = lockCase;
}
@Override
public void run() {
for(int i=0;i<100000;i++) {//遞增100000
lockCase.add();
}
System.out.println(Thread.currentThread().getName()
+" age = "+lockCase.getAge());
}
}
public void add() {
lock.lock();
try {
age++;
} finally {
lock.unlock();
}
}
public void mlus() {
lock.lock();
try {
age--;
} finally {
lock.unlock();
}
}
public int getAge() {
return age;
}
public static void main(String[] args) throws InterruptedException {
LockCase lockCase = new LockCase();
Thread endThread = new TestThread(lockCase,"endThread");
endThread.start();
for(int i=0;i<100000;i++) {//遞減100000
lockCase.mlus();
}
System.out.println(Thread.currentThread().getName()
+" age = "+lockCase.getAge());
}
}
ReetrantReaderWriteLock
讀寫鎖,它把鎖分的更細了,區別了讀鎖和寫鎖,如果在讀多寫少的場景,這個性能比普通的鎖要好很多.
Ø readLock 獲取讀鎖
Ø writeLock 獲取寫鎖
實現一個商品數量的讀取和修改的例子
定義商品服務接口
public interface GoodsService {
public GoodsInfo getNum();//讀取商品數量
public void setNum(int number);//設置商品數量
}
定義服務接口的實現
在這裏插入代碼片public class UseRwLock implements GoodsService {
private GoodsInfo goodsInfo;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();//get
private final Lock writeLock = lock.writeLock();//set
public UseRwLock(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
/**
* 獲取商品數量
* @return
*/
@Override
public GoodsInfo getNum() {
readLock.lock();
SleepTools.ms(5);
try {
return this.goodsInfo;
} finally {
readLock.unlock();
}
}
/**
* 設置商品數量
* @param number
*/
@Override
public void setNum(int number) {
writeLock.lock();
try {
SleepTools.ms(50);
this.goodsInfo.changeNumber(number);
} finally {
writeLock.unlock();
}
}
}
定義讀線程和寫線程以及測試
public class BusiApp {
static final int readWriteRatio = 10;//讀寫線程的比例
static final int minthreadCount = 3;//最少線程數
static CountDownLatch latch= new CountDownLatch(1);
/**
* 讀取線程
*/
private static class GetThread implements Runnable{
private GoodsService goodsService;
public GetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
try {
latch.await();//讓讀寫線程同時運行
} catch (InterruptedException e) {
}
long start = System.currentTimeMillis();
for(int i=0;i<100;i++){//操作100次
goodsService.getNum();
}
System.out.println(Thread.currentThread().getName()+"讀取商品數據耗時:"
+(System.currentTimeMillis()-start)+"ms");
}
}
/**
* 寫入線程
*/
private static class SetThread implements Runnable{
private GoodsService goodsService;
public SetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
try {
latch.await();//讓讀寫線程同時運行
} catch (InterruptedException e) {
}
long start = System.currentTimeMillis();
Random r = new Random();
for(int i=0;i<10;i++){//操作10次
SleepTools.ms(50);
goodsService.setNum(r.nextInt(10));
}
System.out.println(Thread.currentThread().getName()
+"寫商品數據耗時:"+(System.currentTimeMillis()-start)+"ms---------");
}
}
public static void main(String[] args) throws InterruptedException {
GoodsInfo goodsInfo = new GoodsInfo("Cup",100000,10000);
GoodsService goodsService = new UseRwLock(goodsInfo);
for(int i = 0;i<minthreadCount;i++){
Thread setT = new Thread(new SetThread(goodsService));
for(int j=0;j<readWriteRatio;j++) {
Thread getT = new Thread(new GetThread(goodsService));
getT.start();
}
setT.start();
}
latch.countDown();
}
}
可以對比普通的鎖,性能就比較明顯了
public class UseSyn implements GoodsService {
private GoodsInfo goodsInfo;
public UseSyn(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public synchronized GoodsInfo getNum() {
SleepTools.ms(5);
return this.goodsInfo;
}
@Override
public synchronized void setNum(int number) {
SleepTools.ms(5);
goodsInfo.changeNumber(number);
}
}
鎖的使用場景
1、 嘗試非阻塞獲取鎖
2、 可以被中斷的獲取鎖
3、 超時獲取鎖
CAS
概括
比較和交換,有三個操作符
Ø V:內存地址
Ø A:一個期望的舊值
Ø B:一個新值
結論:
操作的時候如果這個內存地址上存放的值等於期望的值A,那麼就將內存地址上的值變爲新值B,否則不做任何操作
問題
Ø ABA:本來被子裏面放了一杯水,走了以後被別人喝了,然後別人又加上了,這個時候你還以爲被子裏面的水沒有動過,其實也就不是之前的水了.
上面的本質和這個是一樣的,可以通過版本號來解決.
Ø 循環的時間長開銷大,自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷
Ø 往往只能保證一個共享變量的原子操作
線程可見性
Voliate
它的本質,一句話概括,就是強制線程每次讀取該值的時候都去“主內存”中取值,線程其實不是直接修改堆內存中的值,而是修改對應的副本值,完了之後,再把線程的副本的值寫回堆內存,這樣堆內存中的值就發生了變化
優點
Ø 可見性
Ø 有序性
Ø 變量操作的讀寫的原子性,但不一定保證
Ø 禁止重排序
Ø JIT編譯器不會對相應代碼做優化
缺點
Ø 讀取成本比讀取普通變量要稍微高
Ø 它被存儲在高速緩存或主內存中,而不是寄存器中
使用場景
Ø 作爲狀態標誌
Ø 保障可見性
Ø 替代鎖
舉例 線程可見性
public class VolatileTest extends Thread {
//線程可見性
volatile boolean flag = false;
//如果沒有加volatile關鍵字發現程序也能正常退出,那是因爲默認的JVM是以客戶端形式運行的,要改成服務端模式,具體如何修改可以參考這個博客
https://www.cnblogs.com/wxw7blog/p/7221756.html
int i = 0;
public void run() {
while (!flag) {
i++;
}
}
public static void main(String[] args) throws Exception {
VolatileTest vt = new VolatileTest();
vt.start();
Thread.sleep(2000);
vt.flag = true;
System.out.println("stope" + vt.i);
}
}
線程會退出的.
例子 無法提供原子性
public class VolatileUnsafe {
private static class VolatileVar implements Runnable {
private volatile int a = 0;
@Override
public void run() {
a = a + 1;
System.out.println(Thread.currentThread().getName() + ":----" + a);
SleepTools.ms(100);
a = a + 1;
System.out.println(Thread.currentThread().getName() + ":----" + a);
}
}
public static void main(String[] args) {
VolatileVar v = new VolatileVar();
Thread t1 = new Thread(v);
Thread t2 = new Thread(v);
Thread t3 = new Thread(v);
Thread t4 = new Thread(v);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
打印的結果是會有重複的,並不是按照理想的順序打印的
線程原子性
保證線程的操作是不可分割的
類型
Ø 基本類型: AtomicInteger/Long/Boolean
這個一本結合volatlie關鍵字一起使用,做一些計數器之類的或者標識位判斷.
Ø 數組類型: AtomicIntergerArray/LongArray
Ø 引用類型: AtomicReference/ AtomicStampedReference
Ø 字段更新類型: AtomicInteger/LongFieldUpdate
這裏面有解決ABA的叫AtomicStampedReference
實例演示 引用類型的使用
public class UseAtomicReference {
public static AtomicReference<UserInfo> atomicUserRef = new AtomicReference<UserInfo>();//原子引用類型的實例
public static void main(String[] args) {
UserInfo user = new UserInfo("Mark", 15);//要修改的實體的實例
// V 設置 A
atomicUserRef.set(user);//用原子引用類型包裝
UserInfo updateUser = new UserInfo("Bill", 17);//要變化成爲的新實例
// 調用CAS 來判斷 V設置B
atomicUserRef.compareAndSet(user, updateUser);
atomicUserRef.compareAndSet(updateUser, user);
System.out.println("ref:"+atomicUserRef.get().getName()+":"+atomicUserRef.get().getAge());
System.out.println("org:"+user.getName()+":"+user.getAge());
}
//定義一個實體類
static class UserInfo {
private String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}
實例演示 解決ABA問題
public class UseAtomicStampedReference {
static AtomicStampedReference<String> asr = new AtomicStampedReference("mark", 0);
public static void main(String[] args) throws InterruptedException {
final int oldStamp = asr.getStamp();//拿初始版本0
final String oldReference = asr.getReference();//初始值
System.out.println(oldReference+"============"+oldStamp);
Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":當前變量值:"
+oldReference + "-當前版本戳:" + oldStamp + "-"
+ asr.compareAndSet(oldReference,
oldReference + "+Java", oldStamp, oldStamp + 1));
}
});
Thread errorStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();//變量的最新值
System.out.println(Thread.currentThread().getName()+":當前變量值:"
+reference + "-當前版本戳:" + asr.getStamp() + "-"
+ asr.compareAndSet(reference,
reference + "+C", oldStamp, oldStamp + 1));
}
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();
System.out.println(asr.getReference()+"============"+asr.getStamp());
}
}
常用方法
Ø getAndIncrement() 增加1,返回增加前的數據
Ø incrementAndGet() 增加1,返回增加後的值
Ø getAndDecrement() 減少1,返回減少前的數據
Ø decrementAndGet() 減少1,返回減少後的值
Ø getAndAdd(int) 增加指定的數據,返回變化前的數據
Ø compareAndSet(int, int) 嘗試新增後對比,若增加成功則返回true否則返回false
第三章 線程協調
多個線程之間協同工作,一般指兩個以上工作的線程
Wait VS Notify
Wait是object的方法,
使用
object.wait:當前正在執行的線程被阻塞
object.notify/notifyall:通知正在等待喚醒的線程
問題
Ø 過早喚醒
Ø 欺騙喚醒
Ø 信號丟失
Ø 無法區分是否是等待超時
Notify VS Notifyall
Notify:只能被喚醒所有等待線程中的一個,至於是哪一個完全是隨機的
Notifyall:喚醒所有被等待的線程
建議使用notifyall
代碼實例
public class Express { //快遞的里程數和公里數之間存在互相通知
public final static String CITY = "ShangHai";
private int km;/*快遞運輸里程數*/
private String site;/*快遞到達地點*/
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
/* 變化公里數,然後通知處於wait狀態並需要處理公里數的線程進行業務處理*/
public synchronized void changeKm(){
this.km = 101;
notifyAll();
}
/* 變化地點,然後通知處於wait狀態並需要處理地點的線程進行業務處理*/
public synchronized void changeSite(){
this.site = "BeiJing";
notifyAll();
}
public synchronized void waitKm(){
while(this.km<=100){//公里數小於100不做處理
try {
wait();
System.out.println("Check Km thread["+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the Km is "+this.km+",I will change db");
}
public synchronized void waitSite(){
while(this.site.equals(CITY)){//快遞到達目的地
try {
wait();
System.out.println("Check Site thread["+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the site is "+this.site+",I will call user");
}
}
測試
public class TestWN {
private static Express express = new Express(0,Express.CITY);
/*檢查里程數變化的線程,不滿足條件,線程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*檢查地點變化的線程,不滿足條件,線程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快遞地點變化
}
}
Condition VS Signal
它不是object的方法,它是基於Lock獲取的,想必wait有一定的優點
創建condition
ReentrantLock.newCondition
常用方法
condition.awatiunit可以解決是否超時
condition.awatit等於object.wait
condition.singal/singall相當於object.notify/notifyall
優點
Ø 解決過早喚醒通知
Ø 實現多路通知
代碼實例
ublic class ExpressCond {
public final static String CITY = "ShangHai";
private int km;/*快遞運輸里程數*/
private String site;/*快遞到達地點*/
private Lock kmLock = new ReentrantLock();
private Lock siteLock = new ReentrantLock();
private Condition kmCond = kmLock.newCondition();
private Condition siteCond = siteLock.newCondition();
public ExpressCond() {
}
public ExpressCond(int km, String site) {
this.km = km;
this.site = site;
}
/* 變化公里數,然後通知處於wait狀態並需要處理公里數的線程進行業務處理*/
public void changeKm(){
kmLock.lock();
try {
this.km = 101;
kmCond.signal();
} finally {
kmLock.unlock();
}
}
/* 變化地點,然後通知處於wait狀態並需要處理地點的線程進行業務處理*/
public synchronized void changeSite(){
siteLock.lock();
try {
this.site = "BeiJing";
siteCond.signal();
} finally {
siteLock.unlock();
}
}
public void waitKm(){
kmLock.lock();
try {
while(this.km<=100){//公里數小於100不做處理
try {
kmCond.await();
System.out.println("Check Km thread["
+Thread.currentThread().getId()+"] is be signal");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
kmLock.unlock();
}
System.out.println("the Km is "+this.km+",I will change db");
}
public void waitSite(){
siteLock.lock();
try {
while(this.site.equals(CITY)){//快遞到達目的地
try {
siteCond.await();
System.out.println("Check Site thread["
+Thread.currentThread().getId()+"] is be signal");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
siteLock.unlock();
}
System.out.println("the site is "+this.site+",I will call user");
}
}
測試效果
public class TestCond {
private static ExpressCond express = new ExpressCond(0,ExpressCond.CITY);
/*檢查里程數變化的線程,不滿足條件,線程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*檢查地點變化的線程,不滿足條件,線程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快遞地點變化
}
}
第四章 併發工具類
CountDownLatch
閉鎖,CountDownLatch這個類能夠使一個線程等待其他線程完成各自的工作後再執行。例如,應用程序的主線程希望在負責啓動框架服務的線程已經啓動所有的框架服務之後再執行。CountDownLatch是通過一個計數器來實現的,計數器的初始值爲初始任務的數量。每當完成了一個任務後,計數器的值就會減1(CountDownLatch.countDown()方法)。當計數器值到達0時,它表示所有的已經完成了任務,然後在閉鎖上等待CountDownLatch.await()方法的線程就可以恢復執行任務。實現最大的並行性:有時我們想同時啓動多個線程,實現最大程度的並行性。例如,我們想測試一個單例類。如果我們創建一個初始計數爲1的CountDownLatch,並讓所有線程都在這個鎖上等待,那麼我們可以很輕鬆地完成測試。我們只需調用 一次countDown()方法就可以讓所有的等待線程同時恢復執行。開始執行前等待n個線程完成各自任務:例如應用程序啓動類要確保在處理用戶請求前,所有N個外部系統已經啓動和運行了,例如處理excel中多個表單。
本質
一個線程等待其他線程完成各自的工作之後,再執行(一般阻塞主線程)
方法
Ø countdown 計數器減1
Ø await 等待
注意
Ø 一個實例只能執行一次通知或者執行
Ø 確保countDown方法一定被執行
代碼實例
主線程要等A和B兩個子線程執行完了,再執行
public class CountDownLatchDemo {
public static void main(String[] args)throws Exception {
CountDownLatch latch = new CountDownLatch(2);
new Thread(new Runnable() {
@Override
public void run() {
latch.countDown();
System.out.println("執行子線程A");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
latch.countDown();
System.out.println("執行子線程B");
}
}).start();
latch.await();
System.out.println("執行主線程");
}
}
模擬服務啓動檢查,所有都啓動成功才執行主業務,否則退出虛擬機
public class ServerStartMainThread {
public static void main(String[] args) {
ServerStart.start();
Runtime.getRuntime().addShutdownHook(new CleanResourceThread());
}
/**
* 清理服務資源
*/
static class CleanResourceThread extends Thread{
@Override
public void run() {
Debug.info("服務正在退出,清理相關資源...");
}
}
/**
* 定義服務接口
*/
interface Service {
void start();
boolean isStarted();
}
/**
* 服務的默認實現
*/
static abstract class AbstractService implements Service{
protected final CountDownLatch countDownLatch ;
protected boolean isStarted = false;
public AbstractService(CountDownLatch countDownLatch){
this.countDownLatch = countDownLatch;
}
@Override
public void start() {
new StartServiceThread().start();
}
@Override
public boolean isStarted() {
return isStarted;
}
/**
* 啓動服務
*/
public abstract void doStart();
/**
* 實現服務啓動的真正線程
*/
class StartServiceThread extends Thread{
@Override
public void run() {
final String serviceName =AbstractService.this.getClass()
.getSimpleName();
Debug.info("Starting %s", serviceName);
try {
doStart();
isStarted = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
Debug.info("Done Starting %s", serviceName);
}
}
}
}
/**
* 啓動DB數據初始化服務
*/
static class DbDataInitService extends AbstractService{
public DbDataInitService(CountDownLatch countDownLatch) {
super(countDownLatch);
}
@Override
public void doStart() {
Tools.randomPause(1000);
System.out.println("SampleServiceA 正在執行數據庫初始化任務");
}
}
/**
* 啓動日誌恢復服務
*/
static class LogFileRecoverService extends AbstractService{
public LogFileRecoverService(CountDownLatch countDownLatch) {
super(countDownLatch);
}
@Override
public void doStart() {
Tools.randomPause(2000);
System.out.println("SampleServiceC 正在啓動日誌恢復服務");
}
}
/**
* 啓動緩存初始化服務
*/
static class CacheInitService extends AbstractService{
public CacheInitService(CountDownLatch countDownLatch) {
super(countDownLatch);
}
@Override
public void doStart() {
Tools.randomPause(2000);
System.out.println("SampleServiceB 正在啓動緩存預熱服務");
//throw new RuntimeException("緩存爲空");
}
}
/**
* 服務啓動管理
*/
static class ServiceManager{
static volatile CountDownLatch countDownLatch;
static Set<Service>services;
/**
* 初始化要啓動的服務
* @return
*/
static Set<Service> getServices(){
Set<Service>serviceSet = new HashSet<Service>();
countDownLatch = new CountDownLatch(3);
serviceSet.add(new DbDataInitService(countDownLatch));
serviceSet.add(new LogFileRecoverService(countDownLatch));
serviceSet.add(new CacheInitService(countDownLatch));
return serviceSet;
}
/**
* 啓動所有的服務
*/
public static void startServices(){
services = getServices();
for (Service service: services){
service.start();
}
}
/**
* 檢查服務啓動的狀態
*/
public static boolean checkServiceState(){
boolean isStarted = true;//默認是成功啓動的
try {
countDownLatch.await(); //等待服務啓動完畢
} catch (InterruptedException e) {
e.printStackTrace();
}
//遍歷每個服務的啓動狀態 有一個沒有啓動就返回false
for (Service service : services){
if (!service.isStarted()){
isStarted = false;
break;
}
}
return isStarted;
}
}
/**
* 封裝服務啓動
*/
static class ServerStart{
public static void start(){
// 啓動服務
ServiceManager.startServices();
// 檢查服務啓動狀態
boolean isOK = ServiceManager.checkServiceState();
if (isOK){
System.out.println("All services were sucessfully started!");
}
else {
// 個別服務啓動失敗,退出JVM
System.err.println("Some service(s) failed to start,exiting JVM...");
System.exit(1);
}
}
}
}
CyclicBarrier
CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續運行。CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞。
CyclicBarrier還提供一個更高級的構造函數CyclicBarrier(int parties,Runnable barrierAction),用於在線程到達屏障時,優先執行barrierAction,方便處理更復雜的業務場景。
CyclicBarrier可以用於多線程計算數據,最後合併計算結果的場景。
本質
一組線程到達一個屏障是被阻塞,知道最後一個線程也達到,屏障纔會開門
(一般阻塞工作線程)
方法
Await:阻塞當前運行線程
注意
它可以循環使用
代碼實例
累加求和,比如開啓3個線程計算1-9之間的和
public class SumThread {
//存放子線程工作結果的容器
private static ConcurrentHashMap<String,Integer> resultMap = new ConcurrentHashMap<>();
// 啓動三個子線程來分別計算 ,最後使用一個結果彙總線程
private static CyclicBarrier barrier = new CyclicBarrier(3,new CollectorThread());
public static void main(String[] args) throws Exception {
new AddThread(1,3).start();
new AddThread(3,6).start();
new AddThread(6,9).start();
}
//對每個線程結果進行彙總
static class CollectorThread extends Thread{
int sum =0;
@Override
public void run() {
for (Map.Entry<String, Integer> map : resultMap.entrySet()) {
sum += map.getValue();
}
System.out.println("thread compute summary result is "+sum);
}
}
//求和線程
static class AddThread extends Thread{
private int start;
private int end;
public AddThread(int start, int end) {
this.start = start;
this.end = end;
}
int sum;
@Override
public void run() {
System.out.println("start compute"+" ["+ start+" to "+end +"]");
for (int i=start;i<=end;i++){
sum+=i;
}
System.out.println("start compute "+"["+ start + "to"+end +"]"+"result is "+sum);
resultMap.put(this.getName(),sum);
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
CyclicBarrier和CountDownLatch的區別
CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,CountDownLatch.await一般阻塞主線程,所有的工作線程執行countDown,而CyclicBarrierton通過工作線程調用await從而阻塞工作線程,直到所有工作線程達到屏障。
Semaphore
Semaphore(信號量)是用來控制同時訪問特定資源的線程數量,它通過協調各個線程,以保證合理的使用公共資源。應用場景Semaphore可以用於做流量控制,特別是公用資源有限的應用場景,比如數據庫連接。假如有一個需求,幾十個線程併發地存儲到數據庫中,而數據庫的連接數只有10個,這時我們必須控制只有10個線程同時獲取數據庫連接保存數據,否則會報錯無法獲取數據庫連接。這個時候,就可以使用Semaphore來做流量控制。
本質
保證在多線程情況下,合理利用公共資源
方法
Ø acquire 獲取許可證
Ø relase 釋放許可證
Ø tryAcquire()方法嘗試獲取許可證
Ø availablePermits 當前可用的許可證數
Ø getQueueLength 正在等待獲取許可證的線程數
Ø hasQueuedThreads是否有線程正在等待獲取許可證
Ø reducePermits減少reduction個許可證
Ø getQueuedThreads返回所有等待獲取許可證的線程集合
場景
流程控制,連接控制,共享控制
代碼實例
模擬多線程獲取數據庫的連接
public class DBPoolSemaphore {
private final static int POOL_SIZE = 10;
private final Semaphore useful,useless;//兩個指示器,分別表示池子還有可用連接和已用連接
//存放數據庫連接的容器 使用鏈表
private static LinkedList<Connection> pool = new LinkedList<Connection>();
//初始化池
static {
for (int i = 0; i < POOL_SIZE; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
public DBPoolSemaphore() {
this.useful = new Semaphore(10);
this.useless = new Semaphore(0);
}
/*歸還連接*/
public void returnConnect(Connection connection) throws InterruptedException {
if(connection!=null) {
System.out.println("當前有"+useful.getQueueLength()+"個線程等待數據庫連接!!"
+"可用連接數:"+useful.availablePermits());
useless.acquire();
synchronized (pool) {
pool.addLast(connection);
}
useful.release();
}
}
/*從池子拿連接*/
public Connection takeConnect() throws InterruptedException {
useful.acquire();
Connection connection;
synchronized (pool) {
connection = pool.removeFirst();
}
useless.release();
return connection;
}
}
測試
public class AppTest {
private static DBPoolSemaphore dbPool = new DBPoolSemaphore();
private static class BusiThread extends Thread{
@Override
public void run() {
Random r = new Random();//讓每個線程持有連接的時間不一樣
long start = System.currentTimeMillis();
try {
Connection connect = dbPool.takeConnect();
System.out.println("Thread_"+Thread.currentThread().getId()
+"_獲取數據庫連接共耗時【"+(System.currentTimeMillis()-start)+"】ms.");
SleepTools.ms(100+r.nextInt(100));//模擬業務操作,線程持有連接查詢數據
System.out.println("查詢數據完成,歸還連接!");
dbPool.returnConnect(connect);
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
Thread thread = new BusiThread();
thread.start();
}
}
}
ThreadLocal
存儲共享變量
方法
Ø initValue 初始化方法 開發者可以實現
Ø set
Ø get
Exchanger
多線程之間的數據交互,這個使用不多
第五章 線程池
優點
1、 降低我們的資源消耗;
2、 提高我們的響應速度,T1:線程創建時間;T2:任務運行的時間;T3:銷燬線程
3、 提高了線程的可管理性
原理
提交一個任務到線程池中,線程池的處理流程如下:
1、判斷線程池裏的核心線程是否都在執行任務,如果不是(核心線程空閒或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列裏。如果工作隊列滿了,則進入下個流程。
3、判斷線程池裏的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。
詳細闡述
- 如果此時線程池中的數量小於corePoolSize,即使線程池中的線程都處於空閒狀態,也要創建新的線程來處理被添加的任務。
- 如果此時線程池中的數量大於等於corePoolSize,但是緩衝隊列 workQueue未滿,那麼任務被放入緩衝隊列。
- 如果此時線程池中的數量大於corePoolSize,緩衝隊列workQueue滿,並且線程池中的數量小於maximumPoolSize,建新的線程來處理添加的任務。
- 如果此時線程池中的數量大於corePoolSize,緩衝隊列workQueue滿,並且線程池中的數量等於maximumPoolSize,那麼通過 handler所指定的策略來處理此任務。也就是處理任務的優先級爲:核心線程corePoolSize、任務隊列workQueue、最大線程maximumPoolSize,如果三者都滿了,使用handler處理被拒絕的任務。
- 當線程池中的線程數量大於corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止。這樣,線程池可以動態的調整池中的線程數。
UML類圖
| ------------------------------------------------------------ |
| |
Ø Executor
一個接口,其定義了一個接收Runnable對象的方法executor,其方法簽名爲executor(Runnable command)
Ø ExecutorService
是一個比Executor使用更廣泛的子類接口,其提供了生命週期管理的方法,以及可跟蹤一個或多個異步任務執行狀況返回Future的方法
Ø AbstractExecutorService
ExecutorService執行方法的默認實現
Ø ThreadPoolExecutor
線程池,可以通過調用Executors以下靜態工廠方法來創建線程池並返回一個ExecutorService對象
Ø ScheduledThreadPoolExecutor
ScheduledExecutorService****的實現,一個可定時調度任務的線程池
Ø ScheduledExecutorService
一個可定時調度任務的接口
Ø Executors
封裝各類線程池的實現,比如
FixedThreadPool
SingleThreadPool
CachedThreadPool
WorkStealingPool
…
內部實現
儘快開發當中我們經常使用的Executors爲我們封裝我的常用線程池,但是如果要更好的使用好線程池,需要從瞭解實現細節,就是
ThreadPoolExecutor
corePoolSize
核心線程數,
maximumPoolSize
允許的最大線程數,
keepAliveTime
空閒的線程存活的時間
TimeUnit
keepAliveTime時間單位
workQueue
阻塞隊列
threadFactory
線程工廠,缺省的線程的命名規則: pool-數字+thead-數字
RejectedExecutionHandler 飽和策略
(1)AbortPolicy:;直接拋出異常,默認
(2)CallerRunsPolicy:;
(3)DiscardOldestPolicy:;丟棄最老的任務
(4)DiscardPolicy:直接丟棄的任務
拒絕策略代碼演示
當用戶請求的線程超過了最大線程數就會把沒有處理的線程存放到隊列中,
如果隊列的容量也滿了,這個時候就會調用對應的拒絕處理handler。
FixedThreadPool
使用固定線程數的線程池,適用於爲了滿足資源管理的需求,而需要限制當前線程數量的應用場景,它適用於負載比較重的服務器
鏈表結構且是有界的
SingleThreadExecutor
創建使用單個線程的,適用於需要保證順序地執行各個任務;並且在任意時間點,不會有多個線程是活動的應用場景。
CachedThreadPool
創建一個會根據需要創建新線程的大小無界的線程池,適用於執行很多的短期異步任務的小程序,或者是負載較輕的服務器
不存儲元素的隊列
WorkStealingPool
利用所有運行的處理器數目來創建一個工作竊取的線程池,使用forkjoin實現
利用forkjoin機制實現了線程之間的’竊取’
ScheduledThreadPoolExecutor
Ø schedule 只調度一次
Ø scheduleAtFixedRate 固定時間間隔循環的執行
Ø scheduleWithFixedDelay 固定延時時間間隔的循環的執行
線程池擴展
如果我們想在一個線程池執行之前,執行之後,關閉的時候做一些事情,這個時候需要自定義線程池的擴展,主要是繼承ThreadPoolExecutor,常用的有三個方法
Ø beforeExecute 線程執行之前調用,如果它拋出異常,那麼after方法也不會被調用
Ø afterExecute 線程執行之後調用,如果拋出的不是error,該方法都會被調用
Ø terminated 線程池退出的時候調用
注意
1、 無論任務是從run中正常返回,還是拋出一個異常而返回,afterExecute都會被調用。如果任務在完成後帶有一個Error,那麼就不會盜用afterExecute。
2、 如果beforeExecute拋出一個RuntimeException,那麼任務將不被執行,並且afterExecute也不會被調用。
3 在線程完成關閉操作時調用terminated,也就是在所有任務都已經完成並且所有工作者線程也已經關閉後。terminated可以用來釋放Executor在其生命週期裏分配的各種資源,此外還可以執行發送通知、記錄日誌或者手機finalize統計信息等操作。
代碼實例
//定義線程池
public class CustomThreadPool extends ThreadPoolExecutor {
public CustomThreadPool() {
super(1, 1, 0L, TimeUnit.SECONDS, null);
}
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private final Logger log = Logger.getLogger("CustomThreadPool");
private final AtomicLong numTasks = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();
//線程執行之前調用的方法
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
log.fine(String.format("Thread %s: start %s", t, r));
startTime.set(System.nanoTime());
}
//線程執行之後調用的方法
@Override
protected void afterExecute(Runnable r, Throwable t) {
try {
long endTime = System.nanoTime();
long taskTime = endTime - startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
log.fine(String.format("Thread %s: end %s, time=%dns",
t, r, taskTime));
} finally {
super.afterExecute(r, t);
}
}
//線程推出時候調用的方法
@Override
protected void terminated() {
try {
log.info(String.format("Terminated: avg time=%dns",
totalTime.get() / numTasks.get()));
} finally {
super.terminated();
}
}
}
線程最佳數量
CPU密集型:建議是n+1
儘量使用較小的線程池,一般爲CPU核心數+1。 因爲CPU密集型任務使得CPU使用率很高,若開過多的線程數,只能增加上下文切換的次數,因此會帶來額外的開銷
IO密集型:建議是2n
IO密集型任務 可以使用稍大的線程池,一般爲2*CPU核心數。 IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候去處理別的任務,充分利用CPU時間
備註:其中n, 線程等待時間所佔比例越高,需要越多線程。線程CPU時間所佔比例越高,需要越少線程
第六章 高級特性
CompletionService
異步批量提交任務接口,且任務是帶返回結果的,就是線程實現的接口是callable,這個時候用它比較合適,因爲它對future做了一個打包裝,把多個future的結果進行了合併.它的創建依賴於一個已經創建好的線程池
創建
ExecutorService exec = Executors.newCachedThreadPool();
CompletionService<Integer> execcomp = new ExecutorCompletionService<Integer>(exec);
實例分析
比如開啓10個線程,計算線程求和
public class CompeletionServiceDemo {
//獲取到一個callable
private Callable<Integer> getCallableTask() {
final Random rand = new Random();
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int i = rand.nextInt(10);
int j = rand.nextInt(10);
int sum = i * j;
System.out.print(sum + "\t");
return sum;
}
};
return callable;
}
//用隊列來進行求和
public int summaryQueue() throws Exception {
BlockingQueue<Future<Integer>> queue = new LinkedBlockingQueue();
int sum = 0, res = 0;
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
Future<Integer> future = executorService.submit(getCallableTask());
queue.add(future);
}
//開始遍歷所有future
int qs = queue.size();
for (int i = 0; i < qs; i++) {
res = queue.take().get();
sum = sum + res;
}
executorService.shutdown();
return sum;
}
//使用CS來合併future計算
public int summaryCompletionService() throws Exception {
int sum = 0, res = 0;
ExecutorService executorService = Executors.newCachedThreadPool();
CompletionService completionService = new ExecutorCompletionService(executorService);
for (int i = 0; i < 5; i++) {
completionService.submit(getCallableTask());
}
//開始遍歷所有future
for (int i = 0; i < 5; i++) {
Future<Integer> future = completionService.take();
res = future.get();
sum = sum + res;
}
executorService.shutdown();
return sum;
}
public static void main(String[] args) throws Exception {
CompeletionServiceDemo cs = new CompeletionServiceDemo();
int sum = cs.summaryCompletionService();
System.out.println("sum result:" + sum);
}
}
ForkJoin
實現多線程的並行計算,採取了分而治之的思想,同時實現了線程之間的’竊取’(一個線程把自己的事情做完了,又會主動去偷偷獲取還在忙碌的線程的任務,做完之後再返回結果-做好事不留名)
本質
把一個大的任務進行拆分成小任務,一直拆到不能拆爲主,然後再把每個小的任務的計算結果進行彙總
類型
Ø RecursiveTask 有返回結果
Ø RecursiveAction 無返回結果
方法
Ø invokeAll 執行所有方法
Ø join 結果集進行累加
執行
ForkJoinPool.invoke
代碼實例
統計某個磁盤目錄下面所有的目錄個數和文件個數
public class SumDirsFiles extends RecursiveTask<Integer> {
private File path;
public SumDirsFiles(File path) {
this.path = path;
}
@Override
protected Integer compute() {
int count = 0;
int dirCount = 0;
List<SumDirsFiles> subTasks = new ArrayList<>();
File[] files = path.listFiles();
if (files!=null){
for (File file : files) {
if (file.isDirectory()) {
// 對每個子目錄都新建一個子任務。
subTasks.add(new SumDirsFiles(file));
dirCount++;//統計目錄個數
} else {
count++;// 遇到文件,文件個數加1。
}
}
System.out.println("目錄:" + path.getAbsolutePath()
+"包含目錄個數:"+dirCount+",文件個數:"+count);
if (!subTasks.isEmpty()) {
// 在當前的 ForkJoinPool 上調度所有的子任務。
for (SumDirsFiles subTask : invokeAll(subTasks)) {
count = count+subTask.join();
}
}
}
return count;
}
public static void main(String [] args){
try {
// 用一個 ForkJoinPool 實例調度“總任務”
ForkJoinPool pool = new ForkJoinPool();
//new一個ForkJoinTask的實例
SumDirsFiles task = new SumDirsFiles(new File("F:/"));
pool.invoke(task);//提交給ForkJoinPool執行Task
System.out.println("Task is Running......");
System.out.println("File counts ="+task.join());
System.out.println("Task end");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
CompletableFuture
基於JDK1.8之後纔有的特性,它提供了異步編排功能,性能更加優異.
本質
基於事件的監聽機制,不需要主動等待結果,而是一旦有結果,事件會主動通知的
優點
對於多個服務異步併發調用,再合併結果,不再阻塞主線程
多個服務併發調用,然後消費結果
對於多個服務,服務1執行完,再執行服務2,服務3,然後消費相關服務結果
方法
如果方法是以async結尾的,代表的是異步的調用,如果指定了線程池,會在線程中執行,如果沒有的話會在默認ForkJoinPool.commonPool中執行.
變換功能
Ø thenApply
Ø thenApplyAsync
public static void thenApply() { //hello world
String result = CompletableFuture.supplyAsync(() -> "hello").thenApply(s -> s + " world").join();
System.out.println(result);
}
結果消耗
Ø thenAccept
Ø thenAcceptAsync
public static void thenAccept(){ //hello world
CompletableFuture.supplyAsync(() -> "hello").thenAccept(s -> System.out.println(s+" world"));
}
執行下一個操作
Ø thenRun
Ø thenRunAsync
public static void thenRun(){//hello world
CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello";
}).thenRun(() -> System.out.println("hello world"));
while (true){}
}
合併操作
Ø thenCompoise
Ø thenCompoiseAsync
public static void thenCombine() { //hello world
String result = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello";
}).thenCombine(CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "world";
}), (s1, s2) -> s1 + " " + s2).join();
System.out.println(result);
}
計算誰快用誰
Ø applyToEither
Ø applyToEitherAsync
public static void applyToEither() {//返回hello world
String result = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "s1";
}).applyToEither(CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello world";
}), s -> s).join();
System.out.println(result);
}
返回一個completablefuture對象
Ø supplyAsync
Ø supply
結算結果完成是處理
Ø whenComplete
Ø whenCompleteAsync
public static void whenComplete() {
String result = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (1 == 1) {
throw new RuntimeException("測試一下異常情況");
}
return "s1";
}).whenComplete((s, t) -> {
System.out.println(s);
System.out.println(t.getMessage());
}).exceptionally(e -> {
System.out.println(e.getMessage());
return "hello world";
}).join();
System.out.println(result);
}
執行完所有方法
Ø allof
執行所有方法,才返回
private void method() throws Exception {
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "f1";
});
f1.whenCompleteAsync(new BiConsumer<String, Throwable>() {
@Override
public void accept(String s, Throwable throwable) {
System.out.println(System.currentTimeMillis() + ":" + s);
}
});
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "f2";
});
f2.whenCompleteAsync(new BiConsumer<String, Throwable>() {
@Override
public void accept(String s, Throwable throwable) {
System.out.println(System.currentTimeMillis() + ":" + s);
}
});
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2);
//阻塞,直到所有任務結束。
System.out.println(System.currentTimeMillis() + ":阻塞");
all.join();
System.out.println(System.currentTimeMillis() + ":阻塞結束");
//一個需要耗時2秒,一個需要耗時3秒,只有當最長的耗時3秒的完成後,纔會結束。
System.out.println("任務均已完成。");
}
Ø anyof
執行方法中的任何一個即可返回
static void anyOfExample() {
StringBuilder result = new StringBuilder();
List messages = Arrays.asList("a", "b", "c");
List<CompletableFuture> futures = messages.stream()
.map(msg -> CompletableFuture.completedFuture(msg).thenApply(s -> delayedUpperCase(s)))
.collect(Collectors.toList());
CompletableFuture.anyOf(futures.toArray(new CompletableFuture[futures.size()])).whenComplete((res, th) -> {
if(th == null) {
result.append(res);
}
});
}
AsyncLoad
隨着業務越來越複雜,對應的代碼也越來越複雜,耗時也越來越多,因此急需一套並行框架,通過搜索發現阿里提供了一個並行框架asyncLoad(https://github.com/alibaba/asyncload.git),但是此框架不支持註解的方式,使的對應的代碼侵入性很大,所以對asyncLoad框架進行擴展,使其能夠支持對應的註解配置的方式來進行異步並行。
https://blog.csdn.net/yujiak/article/details/80044428 基於擴展爲註解方式
第七章 併發容器
ConcurrentHashMap
數據結構
Sengment
HashEntry
鎖分段
常用方法
Ø put
Ø get
Ø putIfAbsent
ConcurrentSkipListMap
數據結構
跳躍鏈表
使用場景
一般開發者用到的比較少,只要在多線程排序的情況下可以使用,單線程針對key排序的可以使用treemap
常用方法
Ø subMap返回此映射的部分視圖,其鍵的範圍從 fromKey 到 toKey
Ø tailMap返回此映射的部分視圖,其鍵大於等於 fromKey。
Ø putIfAbsent 不存在在存儲
ConcurrentSkipListSet
TreeSet 單線程,而它是多線程,底層是基於ConcurrentSkipListMap
CopyOnWriteArrayList/Set
Copy-On-Write簡稱COW,是一種用於程序設計中的優化策略。其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,纔會真正把內容Copy出去形成一個新的內容然後再改,這是一種延時懶惰策略。從JDK1.5開始Java併發包裏提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的併發場景中使用到
什麼是CopyOnWrite容器
CopyOnWrite容器即寫時複製的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裏添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因爲當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
典型的用空間換時間,且如果是併發讀,有可能或讀取到舊的數據
使用場景,適合讀多寫少,比如白名單,黑名單,商品類目訪問
第八章 阻塞隊列
BlockingQueue
提供了線程安全的隊列訪問方式:當阻塞隊列進行插入數據時,如果隊列已滿,線程將會阻塞等待直到隊列非滿;從阻塞隊列取數據時,如果隊列已空,線程將會阻塞等待直到隊列非空
方法
插入
方法名 | 說明 |
---|---|
add | 拋出異常 |
offer | 返回特殊值 |
put | 方法阻塞 |
移除
方法名 | 說明 |
---|---|
remove | 拋出異常 |
take | 方法阻塞 |
poll | 返回特殊值 |
四組不同的行爲方式解釋:
拋異常:如果試圖的操作無法立即執行,拋一個異常。
特定值:如果試圖的操作無法立即執行,返回一個特定的值(常常是 true / false)。
阻塞:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行。
超時:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行,但等待時間不會超過給定值。返回一個特定值以告知該操作是否成功(典型的是true / false。
子類
ArrayBlockingQueue
數組結構,有界的,先進先出
LinkedBlockingQueue
鏈表結構,有界的,先進先出
PriorityBlockingQueue
支持優先級排序的,無界隊列
DelayQueue
支持延時獲取元素,比如 緩存訂單,延時支付,訂單到期
SynchronousQueue
不存儲元素的隊列,只有1個容量的大小
LinkedTransferQueue
鏈表結構,無界的
LinkedBlockingDeque
鏈表結構,雙向隊列
ConcurrentLinkedQueue
鏈表結構,無界的,線程安全隊列
代碼實例
隊列最典型的應用就是生產者和消費者的機制….
public class ThreadQueue03 {
public static void main(String[] args)throws Exception {
// 聲明一個容量爲10的緩存隊列
BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);
ProducerRunnable producer1 = new ProducerRunnable(queue);
ProducerRunnable producer2 = new ProducerRunnable(queue);
ProducerRunnable producer3 = new ProducerRunnable(queue);
ConsumerRunnable consumer = new ConsumerRunnable(queue);
// 藉助Executors
ExecutorService service = Executors.newCachedThreadPool();
// 啓動線程
service.execute(producer1);
service.execute(producer2);
service.execute(producer3);
service.execute(consumer);
// 執行10s
Thread.sleep(10 * 1000);
producer1.stop();
producer2.stop();
producer3.stop();
Thread.sleep(2000);
// 退出Executor
service.shutdown();
}
}
/**
* 生產線程
*/
class ProducerRunnable implements Runnable{
private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;
private BlockingQueue blockingQueue;
private volatile boolean isRunning = true;
private static AtomicInteger count = new AtomicInteger();
public ProducerRunnable(BlockingQueue blockingQueue){
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
String data = null;
Random r = new Random();
System.out.println("啓動生產者線程");
try {
while (isRunning){
System.out.println("正在生產數據");
Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));
data = "data:" + count.incrementAndGet();
System.out.println("將數據:" + data + "放入隊列...");
if (!blockingQueue.offer(data, 2, TimeUnit.SECONDS)) {
System.out.println("放入數據失敗:" + data);
}
}
}
catch (Exception e){
Thread.currentThread().interrupt();
}
finally {
System.out.println("退出生產者線程!");
}
}
public void stop() {
isRunning = false;
}
}
/**
* 消費線程
*/
class ConsumerRunnable implements Runnable{
private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;
private BlockingQueue blockingQueue;
volatile boolean isRunning = true;
public ConsumerRunnable(BlockingQueue blockingQueue){
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
System.out.println("啓動消費者線程!");
Random r = new Random();
try {
while (isRunning) {
System.out.println("正從隊列獲取數據...");
String data = blockingQueue.poll(2, TimeUnit.SECONDS).toString();
if (null != data) {
System.out.println("拿到數據:" + data);
System.out.println("正在消費數據:" + data);
Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));
} else {
// 超過2s還沒數據,認爲所有生產線程都已經退出,自動退出消費線程。
isRunning = false;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
} finally {
System.out.println("退出消費者線程!");
}
}
}
第九章 多線程性能調優
鎖優化
Ø 減少鎖的開銷
Ø 使用帶參數的鎖
Ø 減小鎖的粒度
Ø 減小臨界區的長度
Ø 無鎖化
上下文切換
Ø 控制線程數量
Ø 不要在臨界區執行阻塞IO操作
Ø 不要在臨界區執行比較耗時的操作
Ø 減少GC
寫多讀少且是緩存
在【超高併發】,【寫多讀少】,【定長value】的【業務緩存】場景下:
1) 可以通過水平拆分來降低鎖衝突 : 鎖分段
2) /**
* 給map進行分段加鎖
* @param <K>
* @param <V>
*/
public class SegmentLockMap<K,V> {
private final int LOCK_COUNT = 16;
private final Map<K,V> map;
private final Object[] locks ;
public SegmentLockMap() {
this.map = new HashMap<K,V>();
locks = new Object[LOCK_COUNT];
for (int i=0;i<LOCK_COUNT;i++){
locks[i] = new Object();
}
}
private int keyHashCode(K k){
return Math.abs(k.hashCode() % LOCK_COUNT);
}
public V get(K k){
int keyHashCode = keyHashCode(k);
//鎖定部分資源
synchronized (locks[keyHashCode % LOCK_COUNT]){
return map.get(k);
}
}
}
3)
2)可以通過Map轉Array的方式來最小化鎖衝突,一條記錄一個鎖 : 行級索
MAP變Array+最細鎖粒度優化
3)可以把鎖去掉,最大化併發,但帶來的數據完整性的破壞
4)可以通過簽名的方式保證數據的完整性,實現無鎖緩存 :無鎖
多線程基礎源碼 AQS
數據結構
雙向鏈表,它有區別於雙端鏈表(如果從尾部刪除的話,需要從頭到尾遍歷一遍,耗時間也耗空間),主要優勢就是可以解決從尾部刪除元素.