讀寫鎖
寫鎖:也叫獨佔鎖,一次只能被一個線程佔有。
讀鎖:也叫共享鎖,該鎖可以被多個線程佔有。
ReadWriteLock
,即讀寫鎖,正如它的名字一樣,它包含了讀鎖和寫鎖,一個用於只讀操作,一個用於寫入操作,我們先來看看JDK文檔中對它的說明。
創建一個讀寫鎖對象:
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
加讀鎖和解讀鎖:
readWriteLock.readLock().lock();
readWriteLock.readLock().unlock();
加寫鎖和解寫鎖:
readWriteLock.writeLock().lock();
readWriteLock.writeLock().unlock();
數據讀寫時可以使用讀寫鎖來保證線程安全,示例代碼如下:
package com.wunian.juc.rwlock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 獨佔鎖(寫鎖)一次只能被一個線程佔有
* 共享鎖(讀鎖)一個鎖可以被多個線程佔有
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacheLock myCache=new MyCacheLock();
//模擬線程
//寫
for (int i=1;i<=5;i++){
final int tempInt=i;
new Thread(()->{
myCache.put(tempInt+"",tempInt+"");
},String.valueOf(i)).start();
}
//讀
for (int i=1;i<=5;i++){
final int tempInt=i;
new Thread(()->{
myCache.get(tempInt+"");
},String.valueOf(i)).start();
}
}
}
//加鎖後的讀寫操作
class MyCacheLock{
private volatile Map<String,Object> map=new HashMap<>();
//讀寫鎖
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
//讀:可以被多個線程同時讀
public void get(String key){
//鎖一定要匹配,否則可能導致死鎖
readWriteLock.readLock().lock();//讀鎖,被多個線程同時持有
try {
System.out.println(Thread.currentThread().getName()+"讀取"+key);
Object o=map.get(key);
System.out.println(Thread.currentThread().getName()+"讀取結果"+o);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();//解鎖
}
}
//寫:應該保證原子性,不應該被打擾,寫線程寫入的過程中如果不加鎖,會被讀線程打擾
public void put(String key,Object value){
readWriteLock.writeLock().lock();//寫鎖,只能被一個線程佔有
try {
System.out.println(Thread.currentThread().getName()+"寫入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"寫入成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();//解鎖
}
}
}
阻塞隊列
隊列:是一種先進先出的數據結構。
棧:是一種後進先出的數據結構。
阻塞隊列是一種隊列,我們先來看看JDK文檔中對它的說明。
阻塞隊列在什麼情況下一定會被阻塞?
- 當隊列是滿的,如果還要往它裏面添加元素就會被阻塞。
- 當隊列是空的,如果還要取它裏面的元素就會被阻塞。
什麼時候使用阻塞隊列?
當編寫多線程程序時,對於線程之間的通信,不需要關心喚醒的情況下可以使用阻塞隊列。
阻塞隊列是新知識嗎?
List、Set這些集合類我們都學過,阻塞隊列和它們是一樣的,我們可以來看一張集合類的關係圖。
由上圖可知,BlockingQueue是Queue的子類,而Queue與List、Set一樣都是Collection類的子類。
ArrayBlockingQueue
是BlockingQueue的子類,它含有四組對元素的插入和獲取的API,我們可以用表格來對比一下。
方法 | 會拋出異常 | 返回布爾值,不會拋出異常 | 延時等待 | 一直等待 |
---|---|---|---|---|
插入 | add() | offer(e) | offer(e,time) | put() |
取出 | remove() | poll() | poll(time) | take() |
檢查 | element() | peek() | - | - |
四組API的示例代碼如下:
package com.wunian.juc.queue;
import com.sun.scenario.effect.impl.sw.java.JSWBlend_SRC_OUTPeer;
import java.sql.Time;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 阻塞隊列
*/
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue blockingQueue=new ArrayBlockingQueue(3);
//隊列已滿出現的四種情況:報錯、拋棄不報錯、一直等待、超時等待!
//java.lang.IllegalStateException: Queue full
blockingQueue.add("a");
blockingQueue.add("b");
blockingQueue.add("c");
//blockingQueue.add("d");//會拋出隊列已滿的異常
System.out.println(blockingQueue.element());//檢測第一個元素,輸出
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
//返回布爾值,不拋出異常
// System.out.println(blockingQueue.offer("a"));//true
// System.out.println(blockingQueue.offer("b"));//true
// System.out.println(blockingQueue.offer("c"));//true
// //嘗試等待三秒,三秒鐘後會失敗,返回false
// //System.out.println(blockingQueue.offer("d", 3L,TimeUnit.SECONDS));
// System.out.println(blockingQueue.peek());//檢測第一個元素,輸出
//
// System.out.println("================================");
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// //雖然是空的,還是會等待3秒,然後返回null
// System.out.println(blockingQueue.poll(3L, TimeUnit.SECONDS));
// blockingQueue.put("a");
// blockingQueue.put("b");
// blockingQueue.put("c");
// //System.out.println("準備放入第四個元素");
// //blockingQueue.put("d");//隊列滿了會一直等,並且會阻塞
//
// System.out.println("===============================");
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());//隊列空了會一直等,並且阻塞
}
}
同步隊列
SynchronousQueue
,即同步隊列,是一種特殊的阻塞隊列,因爲它只有一個容量,並且每進行一個put操作,就需要有一個take操作。
示例代碼如下:
package com.wunian.juc.queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* 同步隊列
* 只能存放一個值,一存一取(存一個必須取一個才能繼續存)
*/
public class SynchronuseQueueDemo {
public static void main(String[] args) {
//特殊的阻塞隊列
BlockingQueue<String> blockingQueue=new SynchronousQueue<>();
//存
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" put a");
blockingQueue.put("a");
System.out.println(Thread.currentThread().getName()+" put b");
blockingQueue.put("b");
System.out.println(Thread.currentThread().getName()+" put c");
blockingQueue.put("c");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
//取
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" "+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" "+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" "+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
}
}
線程池
線程池是一種多線程處理形式,處理過程中將任務添加到隊列,然後在創建線程後自動啓動這些任務。
爲什麼要使用線程池?
實現線程複用,線程複用可以提高線程的使用效率,保證內核的充分利用,防止過分調度。
線程池的三大方法
創建只有一個線程的線程池:
ExecutorService pool= Executors.newSingleThreadExecutor();
創建固定線程數的線程池:
ExecutorService pool=Executors.newFixedThreadPool(3);
創建可伸縮的線程池:
ExecutorService pool=Executors.newCachedThreadPool();
線程池的七大參數
我們先看看上面三大方法的源碼:
//可伸縮的線程池
new ThreadPoolExecutor(0, Integer.MAX_VALUE, // 約等於21億
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
//固定線程數的線程池
new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
//單線程的線程池
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
由以上代碼可以知道,三大方法的底層還是創建的ThreadPoolExecutor
對象,這也就是爲什麼阿里巴巴開發手冊要求不能使用三大方法,而必須直接通過創建ThreadPoolExecutor
對象來創建線程池的原因。
我們再來看看定義了七大參數的構造器源碼:
public ThreadPoolExecutor(int corePoolSize, // 核心池線程數大小 (常用)
int maximumPoolSize, // 最大的線程數大小 (常用)
// 超時等待時間,超過一定時間會把核心線程數以外的閒置線程關閉(常用)
long keepAliveTime,
TimeUnit unit, // 時間單位 (常用)
BlockingQueue<Runnable> workQueue, // 阻塞隊列(常用)
ThreadFactory threadFactory, // 線程工廠
RejectedExecutionHandler handler // 拒絕策略(常用)) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
由以上代碼可知,七大參數爲:
int corePoolSize
:核心池線程數,即線程池固定開啓的線程數量。int maximumPoolSize
:最大線程數,當核心池線程全部都在處理任務,阻塞隊列中的任務也已經滿了,一旦增加了新任務,線程池會開啓一個新的線程來處理,開啓的線程數量最多爲最大線程數,超過這個數量,將執行拒絕策略。long keepAliveTime
:超時等待時間,超過這個時間會將閒置的線程關閉,最後只保留核心池線程數量的線程。TimeUnit unit
:keepAliveTime參數的時間單位。BlockingQueue< Runnable > workQueue
:阻塞隊列,用來存放等待線程執行的任務。ThreadFactory threadFactory
:線程工廠,默認值:Executors.defaultThreadFactory()
RejectedExecutionHandler handler
:拒絕策略,當線程池中執行的線程數量達到最大線程數,新增的任務將根據拒絕策略進行處理。
除了ThreadFactory
這個參數使用的是系統默認的線程工廠外,其它六個參數都是必須掌握的。
四大拒絕策略
七大參數中的RejectedExecutionHandler
參數,指的就是拒絕策略,源碼中爲我們提供了四大拒絕策略。
ThreadPoolExecutor.AbortPolicy()
: 拋出異常,丟棄任務。ThreadPoolExecutor.DiscardPolicy()
:不拋出異常,丟棄任務。ThreadPoolExecutor.DiscardOldestPolicy()
: 嘗試獲取任務,不一定執行。ThreadPoolExecutor.CallerRunsPolicy()
:哪來的去哪裏找對應的線程執行。
最大線程數應該如何設置?
- CPU密集型:根據CPU的處理器數量來決定,這樣能夠保證最大效率。
獲取電腦CPU核數:Runtime.getRuntime().availableProcessors();
- IO密集型:例如有50個線程都是進程操作大IO資源,比較耗時,這時就要考慮最大線程數一定要大於這個常用IO的任務數,即最大線程數要大於50。
最終代碼如下:
package com.wunian.juc.threadpool;
import java.util.concurrent.*;
/**
* 線程池
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService pool=new ThreadPoolExecutor(
2,//核心線程數大小(常用)
//最大線程數大小(常用)
Runtime.getRuntime().availableProcessors(),//獲取電腦CPU核數
//超時等待時間(常用,超過一定時間會把核心線程數以外的閒置線程關閉)
3L,
//時間單位(常用)
TimeUnit.SECONDS,
//阻塞隊列(常用)
new LinkedBlockingDeque<>(3),
//線程工廠
Executors.defaultThreadFactory(),
//拒絕策略(常用,4種)
new ThreadPoolExecutor.CallerRunsPolicy()
);
try {
//線程池的使用方式
for(int i=1;i<=100;i++){
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//使用完畢後需要關閉!
pool.shutdown();
}
}
}
四個函數式接口
函數式接口在java.util.function
包下面,所有的函數式接口都可以用來簡化編程模型,都可以使用lambda表達式簡化。
必須掌握的四個函數式接口
Function
:有一個輸入參數,有一個輸出參數。Consumer
:有一個輸入參數,沒有輸出參數。Supplier
:沒有輸入參數,只有輸出參數。Predicate
:有一個輸入參數,判斷是否正確
lambda表達式語法格式可以簡單概括爲:(參數)->{方法體}
,示例代碼如下:
package com.wunian.juc.function;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* 函數式接口是現在必須掌握且精通的
*所有的函數式接口都可以使用lambda表達式簡化
* lambda表達式是java8必須掌握的
*
*/
public class FunctionInterfaceDemo {
public static void main(String[] args) {
/*Function<String,Integer> function=new Function<String,Integer>(){
@Override
public Integer apply(String s) {
return s.length();
}
};*/
//Function lambda表達式格式 (參數)->{方法體}
Function<String,Integer> function=(str)->{return str.length();};
System.out.println(function.apply("1234565"));
/*Predicate<String> predicate=new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
};*/
//Predicate lambda表達式格式 (參數)->{方法體}
Predicate<String> predicate=str->{return str.isEmpty();};
System.out.println(predicate.test("qqq"));
/*Supplier<String> supplier=new Supplier<String>() {
@Override
public String get() {
return "hello word";
}
};*/
//Supplier lambda表達式格式 (參數)->{方法體}
Supplier<String> supplier=()->{return "hello juc";};
System.out.println(supplier.get());
/*Consumer<String> consumer=new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};*/
//Consumer lambda表達式格式 (參數)->{方法體}
Consumer<String> consumer=(s)->{
System.out.println(s);
};
consumer.accept("hello consumer");
}
}
Stream流式計算
數據庫和集合都是用來存數據的,我們可以把計算和處理數據交給Stream。先來看看JDK文檔中對它的說明。
比如現在有一個集合,需要按條件用戶篩選以下條件:
1、id 爲偶數
2、年齡大於24
3、用戶名大寫 映射
4、用戶名倒排序
5、輸出一個用戶
並且只能用一行代碼完成!
使用Stream流式計算,這個問題很好處理,代碼如下:
package com.wunian.juc.stream;
import java.util.Arrays;
import java.util.List;
/**
* 流式計算
* 數據庫、集合:存數據的
*計算和處理數據交給Stream
*/
public class StreamDemo {
public static void main(String[] args) {
User u1=new User(1,"a",23);
User u2=new User(2,"b",20);
User u3=new User(3,"c",26);
User u4=new User(4,"d",30);
User u5 =new User(5,"e",21);
//存儲
List<User> users= Arrays.asList(u1,u2,u3,u4,u5);
//計算等操作交給流
//forEach(消費者類型接口)
users.stream()
.filter(u->{return u.getId()%2==0;})
.filter(u->{return u.getAge()>24;})
.map(u->{return u.getName().toUpperCase();})
.sorted((o1,o2)->{return o2.compareTo(o1);})
.limit(1)
.forEach(System.out::println);
}
}
ForkJoin分支合併
ForkJoin採用了分治算法思想,在必要的情況下,將一個大任務,進行拆分(fork) 成若干個子任務(拆到不能再拆,這裏就是指我們制定的拆分的臨界值),再將一個個小任務的結果進行join彙總。
MapReduce:input->split->map->reduce->output
主要就是兩步:
1、任務拆分
2、結果合併
前提
使用ForkJoin的前提是在大數據量的情況下,如果數據量很小,ForkJoin的效率還不如不用來的快。
ForkJoin的工作原理
假如兩個CPU上有不同的任務,這時候B已經執行完,A還有任務等待執行,這時候B就會將A隊尾的任務偷過來,加入自己的隊列中,這叫做工作竊取,ForkJoin的底層維護的是一個雙端隊列
好處:處理效率高。
壞處:可能產生資源爭奪。
我們可以使用求和計算測試一下ForkJoin,這就需要用到RecursiveTask
類了,先來看看它的JDK文檔。
先來創建一個類繼承RecursiveTask
,重寫其compute
方法,代碼如下:
package com.wunian.juc.forkjoin;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
/**
* 求和計算
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
private static final Long tempLong=10000L;//臨界值,只要超過了這個值,ForkJoin效率就會更高
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
//計算方法
@Override
protected Long compute() {
if((end-start)<=tempLong){
Long sum=0L;
for(Long i=start;i<=end;i++){
sum+=i;
}
return sum;
}else{//超過臨界值,用第二種方式
long middle=(end+start)/2;
//ForkJoin實際上是通過遞歸來實現
ForkJoinDemo right=new ForkJoinDemo(start,middle);
right.fork();//壓入線程隊列
ForkJoinDemo left=new ForkJoinDemo(middle+1,end);
left.fork();//壓入線程隊列
//獲得結果join,會阻塞等待結果
return right.join()+left.join();
}
}
}
創建一個測試類,分別測試普通方法、ForkJoin方法、並行流計算方法的計算效率,代碼如下:
package com.wunian.juc.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
/**
* 測試forkjoin
*/
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//test1();//6345
//test2();//13476
test3();//902
}
//普通方法
public static void test1() {
long sum=0L;
long start=System.currentTimeMillis();
for(Long i=0L;i<=10_0000_0000L;i++){
sum+=i;
}
long end=System.currentTimeMillis();
System.out.println("times:"+(end-start)+" rs=>:"+sum);
}
//forkjoin(計算大數據量時纔有效果,數據較小時可能還不如普通方法快)
public static void test2() throws ExecutionException, InterruptedException {
long start=System.currentTimeMillis();
ForkJoinPool forkJoinPool=new ForkJoinPool();
ForkJoinDemo forkJoinWork=new ForkJoinDemo(0L,10_0000_0000L);
ForkJoinTask<Long> submit=forkJoinPool.submit(forkJoinWork);
Long sum=submit.get();
long end=System.currentTimeMillis();
System.out.println("times:"+(end-start)+" rs=>:"+sum);
}
//並行流計算
public static void test3() {
long sum=0L;
long start=System.currentTimeMillis();
sum=LongStream.rangeClosed(0L,10_0000_0000L).parallel().reduce(0,Long::sum);//並行計算
long end=System.currentTimeMillis();
System.out.println("times:"+(end-start)+" rs=>:"+sum);
}
}
最後的測試結果表明,在計算數據量較大時,並行流計算方法是最快的,ForkJoin方法次之,普通方法最慢。在計算數據量很小時,ForkJoin方法和並行流計算方法反而比普通方法慢,再次驗證了使用ForkJoin的前提是在大數據量的情況下的這個結論。
異步回調
以往我們常常會使用callable來進行異步調用,但是callable沒有返回值。與之相比,Future可以有返回值,也可以沒有返回值。我們可以使用Future的子類completableFuture
來測試一下,代碼如下:
package com.wunian.juc.future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 異步回調 callable沒有返回值,使用Future
*
*/
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//沒有返回值
/* CompletableFuture<Void> completableFuture=CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"沒有返回值!");
});
System.out.println("111111");
completableFuture.get();*/
//有返回值
CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"=>supply Async!");
int i=10/0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t,u)->{
System.out.println("t=>"+t);//正確結果
System.out.println("u=>"+u);//錯誤信息
}).exceptionally(e->{//失敗,如果錯誤就返回錯誤的結果
System.out.println("e:"+e.getMessage());
return 500;
}).get());
}
}
由以上代碼我們可以知道,CompletableFuture不但有返回值,並且當異步調用過程中如果出現異常,連錯誤信息也會返回,這樣就可以很方便的做一些異常處理,顯然這點要比callable強大很多。