并发编程专题
并发和并行
目标都是最大化CPU的使用率
并行(parallel)
指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency)
指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执 行,使得在宏观上具有多个进程同时执行的
效果,但在微观上并不是同时执行的,只是把时间 分成若干段,使多个进程快速交替的执行。
并发三大特性
并发编程Bug的源头:可见性、原子性和有序性问题
可见性
当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java 内存模型是通过在变量 修改后将新值同步回
主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
如何保证可见性
- 通过 volatile 关键字保证可见性。
- 通过 内存屏障保证可见性。
- 通过 synchronized 关键字保证可见性。
- 通过 Lock保证可见性。
- 通过 final 关键字保证可见性
有序性
即程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题。
如何保证有序性
- 通过 volatile 关键字保证有序性。
- 通过 内存屏障保证有序性。
- 通过 synchronized 关键字保证有序性。
- 通过 Lock保证有序性。
原子性
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。
如何保证原子性
-
通过 synchronized 关键字保证原子性。
-
通过 Lock保证原子性。
-
通过 CAS保证原子性。
可见性问题深入分析
/**
*
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
* hsdis-amd64.dll
* 可见性案例
*/
public class VisibilityTest {
// storeLoad JVM内存屏障 ----> (汇编层面指令) lock; addl $0,0(%%rsp)
// lock前缀指令不是内存屏障的指令,但是有内存屏障的效果 缓存失效
private volatile boolean flag = true;
private Integer count = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
//TODO 业务逻辑
count++;
//JMM模型 内存模型: 线程间通信有关 共享内存模型
//没有跳出循环 可见性的问题
//能够跳出循环 内存屏障
//UnsafeFactory.getUnsafe().storeFence();
//能够跳出循环 ? 释放时间片,上下文切换 加载上下文:flag=true
//Thread.yield();
//能够跳出循环 内存屏障
//System.out.println(count);
//LockSupport.unpark(Thread.currentThread());
//shortWait(1000000); //1ms 本地内存flag失效
//shortWait(1000);
// try {
// Thread.sleep(1); //内存屏障
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//总结: Java中可见性如何保证? 方式归类有两种:
//1. jvm层面 storeLoad内存屏障 ===> x86 lock替代了mfence
// 2. 上下文切换 Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB通过flag控制threadA的执行时间
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
未加volatile时可见性分析: ThreadA把flag=true从主内存加载到本地内存之后,ThreadB修改flag=false,刷新回主内存(非实时,在某个时间才会刷回),ThreadA由于while(flag)优先级比较高(可理解为需要实时用到flag,减少性能损耗),优先从本地内存中找flag,命中为true则直接返回
加volatile时可见性分析:ThreadB修改完flag=false会把结果实时刷回主内存,同时ThreadA中本地内存的变量副本失效,需要从主内存重新读取
总结:保证线程可见性可归结为两种方式
- Thread.yield() 线程上下文切换,释放时间片,当前线程执行时间耗尽会保留现场(数据刷新回主内存,同时本地内存数据失效),当再次竞争获取cpu资源执行时,会重新从主内存读取,通过程序计数器定位到接下来需要执行的指令,从而保证可见性
- 内存屏障 (volatile、synchronized 、lock、Unsafe().storeFence()、System打印语句、LockSupport.unpark(Thread.currentThread())本质上都是内存屏障),在linux系统x86中的实现通过storeload利用lock前缀指令. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议(MESI协议),刷新store buffer的操作会导致其他cache中的副本失效。
原子性问题深入分析
public class Test {
private volatile static int sum = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(()->{
for (int j = 0; j < 10000; j++) {
sum++;
}
});
thread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sum);
}
}
十个线程每个线程把sum(初始值为0)值加一万,结果执行结果小于十万?
分析:假如主内存sum=5,ThreadA、ThreadB变量副本也为5,由于sum被volatile修饰,当ThreadA修改sum=6还没有回写刷新主内存,ThreadB中sum=5也已经加载到cpu寄存器中参与sum++运算,ThreadA中sum=6刷新回主内存时,根据MESI缓存一致性协议ThreadB中sum++运算结果失效,因此执行结果小于十万
有序行问题深入分析
public class SingletonFactory {
private static /*volatile*/ SingletonFactory myInstance;
public static SingletonFactory getMyInstance() {
if (myInstance == null) {
synchronized (SingletonFactory.class) {
if (myInstance == null) {
// 1. 开辟一片内存空间
// 3. myInstance指向内存空间的地址
// 2. 对象初始化
myInstance = new SingletonFactory();
}
}
}
return myInstance;
}
public static void main(String[] args) {
SingletonFactory.getMyInstance();
}
}
分析:双端检索单例模式情况下,new SingletonFactory()创建对象可以分为三步,如果没有volatile修饰2,3步指令重排,假如ThreadA执行到第3步还未初始化对象,ThreadB判断myInstance == null为false(ThreadA已经分配了myInstance指向内存空间的地址),直接返回则出现问题!
public class ReOrderTest {
private static int x = 0, y = 0;
private volatile static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i=0;
while (true) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
/**
* x,y: 00, 10, 01, 11
*/
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
shortWait(20000);
a = 1; // volatile写
// StoreLoad
//UnsafeFactory.getUnsafe().storeFence();
x = b; // volatile读
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
//UnsafeFactory.getUnsafe().storeFence();
y = a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("第" + i + "次(" + x + "," + y + ")");
if (x==0&&y==0){
break;
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
分析:如果没有volatile修饰则指令重排序可能会出现x,y == 0的情况,volatile和Unsafe().storeFence()效果相同,相当于在线程写读中间加入屏障,写读之间的顺序不可以重排,多线程所有对于内存屏障后的读操作对于写操作是可见的,所以禁止重排序也是为了可见性
缓存一致性(Cache coherence)
在共享内存多处理器系统中,每个处理器都有一个单独的缓存内存,共享数据可能有多个副本:一个副本在主内存中,一个副本在请求它的每个处理器的本地缓存中。当数据的一个副本发生更改时,其他副本必须反映该更改。存一致性是确保共享操作数(数据)值的变化能够及时地在整个系统中传播的规程。
一致性协议在多处理器系统中应用于高速缓存一致性。为了保持一致性,人们设计了各种模型和协议,如MSI、MESI(又名Illinois)、MOSI、MOESI、MERSI、MESIF、write-once、Synapse、Berkeley、Firefly和Dragon协议。
MESI协议
是一个基于写失效的缓存一致性协议,是支持回写(write-back)缓存的最常用协议。
已修改Modified (M) (某个线程修改了共享变量副本)
独占Exclusive (E) (单独一个线程拥有该共享变量副本)
共享Shared (S) (多个线程拥有该共享变量副本)
无效Invalid (I) (一个线程修改了本地内存变量副本后,其他线程的变量副本将会失效)
线程池
线程池和线程对比
/***
* 使用线程的方式去执行程序
*/
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Long start = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 100000; i++) {
Thread thread = new Thread() {
@Override
public void run() {
list.add(random.nextInt());
}
};
thread.start();
thread.join();
}
System.out.println("时间:" + (System.currentTimeMillis() - start));
System.out.println("大小:" + list.size());
}
}
/***
* 线程池执行
*/
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
Long start = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<Integer>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
list.add(random.nextInt());
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
System.out.println("时间:"+(System.currentTimeMillis() - start));
System.out.println("大小:"+list.size());
}
}
思考:为什么开启十万个线程执行上述任务和用单例线程池执行的时间差这么多?
几种不同的线程池
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executorService1 = Executors.newCachedThreadPool();//快
ExecutorService executorService2 = Executors.newFixedThreadPool(10);//慢
ExecutorService executorService3 = Executors.newSingleThreadExecutor();//最慢
RejectedExecutionHandler rejectedExecutionHandler= new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 写数据库的代码
}
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10),new MonkeyRejectedExecutionHandler());//自定义线程
for (int i = 1; i <= 100; i++) {
threadPoolExecutor.execute(new MyTask(i));
}
}
}
/***
* 项目
*/
class MyTask implements Runnable {
int i = 0;
public MyTask(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "程序员做第" + i + "个项目");
try {
Thread.sleep(3000L);//业务逻辑
} catch (Exception e) {
e.printStackTrace();
}
}
}
ThreadPoolExecutor execute方法源码分析
int c = ctl.get();
/*判断当前的线程数是否小于corePoolSize如果是,使用入参任务通过addWord方法创建一个新的线程,
如果能完成新线程创建exexute方法结束,成功提交任务;*/
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
/*在第一步没有完成任务提交;状态为运行并且能成功加入任务到工作队列后,再进行一次check,如果状态在任务加入队列后变为了非运行(有可能是在执行到这里线程池shutdown了)非运行状态下当然是需要reject;然后再判断当前线程数是否为0(有可能这个时候线程数变为了0),如是,新增一个线程*/
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
/*如果不能加入任务到工作队列,将尝试使用任务新增一个线程,如果失败,则是线程池已经shutdown或者线程池已经达到饱和状态,所以reject;从上面新增任务的execute方法也可以看出,拒绝策略不仅仅是在饱和状态下使用,在线程池进入到关闭阶段同样需要使用到;*/
else if (!addWorker(command, false))
reject(command);
线程池拒绝策略
ThreadPoolExecutor内部有实现4个拒绝策略:
- CallerRunsPolicy,由调用execute方法提交任务的线程来执行这个任务;
- AbortPolicy,抛出异常RejectedExecutionException拒绝提交任务;
- DiscardPolicy,直接抛弃任务,不做任何处理;
- DiscardOldestPolicy,去除任务队列中的第一个任务(最旧的),重新提交;
并不推荐使用默认线程池,比如FixedThreadPool、SingleThreadExecutor阻塞队列为LinkedBlockingQueue为无界队列、CachedThreadPool最大线程数为Integer.MAX_VALUE可能会导致OOM,而且定长线程池比如FixedThreadPool在分治算法思想ForkJoin很可能会导致饥饿线程死锁问题!自定义线程池时除了合理分配线程数量,最好自定义线程工厂(创建新线程的线程名称根据业务类型相关联,生产出问题方便快速定位)和拒绝策略(比如拒绝的任务入库操作)
ScheduledThreadPoolExecutor
schedule:延迟多长时间之后只执行一次;
scheduledAtFixedRate固定:延迟指定时间后执行一次,之后按照固定的时长周期执行;
scheduledWithFixedDelay非固定:延迟指定时间后执行一次,之后按照:上一次任务执行时
长 + 周期的时长 的时间去周期执行;
schedule方法会把任务放入DelayedWorkQueue中,由ReentrantLock和condition保证线程安全和等待唤醒
深入理解java线程
线程的同步互斥
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当 它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。
四种线程同步互斥的控制方法
- 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访 问。(在一段时间内只允许一个线程访问的资源就称为临界资源)。
- 互斥量:为协调共同对一个共享资源的单独访问而设计的。
- 信号量:为控制一个具有有限数量用户资源而设计。
- 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
上下文切换
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:
-
暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
-
从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
-
返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
上下文切换只能在内核模式下发生。内核模式是CPU的特权模式,其中只有内核运行,并提供对 所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但 它们可以通过系统调用运行部分内核代码。上下文切换通常是计算密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本, 实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是尽可能地避免不必要的上下文切换。
查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p 查看某个进程(PID)的所有线程
Java
- jps 命令查看所有 Java 进程
- jstack 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
Java线程的生命周期
Java 语言中线程共有六种状态,分别是:
-
NEW(初始化状态)
-
RUNNABLE(可运行状态+运行状态)
-
BLOCKED(阻塞状态)
-
WAITING(无时限等待)
-
TIMED_WAITING(有时限等待)
-
TERMINATED(终止状态)
Thread常用方法
sleep方法
- 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
- sleep当传入参数为0时,和yield相同
yield方法
- yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高 (至少是相同)的线程获得执行机会,不会释放对象锁;
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比 它优先级更高的线程;
- 具体的实现依赖于操作系统的任务调度器
join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之 后才能继续运行的场景。
public class ThreadJoinDemo {
public static void main(String[] sure) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t begin");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t finished");
}
});
long start = System.currentTimeMillis();
t.start();
//主线程等待线程t执行完成
t.join();
System.out.println("执行时间:" + (System.currentTimeMillis() - start));
System.out.println("Main finished");
}
}
思考:如何正确优雅的停止线程?
stop方法
stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。
public class ThreadStopDemo {
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "执行完成");
}
});
thread.start();
Thread.sleep(2000);
// 停止thread,并释放锁
thread.stop();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "等待获取锁");
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
}
}
}).start();
}
}
stop会释放对象锁,可能会造成数据不一致。
Java线程的中断机制
Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一 种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。
API的使用
- interrupt();将线程的中断标志位设置为true,不会停止线程
- isInterrupted(); 判断当前线程的中断标志位是否为true,不会清除中断标志位
- Thread.interrupted();判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
利用中断机制优雅的停止线程
public class ThreadStopDemo2 implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
//需要重新设置线程中断状态为true
Thread.currentThread().interrupt();
}
}
System.out.println("线程停止: stop thread");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadStopDemo2());
thread.start();
Thread.sleep(5);
thread.interrupt();
}
}
注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况(如果catch中捕获到中断异常,会清除中断标记位,需要在catch代码块重新设置中断标记为true)
处于休眠中的线程被中断(sleep和wait),线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。如果不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。
Java线程间通信
等待唤醒(等待通知)机制
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法, 线程将进入等待队列进行等待直到被唤醒。
public class WaitDemo {
private static Object lock = new Object();
private static boolean flag = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
while (flag){
try {
System.out.println("wait start .......");
//等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait end ....... ");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
if (flag){
synchronized (lock){
if (flag){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//通知
lock.notifyAll();
System.out.println("notify .......");
flag = false;
}
}
}
}
}).start();
}
}
使用wait,notify来实现等待唤醒功能至少有两个缺点:
-
wait和notify都是Object中的方法,在调用这两个方法前必须先获 得锁对象,这限制了其使用场合:只能在同步代码块中。
-
当对象的等待队列中有多个线程时,notify只能随机选择一个线程唤醒,无法唤醒指定的线程
使用LockSupport的话,我们可以在任何场合使线程阻塞,同时也可以指 定要唤醒的线程,相当的方便。LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用 unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒。
import java.util.concurrent.locks.LockSupport;
public class LockSupportTest {
public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("唤醒parkThread");
//为指定线程parkThread提供“许可”
LockSupport.unpark(parkThread);
}
static class ParkThread implements Runnable{
@Override
public void run() {
System.out.println("ParkThread开始执行");
// 等待“许可”
LockSupport.park();
System.out.println("ParkThread执行完成");
}
}
}
import java.util.concurrent.locks.LockSupport;
public class ParkUnparkDemo {
//包子铺对象
volatile Object bozipu = null;
//资源锁
Object baozi_lock = new Object();
public static void main(String[] args) throws Exception{
System.out.println("主线程开始运行");
new ParkUnparkDemo().parkUnparkTest();
}
/**
* 测试park/unpark方法的使用
*/
public void parkUnparkTest() throws Exception {
//开启消费者线程1:等待包子铺有包子之后,进行消费买包子
Thread consumer =new Thread(() -> {
//如果包子铺没有开业,则等待包子铺开业的"许可"
System.out.println("等待包子铺开张。。。");
//这里使用while进行是否被唤醒,不要使用if,因为会有伪唤醒的状态
while (bozipu == null){
LockSupport.park();
System.out.println("包子已经买到,可以回家了!");
}
});
consumer.start();
//等待3秒钟开始创建包子铺
Thread.sleep(3000);
bozipu = new Object();
//给线程consummer颁发许可
LockSupport.unpark(consumer);
System.out.println("已经通知消费者包子铺开张");
}
}
LockSupport就是通过控制变量_counter来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。
-
当调用park()方法时,会将_counter置为0,同时判断前值,小于1说明前面被 unpark过,则直接退出,否则将使该线程阻塞。
-
当调用unpark()方法时,会将_counter置为1,同时判断前值,小于1会进行线 程唤醒,否则直接退出。
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法 时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。
CAS
什么是 CAS
CAS(Compare And Swap,比较并交换),通常指的是这样一种原子操作:针对一个变量,首 先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值。伪代码:
if (value == expectedValue) {
value = newValue;
}
以上伪代码描述了一个由比较和赋值两阶段组成的复合操作,CAS 可以看作是它们合并后的整体 ——一个不可分割的原子操作,并且其原子性是直接在硬件层面得到保障的。 CAS可以看做是乐观锁(对比数据库的悲观、乐观锁)的一种实现方式,Java原子类中的递增操 作就通过CAS自旋实现的。 CAS是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
CAS应用
CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作
它们都是 native 方法,由 Java 虚拟机提供具体实现,这意味着不同的 Java 虚拟机对它们的实现可能会略有不同。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
以 compareAndSwapInt 为例,Unsafe 的 compareAndSwapInt 方法接收 4 个参数,分别是:对象实例、内存偏移量、字段期望值、字段新值。该方法会针对指定对象实例中的相应偏移量的字段执行 CAS 操作。
CAS源码分析
Hotspot 虚拟机对compareAndSwapInt 方法的实现如下
#unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jo
bject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 根据偏移量,计算value的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// Atomic::cmpxchg(x, addr, e) cas逻辑 x:要交换的值 e:要比较的值
//cas成功,返回期望值e,等于e,此方法返回true
//cas失败,返回内存中的value值,不等于e,此方法返回false
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。
CAS缺陷
CAS 虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:
-
自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销
-
只能保证一个共享变量原子操作
-
ABA 问题
Atomic原子操作类介绍
在J.U.C下的atomic包提供了一系列的操作简单, 性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的 字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用 CAS操作具体实现。
AtomicIntegerArray为例总结常用的方法
//addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加;
public final int addAndGet(int i, int delta) {
return getAndAdd(i, delta) + delta;
}
//getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;
public final int getAndIncrement(int i) {
return getAndAdd(i, 1);
}
//compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
public class AtomicIntegerArrayTest {
static int[] value = new int[]{ 1, 2, 3, 4, 5 };
static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
public static void main(String[] args) throws InterruptedException {
//设置索引0的元素为100
atomicIntegerArray.set(0, 100);
System.out.println(atomicIntegerArray.get(0));
//以原子更新的方式将数组中索引为1的元素与输入值相加
atomicIntegerArray.getAndAdd(1,5);
System.out.println(atomicIntegerArray);
}
}
原子更新引用类型
AtomicReference作用是对普通对象的封装,它可以保证你在修改对象引用时的线程安全性。
public class AtomicReferenceTest {
public static void main( String[] args ) {
User user1 = new User("张三", 23);
User user2 = new User("李四", 25);
User user3 = new User("王五", 20);
//初始化为 user1
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println("初始值:" + atomicReference.get());
new Thread(() -> {
//把 user2 赋给 atomicReference
boolean b = atomicReference.compareAndSet(user1, user2);
System.out.println(Thread.currentThread().getName() + "修改" + b + "最终结果:" +atomicReference.get());
},"Thread1").start();
new Thread(() -> {
//把 user3 赋给 atomicReference
boolean b = atomicReference.compareAndSet(user1, user3);
System.out.println(Thread.currentThread().getName() + "修改" + b + "最终结果:" +atomicReference.get());
},"Thread2").start();
}
}
@Data
@AllArgsConstructor
class User {
private String name;
private Integer age;
}
LongAdder/DoubleAdder
高并发环境下, N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong的自旋会成为
瓶颈。 这就是LongAdder引入的初衷——解决高并发环境下AtomicInteger, AtomicLong的自旋瓶颈问题。
public class LongAdderTest {
public static void main(String[] args) {
/* testAtomicLongVSLongAdder(10, 10000);
System.out.println("==================");
testAtomicLongVSLongAdder(10, 200000);
System.out.println("==================");*/
testAtomicLongVSLongAdder(100, 200000);
}
static void testAtomicLongVSLongAdder(final int threadCount, final int times) {
try {
long start = System.currentTimeMillis();
testLongAdder(threadCount, times);
long end = System.currentTimeMillis() - start;
System.out.println("条件>>>>>>线程数:" + threadCount + ", 单线程操作计数" + times);
System.out.println("结果>>>>>>LongAdder方式增加计数" + (threadCount * times) + "次,共计耗时:" + end);
long start2 = System.currentTimeMillis();
testAtomicLong(threadCount, times);
long end2 = System.currentTimeMillis() - start2;
System.out.println("条件>>>>>>线程数:" + threadCount + ", 单线程操作计数" + times);
System.out.println("结果>>>>>>AtomicLong方式增加计数" + (threadCount * times) + "次,共计耗时:" + end2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static void testAtomicLong(final int threadCount, final int times) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
AtomicLong atomicLong = new AtomicLong();
for (int i = 0; i < threadCount; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < times; j++) {
atomicLong.incrementAndGet();
}
countDownLatch.countDown();
}
}, "my-thread" + i).start();
}
countDownLatch.await();
}
static void testLongAdder(final int threadCount, final int times) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
LongAdder longAdder = new LongAdder();
for (int i = 0; i < threadCount; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < times; j++) {
longAdder.add(1);
}
countDownLatch.countDown();
}
}, "my-thread" + i).start();
}
countDownLatch.await();
}
}
LongAdder原理
设计思路
AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
什么是ABA问题
当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改成功。
@Slf4j
public class ABATest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
new Thread(()->{
int value = atomicInteger.get();
log.debug("Thread1 read value: " + value);
// 阻塞1s
LockSupport.parkNanos(1000000000L);
// Thread1通过CAS修改value值为3
if (atomicInteger.compareAndSet(value, 3)) {
log.debug("Thread1 update from " + value + " to 3");
} else {
log.debug("Thread1 update fail!");
}
},"Thread1").start();
new Thread(()->{
int value = atomicInteger.get();
log.debug("Thread2 read value: " + value);
// Thread2通过CAS修改value值为2
if (atomicInteger.compareAndSet(value, 2)) {
log.debug("Thread2 update from " + value + " to 2");
// do something
value = atomicInteger.get();
log.debug("Thread2 read value: " + value);
// Thread2通过CAS修改value值为1
if (atomicInteger.compareAndSet(value, 1)) {
log.debug("Thread2 update from " + value + " to 1");
}
}
},"Thread2").start();
}
}
ABA问题的解决方案
数据库有个锁称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加。 Java也提供了相应的原子引用类AtomicStampedReference
synchronized
synchronized底层原理
synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语 Mutex(互斥量),它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的 优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操 作的开销,内置锁的并发性能已经基本与Lock持平。
Monitor(管程/监视器)
管程是指管理共享变 量以及对共享变量操作的过程,让它们支持并发。
AQS
什么是AQS
java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于 AbstractQueuedSynchronizer(简称AQS) 实现的,AQS是一个抽象同步框架,可以用来实 现一个依赖状态的同步器。 JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的。
注:AQS已经默认实现了条件、等待队列的入队出队操作,如果需要实现一把锁只需实现获取或释放独占、共享资源的方法即可
AQS具备的特性:
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
AQS内部维护属性volatile int state
- state表示资源的可用状态
AQS定义两种资源共享方式
- Exclusive-独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
AQS定义两种队列
- 同步等待队列: 主要用于维护获取锁失败时入队的线程
- 条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用 signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁
AQS 定义了5个队列中节点状态:
-
值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
-
CANCELLED,值为1,表示当前的线程被取消;
-
SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
-
CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
-
PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
自定义同步器在实现时只需要实现共享 资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现 它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
同步等待队列
基于双向链表数据结构的队列,是先进先出线程等待队列
AQS 依赖CLH同步队列来完成同步状态的管理:
-
当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造 成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
-
当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
-
通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)
条件等待队列
AQS中条件队列是使用单向列表保存的,用nextWaiter来连接:
- 调用await方法阻塞线程;
- 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)
等待唤醒机制之await/signal测试
@Slf4j
public class ConditionTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
log.debug(Thread.currentThread().getName() + " 开始处理任务");
//会释放当前持有的锁,然后阻塞当前线程
condition.await();
log.debug(Thread.currentThread().getName() + " 结束处理任务");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
lock.lock();
try {
log.debug(Thread.currentThread().getName() + " 开始处理任务");
Thread.sleep(2000);
//唤醒因调用Condition#await方法而阻塞的线程
condition.signal();
log.debug(Thread.currentThread().getName() + " 结束处理任务");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
}
注意:调用Condition的await()和signal()方法,都必须在lock保护之内。
ReentrantLock
ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手 段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。
相对于 synchronized, ReentrantLock具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
总结了几点synchronized和ReentrantLock的区别:
- synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
- synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
- synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
- synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以 被中断的;
- 在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally 块中显示释放锁;
- ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
- synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;
ReentrantLock源码分析
final void lock() { //加锁
if (compareAndSetState(0, 1))
//通过cas抢占锁(设置state),成功则把当前线程设置为持有线程
setExclusiveOwnerThread(Thread.currentThread());
else
//
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
//获取锁失败 再次尝试获取,失败则准备入队
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);//构建队列 入队操作
return node;
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
//park阻塞线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
public final boolean release(int arg) { //arg=1
if (tryRelease(arg)) { //尝试解锁
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒同步等待队列头节点线程 unpark
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//判断当前线程和持有锁线程是否为同一线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//重新设置当前持有锁线程为null
free = true;
setExclusiveOwnerThread(null);
}
//设置锁状态为0
setState(c);
return free;
}
开启三个线程模拟抢占锁,IDEA debug模式设置为Thread,控制线程执行顺序主线流程如下
Semaphore
Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的。
常用方法
构造器
permits 表示许可证的数量(资源数)
- acquire() 表示阻塞并获取许可
- tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞
- release() 表示释放许可
- int availablePermits():返回此信号量中当前可用的许可证数。
- int getQueueLength():返回正在等待获取许可证的线程数。
- boolean hasQueuedThreads():是否有线程正在等待获取许可证。
- void reducePermit(
- int reduction):减少 reduction 个许可证
- Collection getQueuedThreads():返回所有等待获取许可证的线程集合
可以用于做流量控制,特别是公用资源有限的应用场景
public class SemaphoneTest {
/**
* 实现一个同时只能处理5个请求的限流器
*/
private static Semaphore semaphore = new Semaphore(5);
/**
* 定义一个线程池
*/
private static ThreadPoolExecutor executor = new ThreadPoolExecutor
(10, 50, 60,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(200));
/**
* 模拟执行方法
*/
public static void exec() {
try {
//占用1个资源
semaphore.acquire(1);
//TODO 模拟业务执行
System.out.println("执行exec方法");
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放一个资源
semaphore.release(1);
}
}
public static void main(String[] args) throws InterruptedException {
{
for (; ; ) {
Thread.sleep(100);
// 模拟请求以10个/s的速度
executor.execute(() -> exec());
}
}
}
}
Semaphore源码分析
获取资源acquire
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
} else {
if (this.tryAcquireShared(arg) < 0) {
this.doAcquireSharedInterruptibly(arg);
}
}
}
//获取资源cas设置剩余资源 返回剩余资源如果没有资源返回负数
final int nonfairTryAcquireShared(int acquires) {
int available;
int remaining;
do {
available = this.getState();
remaining = available - acquires;
} while(remaining >= 0 && !this.compareAndSetState(available, remaining));
return remaining;
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//入队
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//尝试获取资源
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//修改前驱节点可唤醒状态
if (shouldParkAfterFailedAcquire(p, node) &&
//park阻塞
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
释放资源release
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//cas操作把state加上当前释放的数量
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {//修改为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒
LockSupport.unpark(s.thread);
}
5个线程3个资源模拟
CountDownLatch
CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置。如果你需要一个重置count的版本,那么请考虑使用CyclicBarrier。
构造器
常用方法
// 调用 await() 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
public void await() throws InterruptedException { };
// 和 await() 类似,若等待 timeout 时长后,count 值还是没有变为 0,不再等待,继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
// 会将 count 减 1,直至为 0
public void countDown() { };
应用场景
场景1 让多个线程等待:模拟并发,让并发线程一起执行
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
//准备完毕……运动员都阻塞在这,等待号令
countDownLatch.await();
String parter = "【" + Thread.currentThread().getName() + "】";
System.out.println(parter + "开始执行……");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);// 裁判准备发令
System.out.println("发令countDown");
countDownLatch.countDown();// 发令枪:执行发令
}
}
场景2 让单个线程等待:多个线程(任务)完成后,进行汇总合并
很多时候,我们的并发任务,存在前后依赖关系;比如数据详情页需要同时调用多个接口获取数据,并发请求获取到数据后、需要进行结果合并;或者多个数据操作完成后,需要数据check;这其实都是:在多个线程(任务)完成后,进行汇总合并的场景。
public class CountDownLatchTest2 {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(() -> {
try {
Thread.sleep(1000 +
ThreadLocalRandom.current().nextInt(1000));
System.out.println(Thread.currentThread().getName()
+ " finish task" + index);
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
countDownLatch.await();
System.out.println("主线程:在所有任务运行完成后,进行结果汇总");
}
}
CountDownLatch实现原理
底层基于 AbstractQueuedSynchronizer 实现,CountDownLatch 构造函数中指定的count直接赋给AQS的state;每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;这一步是由最后一个执行countdown方法的线程执行的。
而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。
CyclicBarrier
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
CyclicBarrier的使用
// parties表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
public CyclicBarrier(int parties)
// 用于在线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景(该线程的执行时机是在到达屏障之后再执行)
public CyclicBarrier(int parties, Runnable barrierAction)
重要方法
//屏障 指定数量的线程全部调用await()方法时,这些线程不再阻塞
// BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
//循环 通过reset()方法可以进行重置
public void reset()
CyclicBarrier应用场景
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的场景。
public class CyclicBarrierTest2 {
//保存每个学生的平均成绩
private ConcurrentHashMap<String, Integer> map=new ConcurrentHashMap<String,Integer>();
private ExecutorService threadPool= Executors.newFixedThreadPool(3);
private CyclicBarrier cb=new CyclicBarrier(3,()->{
int result=0;
Set<String> set = map.keySet();
for(String s:set){
result+=map.get(s);
}
System.out.println("三人平均成绩为:"+(result/3)+"分");
});
public void count(){
for(int i=0;i<3;i++){
threadPool.execute(new Runnable(){
@Override
public void run() {
//获取学生平均成绩
int score=(int)(Math.random()*40+60);
map.put(Thread.currentThread().getName(), score);
System.out.println(Thread.currentThread().getName()
+"同学的平均成绩为:"+score);
try {
//执行完运行await(),等待所有学生平均成绩都计算完毕
cb.await();
System.out.println(Thread.currentThread().getName() + "当前线程执行完毕");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
}
}
public static void main(String[] args) {
CyclicBarrierTest2 cb=new CyclicBarrierTest2();
cb.count();
}
}
计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景
public class CyclicBarrierTest3 {
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 5, 1000, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
(r) -> new Thread(r, counter.addAndGet(1) + " 号 "),
new ThreadPoolExecutor.AbortPolicy());
CyclicBarrier cyclicBarrier = new CyclicBarrier(5,
() -> System.out.println("裁判:比赛开始~~"));
for (int i = 0; i < 10; i++) {
threadPoolExecutor.submit(new Runner(cyclicBarrier));
}
}
static class Runner extends Thread{
private CyclicBarrier cyclicBarrier;
public Runner (CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
int sleepMills = ThreadLocalRandom.current().nextInt(1000);
Thread.sleep(sleepMills);
System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms" + cyclicBarrier.getNumberWaiting());
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
}
}
}
CyclicBarrier与CountDownLatch的区别
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset() 方法重置。
- CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现
CyclicBarrier源码分析
ReentrantReadWriteLock
ReentrantReadWriteLock类结构
ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也就是说,写锁是独占的,读锁是共享的。
如何使用读写锁
//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
private Object data = null;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public void get() {
log.debug(Thread.currentThread().getName() + " be ready to read data!");
//上读锁,其他线程只能读不能写
readLock.lock();
try {
Thread.sleep(1000);
log.debug(Thread.currentThread().getName() + " have read data :" + data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
readLock.unlock();
}
}
public void put(Object data) {
log.debug(Thread.currentThread().getName() + " be ready to write data!");
//上写锁,不允许其他线程读也不允许写
writeLock.lock();
try {
Thread.sleep(5000);
this.data = data;
log.debug(Thread.currentThread().getName() + " have write data: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
writeLock.unlock();
}
}
注意事项
- 读锁不支持条件变量
- 重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
- 重入时支持降级: 持有写锁的情况下可以去获取读锁
ReentrantReadWriteLock适合读多写少的场景
public class Cache {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
// 获取一个key对应的value
public static final Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
// 设置key对应的value,并返回旧的value
public static final Object put(String key, Object value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
// 清空所有的内容
public static final void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
}
上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而 只有写锁被释放之后,其他读写操作才能继续。Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性
锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。
锁降级的使用示例
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
private volatile boolean update = false;
public void processData() {
readLock.lock();
if (!update) {
// 必须先释放读锁
readLock.unlock();
// 锁降级从写锁获取到开始
writeLock.lock();
try {
if (!update) {
// TODO 准备数据的流程(略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
// 锁降级完成,写锁降级为读锁
}
try {
//TODO 使用数据的流程(略)
} finally {
readLock.unlock();
}
}
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
ReentrantReadWriteLock源码分析
设计的精髓:用一个变量如何维护多种状态
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。
通过位运算。假如当前同步状态为S,
那么:
- 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
- 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16),也就是S+0x00010000
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状
态(S>>>16)大于0,即读锁已被获取。
写锁的获取
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
写锁的获取是通过重写AQS中的tryAcquire方法实现的
protected final boolean tryAcquire(int acquires) {
//当前线程
Thread current = Thread.currentThread();
//获取state状态 存在读锁或者写锁,状态就不为0
int c = getState();
//获取写锁的重入数
int w = exclusiveCount(c);
//当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
if (c != 0) {
// c!=0 && w==0 表示存在读锁
// 当前存在读锁或者写锁已经被其他写线程获取,则写锁获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 超出最大范围 65535
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//同步state状态
setState(c + acquires);
return true;
}
// writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁
//c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置写锁为当前线程所有
setExclusiveOwnerThread(current);
return true;
}
- 读写互斥
- 写写互斥
- 写锁支持同一个线程重入
- writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
写锁的释放
写锁释放通过重写AQS的tryRelease方法实现
protected final boolean tryRelease(int releases) {
//若锁的持有者不是当前线程,抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() ‐releases;
//当前写状态是否为0,为0则释放写锁
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
读锁的获取
实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。读锁的获取实现方法为
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果写锁已经被获取并且获取写锁的线程不是当前线程,当前线程获取读锁失败返回‐1
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return ‐1;
//计算出读锁的数量
int r = sharedCount(c);
/**
* 读锁是否阻塞 readerShouldBlock()公平与非公平的实现
* r < MAX_COUNT: 持有读锁的线程小于最大数(65535)
* compareAndSetState(c, c + SHARED_UNIT) cas设置获取读锁线程的数量
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { //当前线程获取读锁
if (r == 0) { //设置第一个获取读锁的线程
firstReader = current;
firstReaderHoldCount = 1; //设置第一个获取读锁线程的重入数
} else if (firstReader == current) { // 表示第一个获取读锁的线程重入
firstReaderHoldCount++;
} else { // 非第一个获取读锁的线程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++; //记录其他获取读锁的线程的重入次数
}
return 1;
}
// 尝试通过自旋的方式获取读锁,实现了重入逻辑
return fullTryAcquireShared(current);
}
- 读锁共享,读读不互斥
- 读锁可重入,每个获取读锁的线程都会记录对应的重入数
- 读写互斥,锁降级场景除外
- 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
- readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
读锁的释放
获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的写操作。读锁释放的实现主要通过方法tryReleaseShared:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果当前线程是第一个获取读锁的线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount‐‐; //重入次数减1
} else { //不是第一个获取读锁的线程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
‐‐rh.count; //重入次数减1
}
for (; ; ) { //cas更新同步状态
int c = getState();
int nextc = c ‐SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
阻塞队列
BlockingQueue接口
BlockingQueue 继承了 Queue 接口,是队列的一种。Queue 和 BlockingQueue 都是在 Java5 中加入的。阻塞队列(BlockingQueue)是一个在队列基础上又支持了两个附加操作的队列,
常用解耦。两个附加操作:
- 支持阻塞的插入方法put: 队列满时,队列会阻塞插入元素的线程,直到队列不满。
- 支持阻塞的移除方法take: 队列空时,获取元素的线程会等待队列变为非空
BlockingQueue常用方法示例
方法 | 抛异常 | 返回特定值 | 阻塞 | 阻塞特定时间 |
---|---|---|---|---|
入队 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
出队 | remove() | poll() | take() | poll(time, unit) |
获取队首元素 | element() | peek() | 不支持 | 不支持 |
element 方法是返回队列的头部节点,但是并不删除。如果队列为空,抛出异常
poll 方法作用也是移除并返回队列的头节点。 如果队列为空,返回null
offer 方法用来插入一个元素。如果添加成功会返回 true,而如果队列已经满了,返回false
poll 方法作用也是移除并返回队列的头节点。 如果队列为空,返回null
peek 方法返回队列的头元素但并不删除。 如果队列为空,返回null
put 方法的作用是插入元素。如果队列已满就无法继续插入,阻塞插入线程,直至队列空出位置
take 方法的作用是获取并移除队列的头结点。如果执队列里无数据,则阻塞,直到队列里有数据
是否有界
阻塞队列还有一个非常重要的属性,那就是容量的大小,分为有界和无界两种。无界队列意味着里面可以容纳非常多的元素,例如 LinkedBlockingQueue 的上限是Integer.MAX_VALUE,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。但是有的阻塞队列是有界的,例如 ArrayBlockingQueue 如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。
应用场景
BlockingQueue 是线程安全的,我们在很多场景下都可以利用线程安全的队列来优雅地解决我们业务自身的线程安全问题。使用生产者/消费者模式的时候,我们生产者只需要往队列里添加元素,而消费者只需要从队列里取出它们就可以了,考虑锁等线程安全问题的重任从“你”转移到了“队列”上,降低了我们开发的难度和工作量。
同时,队列它还能起到一个隔离的作用。比如说我们开发一个银行转账的程序,那么生产者线程不需要关心具体的转账逻辑,只需要把转账任务,如账户和金额等信息放到队列中就可以,而不需要去关心银行这个类如何实现具体的转账业务。而作为银行这个类来讲,它会去从队列里取出来将要执行的具体的任务,再去通过自己的各种方法来完成本次转账。这样就实现了具体任务与执行任务类之间的解耦,任务被放在了阻塞队列中,而负责放任务的线程是无法直接访问到我们银行具体实现转账操作的对象的,实现了隔离,提高了安全性。
常见阻塞队列
BlockingQueue 接口的实现类都被放在了 juc 包中,它们的区别主要体现在存储结构上或对元素操作上的不同,对于take与put操作的原理是类似的。
队列 | 描述 |
---|---|
ArrayBlockingQueue | 基于数组结构实现的一个有界阻塞队列 |
LinkedBlockingQueue | 基于链表结构实现的一个有界阻塞队列(上限很大可理解无界) |
PriorityBlockingQueue | 支持按优先级排序的无界阻塞队列 |
DelayQueue | 基于优先级队列(PriorityBlockingQueue)实现的无界阻塞队 列 |
SynchronousQueue | 不存储元素的阻塞队列 |
LinkedTransferQueue | 基于链表结构实现的一个无界阻塞队列 |
LinkedBlockingDeque | 基于链表结构实现的一个双端阻塞队列 |
ArrayBlockingQueue
ArrayBlockingQueue是最典型的有界阻塞队列,其内部是用数组存储元素的,初始化时需
要指定容量大小,利用 ReentrantLock 实现线程安全。
在生产者-消费者模型中使用时,如果生产速度和消费速度基本匹配的情况下,使用
ArrayBlockingQueue是个不错选择;当如果生产速度远远大于消费速度,则会导致队列填满,大量生产线程被阻塞。使用独占锁ReentrantLock实现线程安全,入队和出队操作使用同一个锁对象,也就是只能有一个线程可以进行入队或者出队操作;这也就意味着生产者和消费者无法并行操作,在高并发场景下会成为性能瓶颈。
ArrayBlockingQueue使用
BlockingQueue queue = new ArrayBlockingQueue(1024);
queue.put("1"); //向队列中添加元素
Object object = queue.take(); //从队列中取出元素
ArrayBlockingQueue的原理
利用了Lock锁的Condition通知机制进行阻塞控制。
核心:一把锁,两个条件
//数据元素数组
final Object[] items;
//下一个待取出元素索引
int takeIndex;
//下一个待添加元素索引
int putIndex;
//元素个数
int count;
//内部锁
final ReentrantLock lock;
//消费者
private final Condition notEmpty;
//生产者
private final Condition notFull;
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
...
lock = new ReentrantLock(fair); //公平,非公平
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
入队put方法
public void put(E e) throws InterruptedException {
//检查是否为空
checkNotNull(e);
final ReentrantLock lock = this.lock;
//加锁,如果线程中断抛出异常
lock.lockInterruptibly();
try {
//阻塞队列已满,则将生产者挂起,等待消费者唤醒
//设计注意点: 用while不用if是为了防止虚假唤醒
while (count == items.length)
notFull.await(); //队列满了,使用notFull等待(生产者阻塞)
// 入队
enqueue(e);
} finally {
lock.unlock(); // 唤醒消费者线程
}
}
private void enqueue(E x) {
final Object[] items = this.items;
//入队 使用的putIndex
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0; //设计的精髓: 环形数组,putIndex指针到数组尽头了,返回头部
count++;
//notEmpty条件队列转同步队列,准备唤醒消费者线程,因为入队了一个元素,肯定不为空了
notEmpty.signal();
}
出队take方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//加锁,如果线程中断抛出异常
lock.lockInterruptibly();
try {
//如果队列为空,则消费者挂起
while (count == 0)
notEmpty.await();
//出队
return dequeue();
} finally {
lock.unlock();// 唤醒生产者线程
}
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex]; //取出takeIndex位置的元素
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0; //设计的精髓: 环形数组,takeIndex 指针到数组尽头了,返回头部
count‐‐;
if (itrs != null)
itrs.elementDequeued();
//notFull条件队列转同步队列,准备唤醒生产者线程,此时队列有空位
notFull.signal();
return x;
}
LinkedBlockingQueue
LinkedBlockingQueue是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限,队列可以随着元素的添加而动态增长,但是如果没有剩余内存,则队列将抛出OOM错误。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。
LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。
LinkedBlockingQueue的原理
// 容量,指定容量就是有界队列
private final int capacity;
// 元素数量
private final AtomicInteger count = new AtomicInteger();
// 链表头 本身是不存储任何元素的,初始化时item指向null
transient Node<E> head;
// 链表尾
private transient Node<E> last;
// take锁 锁分离,提高效率
private final ReentrantLock takeLock = new ReentrantLock();
// notEmpty条件
// 当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
private final Condition notEmpty = takeLock.newCondition();
// put锁
private final ReentrantLock putLock = new ReentrantLock();
// notFull条件
// 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
private final Condition notFull = putLock.newCondition();
//典型的单链表结构
static class Node<E> {
E item; //存储元素
Node<E> next; //后继节点 单链表结构
Node(E x) {
item = x;
}
}
入队put方法
public void put(E e) throws InterruptedException {
// 不允许null元素
if (e == null) throw new NullPointerException();
int c = ‐1;
// 新建一个节点
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 使用put锁加锁
putLock.lockInterruptibly();
try {
// 如果队列满了,就阻塞在notFull上等待被其它线程唤醒(阻塞生产者线程)
while (count.get() == capacity) {
notFull.await();
}
// 队列不满,就入队
enqueue(node);
c = count.getAndIncrement();// 队列长度加1,返回原值
// 如果现队列长度小于容量,notFull条件队列转同步队列,准备唤醒一个阻塞在notFull条件上的线程(可以继续入队)
// 这里为啥要唤醒一下呢?
// 因为可能有很多线程阻塞在notFull这个条件上,而取元素时只有取之前队列是满的才会唤醒notFull, 此处不用等到取元素时才唤醒
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock(); // 真正唤醒生产者线程
}
// 如果原队列长度为0,现在加了一个元素后立即唤醒阻塞在notEmpty上的线程
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
// 直接加到last后面,last指向入队元素
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();// 加take锁
try {
notEmpty.signal();// notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程
} finally {
takeLock.unlock(); // 真正唤醒消费者线程
}
}
出队take方法
public E take() throws InterruptedException {
E x;
int c = ‐1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
// 使用takeLock加锁
takeLock.lockInterruptibly();
try {
// 如果队列无元素,则阻塞在notEmpty条件上(消费者线程阻塞)
while (count.get() == 0) {
notEmpty.await();
}
// 否则,出队
x = dequeue();
c = count.getAndDecrement();//长度‐1,返回原值
if (c > 1)// 如果取之前队列长度大于1,notEmpty条件队列转同步队列,准备唤醒阻塞在n otEmpty上的线程,原因与入队同理
notEmpty.signal();
} finally {
takeLock.unlock(); // 真正唤醒消费者线程
}
// 为什么队列是满的才唤醒阻塞在notFull上的线程呢?
// 因为唤醒是需要加putLock的,这是为了减少锁的次数,所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程,
// 这也是锁分离带来的代价
// 如果取之前队列长度等于容量(已满),则唤醒阻塞在notFull的线程
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
// head节点本身是不存储任何元素的
// 这里把head删除,并把head下一个节点作为新的值
// 并把其值置空,返回原来的值
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // 方便GC
head = first;
E x = first.item;
first.item = null;
return x;
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();// notFull条件队列转同步队列,准备唤醒阻塞在notFull上的线程
} finally {
putLock.unlock(); // 解锁,这才会真正的唤醒生产者线程
}
}
LinkedBlockingQueue与ArrayBlockingQueue对比
- 队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
- 数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
- 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
- 两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
SynchronousQueue
SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take。SynchronousQueue 最大的不同之处在于,它的容量为 0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff),所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。SynchronousQueue 会把元素直接从生产者传给消费者,在此期间并不需要做存储,所以如果运用得当,它的效率是很高的。
应用场景
SynchronousQueue非常适合传递性场景做交换工作,生产者的线程和消费者的线程同步
传递某些信息、事件或者任务。
SynchronousQueue的一个使用场景是在线程池里。如果我们不确定来自生产者请求数量,但是这些请求需要很快的处理掉,那么配合SynchronousQueue为每个生产者请求分配一个消费线程是处理效率最高的办法。Executors.newCachedThreadPool()就使用了SynchronousQueue。
DelayQueue
DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。延迟队列的特点是:不是先进先出,而是会按照延迟时间的长短来排序,下一个即将执行的任务会排到队列的最前面。
它是无界队列,放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了Comparable 接口,所以自然就拥有了比较和排序的能力,代码如下:
public interface Delayed extends Comparable<Delayed> {
//getDelay 方法返回的是“还剩下多长的延迟时间才会被执行”,
//如果返回 0 或者负数则代表任务已过期。
//元素会根据延迟时间的长短被放到队列的不同位置,越靠近队列头代表越早过期。
long getDelay(TimeUnit unit);
}
DelayQueue使用
DelayQueue<OrderInfo> queue = new DelayQueue<OrderInfo>();
DelayQueue的原理
//用于保证队列操作的线程安全
private final transient ReentrantLock lock = new ReentrantLock();
// 优先级队列,存储元素,用于保证延迟低的优先执行
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 用于标记当前是否有线程在排队(仅用于取元素时) leader 指向的是第一个从队列获取元素阻塞的线程
private Thread leader = null;
// 条件,用于表示现在是否有可取的元素 当新元素到达,或新线程可能需要成为leader时被通知
private final Condition available = lock.newCondition();
public DelayQueue() {
}
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
入队put方法
public void put(E e) {
offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 入队
q.offer(e);
if (q.peek() == e) {
// 若入队的元素位于队列头部,说明当前元素延迟最小
// 将 leader 置空
leader = null;
// available条件队列转同步队列,准备唤醒阻塞在available上的线程
available.signal();
}
return true;
} finally {
lock.unlock(); // 解锁,真正唤醒阻塞的线程
}
}
出队take方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (; ; ) {
E first = q.peek();// 取出堆顶元素
if (first == null)// 如果堆顶元素为空,说明队列中还没有元素,直接阻塞等待
available.await();
else {
long delay = first.getDelay(NANOSECONDS);// 堆顶元素的到期时间
if (delay <= 0)// 如果小于0说明已到期,直接调用poll()方法弹出堆顶元素
return q.poll();
// 如果delay大于0 ,则下面要阻塞了
// 将first置为空方便gc
first = null;
// 如果前面有其它线程在等待,直接进入等待
if (leader != null)
available.await();
else {
// 如果leader为null,把当前线程赋值给它
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 等待delay时间后自动醒过来
// 醒过来后把leader置空并重新进入循环判断堆顶元素是否到期
// 这里即使醒过来后也不一定能获取到元素
// 因为有可能其它线程先一步获取了锁并弹出了堆顶元素
// 条件锁的唤醒分成两步,先从Condition的队列里出队
// 再入队到AQS的队列中,当其它线程调用LockSupport.unpark(t)的时候才会真正唤醒
available.awaitNanos(delay);
} finally {
// 如果leader还是当前线程就把它置为空,让其它线程有机会获取元素
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 成功出队后,如果leader为空且堆顶还有元素,就唤醒下一个等待的线程
if (leader == null && q.peek() != null)
// available条件队列转同步队列,准备唤醒阻塞在available上的线程
available.signal();
// 解锁,真正唤醒阻塞的线程
lock.unlock();
}
}
如何选择适合的阻塞队列
功能
比如是否需要阻塞队列帮我们排序,如优先级排序、延迟执行等。如果有这个需要,我们就必须选择类似于 PriorityBlockingQueue 之类的有排序能力的阻塞队列。
容量
说是否有存储的要求,还是只需要“直接传递”。在考虑这一点的时候,我们知道前面介绍的那几种阻塞队列,有的是容量固定的,如 ArrayBlockingQueue;有的默认是容量无限的,如 LinkedBlockingQueue;而有的里面没有任何容量,如SynchronousQueue;而对于 DelayQueue 而言,它的容量固定就是Integer.MAX_VALUE。所以不同阻塞队列的容量是千差万别的,需要根据任务数量来推算出合适的容量,从而去选取合适的 BlockingQueue。
能否扩容
有时我们并不能在初始的时候很好的准确估计队列的大小,因为业务可能有高峰期、低谷期。如果一开始就固定一个容量,可能无法应对所有的情况,也是不合适的,有可能需要动态扩容。如果我们需要动态扩容的话,那么就不能选择ArrayBlockingQueue ,因为它的容量在创建时就确定了,无法扩容。相反,PriorityBlockingQueue 即使在指定了初始容量之后,后续如果有需要,也可以自动扩容。所以可以根据是否需要扩容来选取合适的队列。
内存结构
们分析过 ArrayBlockingQueue 的源码,看到了它的内部结构是“数组”的形式。和它不同的是,LinkedBlockingQueue 的内部是用链表实现的,所以这里就需要我们考虑到,ArrayBlockingQueue 没有链表所需要的“节点”,空间利用率更高。所以如果对性能有要求可以从内存的结构角度去考虑这个问题。
性能
比如 LinkedBlockingQueue 由于拥有两把锁,它的操作粒度更细,在并发程度高的时候,相对于只有一把锁的 ArrayBlockingQueue 性能会更好。另外,SynchronousQueue 性能往往优于其他实现,因为它只需要“直接传递”,而不需要存储的过程。如果我们的场景需要直接传递的话,可以优先考虑 SynchronousQueue。
ForkJoin
线程池的线程数设置多少合适?
CPU密集型任务
CPU密集型任务也叫计算密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍以上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。
IO密集型任务
IO密集型任务,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。对于这种任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用CPU 去执行其他的任务,互不影响,这样的话在工作队列中等待的任务就会减少,可以更好地利用资源。
《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法:
线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
如果任务的平均等待时间长,线程数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。
太少的线程数会使得程序整体性能降低,而过多的线程也会消耗内存等其他资源,所以如果想要更准确的话,可以进行压测,监控 JVM 的线程情况以及 CPU 的负载情况,根据实际情况衡量应该创建的线程数,合理并充分利用资源。
如何充分利用多核CPU的性能,计算一个很大数组中所有整数的和?
- 单线程相加,一个for循环搞定
- 利用多线程进行任务拆分,比如借助线程池进行分段相加,最后再把每个段的结果相加。
public class SumTask implements Callable<Long> {
int lo;//数组开始下标
int hi;//数组结束下标
int[] arr;
public SumTask(int[] a, int l, int h) {
lo = l;
hi = h;
arr = a;
}
@Override
public Long call() { //override must have this type
//System.out.printf("The range is [%d - %d]\n", lo, hi);
long result = 0;
for (int j = lo; j < hi; j++) {
result += arr[j];
}
//求和
return result;
}
}
/**
* 多线程计算1亿个整数的和
*/
public class SumMultiThreads {
//拆分的粒度
public final static int NUM = 10000000;
public static long sum(int[] arr, ExecutorService executor) throws Exception {
long result = 0;
//线程数
int numThreads = arr.length / NUM > 0 ? arr.length / NUM : 1;
//任务分解
SumTask[] tasks = new SumTask[numThreads];
//future结果集
Future<Long>[] sums = new Future[numThreads];
for (int i = 0; i < numThreads; i++) {
tasks[i] = new SumTask(arr, (i * NUM), ((i + 1) * NUM));
sums[i] = executor.submit(tasks[i]);
}
//结果合并
for (int i = 0; i < numThreads; i++) {
//阻塞获取结果
result += sums[i].get();
}
return result;
}
public static void main(String[] args) throws Exception {
// 准备数组
int[] arr = Utils.buildRandomIntArray(100000000);
//获取线程数
int numThreads = arr.length / NUM > 0 ? arr.length / NUM : 1;
System.out.printf("The array length is: %d\n", arr.length);
// 构建线程池
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
//预热
//((ThreadPoolExecutor)executor).prestartAllCoreThreads();
Instant now = Instant.now();
// 数组求和
long result = sum(arr, executor);
System.out.println("执行时间:"+Duration.between(now,Instant.now()).toMillis());
System.out.printf("The result is: %d\n", result);
executor.shutdown();
}
}
结论:在粒度和核心线程数量合适情况下,多线程会更快
分治算法
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
分治算法的步骤如下:
-
分解:将要解决的问题划分成若干规模较小的同类问题;
-
求解:当子问题划分得足够小时,用较简单的方法解决;
-
合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。
在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题。
public class SumRecursiveMT {
public static class RecursiveSumTask implements Callable<Long> {
//拆分的粒度
public static final int SEQUENTIAL_CUTOFF = 100000;
int lo;
int hi;
int[] arr; // arguments
ExecutorService executorService;
RecursiveSumTask(ExecutorService executorService, int[] a, int l, int h) {
this.executorService = executorService;
this.arr = a;
this.lo = l;
this.hi = h;
}
@Override
public Long call() throws Exception {
System.out.format("%s range [%d-%d] begin to compute %n",
Thread.currentThread().getName(), lo, hi);
long result = 0;
//最小拆分的阈值
if (hi - lo <= SEQUENTIAL_CUTOFF) {
for (int i = lo; i < hi; i++) {
result += arr[i];
}
// System.out.format("%s range [%d-%d] begin to finished %n",
// Thread.currentThread().getName(), lo, hi);
} else {
RecursiveSumTask left = new RecursiveSumTask(
executorService, arr, lo, (hi + lo) / 2);
RecursiveSumTask right = new RecursiveSumTask(
executorService, arr, (hi + lo) / 2, hi);
Future<Long> lr = executorService.submit(left);
Future<Long> rr = executorService.submit(right);
result = lr.get() + rr.get();
// System.out.format("%s range [%d-%d] finished to compute %n",
// Thread.currentThread().getName(), lo, hi);
}
return result;
}
}
public static long sum(int[] arr) throws Exception {
//TODO 思考: 用 Executors.newFixedThreadPool可以吗? 定长线程的饥饿
//ExecutorService executorService = Executors.newFixedThreadPool(12);
ExecutorService executorService = Executors.newCachedThreadPool();
//递归任务 求和
RecursiveSumTask task = new RecursiveSumTask(executorService, arr, 0, arr.length);
//返回结果
long result = executorService.submit(task).get();
executorService.shutdown();
return result;
}
public static void main(String[] args) throws Exception {
//准备数组
int[] arr = Utils.buildRandomIntArray(100000000);
System.out.printf("The array length is: %d\n", arr.length);
Instant now = Instant.now();
//数组求和
long result = sum(arr);
System.out.println("执行时间:"+ Duration.between(now,Instant.now()).toMillis());
System.out.printf("The result is: %d\n", result);
}
}
应用场景
分治思想在很多领域都有广泛的应用,例如算法领域有分治算法(归并排序、快速排序都属于分治算法,二分法查找也是一种分治算法);大数据领域知名的计算框架 MapReduce 背后的思想也是分治。既然分治这种任务模型如此普遍,那 Java 显然也需要支持,Java 并发包里提供了一种叫做 Fork/Join 的并行计算框架,就是用来支持分治这种任务模型的。
**传统线程池ThreadPoolExecutor有两个明显的缺点:一是无法对大任务进行拆分,对于某个任务只能由单线程执行;二是工作线程从队列中获取任务时存在竞争情况。**这两个缺点都会影响任务的执行效率。为了解决传统线程池的缺陷,Java7中引入Fork/Join框架,并在Java8中得到广泛应用。Fork/Join框架的核心是ForkJoinPool类,它是对AbstractExecutorService类的扩展。ForkJoinPool允许其他线程向它提交任务,并根据设定将这些任务拆分为粒度更细的子任务,这些子任务将由ForkJoinPool内部的工作线程来并行执行,并且工作线程之间可以窃取彼此之间的任务。
ForkJoinPool最适合计算密集型任务,而且最好是非阻塞任务。ForkJoinPool是ThreadPoolExecutor线程池的一种补充,是对计算密集型场景的加强。根据经验和实验,任务总数、单任务执行耗时以及并行数都会影响到Fork/Join的性能。所以,当你使用Fork/Join框架时,你需要谨慎评估这三个指标,最好能通过模拟对比评估,不要凭感觉冒然在生产环境使用。
Fork/Join的使用
Fork/Join 计算框架主要包含两部分,一部分是分治任务的线程池 ForkJoinPool,另一部分是分治任务 ForkJoinTask
ForkJoinPool
ForkJoinPool 是用于执行 ForkJoinTask 任务的执行池,不再是传统执行池Worker+Queue 的组合式,而是维护了一个队列数组 WorkQueue(WorkQueue[]),这样在提交任务和线程任务的时候大幅度减少碰撞。
ForkJoinPool构造器
ForkJoinPool中有四个核心参数,用于控制线程池的并行数、工作线程的创建、异常处理和模式指定等。各参数解释如下:
- int parallelism:指定并行级别(parallelism level)。ForkJoinPool将根据这个设定,决定工作线程的数量。如果未设置的话,将使用Runtime.getRuntime().availableProcessors()来设置并行级别;
- ForkJoinWorkerThreadFactory factory:ForkJoinPool在创建线程时,会通过factory来创建。注意,这里需要实现的是ForkJoinWorkerThreadFactory,而不是ThreadFactory。如果你不指定factory,那么将由默认的DefaultForkJoinWorkerThreadFactory负责线程的创建工作;
- UncaughtExceptionHandler handler:指定异常处理器,当任务在运行中出错时,将由设定的处理器处理;
- boolean asyncMode:设置队列的工作模式:asyncMode ? FIFO_QUEUE : LIFO_QUEUE。当asyncMode为true时,将使用先进先出队列,而为false时则使用后进先出的模式。
按类型提交不同任务
返回值 | 方法 | |
---|---|---|
提交异步执行 | void | execute(ForkJoinTask task) execute(Runnable task) |
等待并获取结果 | T | invoke(ForkJoinTask task) |
提交执行获取Future结果 | ForkJoinTask | submit(ForkJoinTask task) submit(Callable task) submit(Runnable task) submit(Runnable task, T result) |
- execute类型的方法在提交任务后,不会返回结果。ForkJoinPool不仅允许提交ForkJoinTask类型任务,还允许提交Runnable任务执行Runnable类型任务时,将会转换为ForkJoinTask类型。由于任务是不可切分的,所以这类任务无法获得任务拆分这方面的效益,不过仍然可以获得任务窃取带来的好处和性能提升。
- invoke方法接受ForkJoinTask类型的任务,并在任务执行结束后,返回泛型结果。如果提交的任务是null,将抛出空指针异常。
- submit方法支持三种类型的任务提交:ForkJoinTask类型、Callable类型和Runnable类型。在提交任务后,将返回ForkJoinTask类型的结果。如果提交的任务是null,将抛出空指针异常,并且当任务不能按计划执行的话,将抛出任务拒绝异常。
ForkJoinTask
ForkJoinTask是ForkJoinPool的核心之一,它是任务的实际载体,定义了任务执行时的具体逻辑和拆分逻辑。ForkJoinTask继承了Future接口,所以也可以将其看作是轻量级的Future。
ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。
- fork()——提交任务
fork()方法用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。
- join()——获取任务执行结果
join()方法用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。
通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下三个子类:
- RecursiveAction:用于递归执行但不需要返回结果的任务。
- RecursiveTask :用于递归执行需要返回结果的任务。
- CountedCompleter :在任务完成执行后会触发执行一个自定义的钩子函数
import java.util.concurrent.RecursiveTask;
//递归任务 用于计算数组总和
public class LongSum extends RecursiveTask<Long> {
// 任务拆分最小阈值
static final int SEQUENTIAL_THRESHOLD = 10000000;
int low;
int high;
int[] array;
LongSum(int[] arr, int lo, int hi) {
array = arr;
low = lo;
high = hi;
}
@Override
protected Long compute() {
//当任务拆分到小于等于阀值时开始求和
if (high - low <= SEQUENTIAL_THRESHOLD) {
long sum = 0;
for (int i = low; i < high; ++i) {
sum += array[i];
}
return sum;
} else { // 任务过大继续拆分
int mid = low + (high - low) / 2;
LongSum left = new LongSum(array, low, mid);
LongSum right = new LongSum(array, mid, high);
// 提交任务
left.fork();
right.fork();
//获取任务的执行结果,将阻塞当前线程直到对应的子任务完成运行并返回结果
long rightAns = right.join();
long leftAns = left.join();
return leftAns + rightAns;
}
}
}
/**
* 利用ForkJoinPool计算1亿个整数的和
*/
public class LongSumMain {
// 获取逻辑处理器数量 12
static final int NCPU = Runtime.getRuntime().availableProcessors();
public static void main(String[] args) throws Exception {
//准备数组
int[] array = Utils.buildRandomIntArray(100000000);
//递归任务
LongSum ls = new LongSum(array, 0, array.length);
// 构建ForkJoinPool
ForkJoinPool fjp = new ForkJoinPool(NCPU);
now = Instant.now();
//ForkJoin计算数组总和
ForkJoinTask<Long> result = fjp.submit(ls);
System.out.println("forkjoin sum=" + result.get());
System.out.println("执行时间:"+ Duration.between(now,Instant.now()).toMillis());
fjp.shutdown();
}
}
ForkJoinTask使用限制
ForkJoinTask最适合用于纯粹的计算任务,也就是纯函数计算,计算过程中的对象都是独立的,对外部没有依赖。提交到ForkJoinPool中的任务应避免执行阻塞I/O。
ForkJoinPool 的工作原理
- ForkJoinPool 内部有多个工作队列,当我们通过 ForkJoinPool 的 invoke() 或者submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个工作队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的工作队列中。
- ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
- 每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的top,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从top取出任务来执行。
- 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务,窃取的任务位于其他线程的工作队列的base,也就是说工作线程在窃取其他工作线程的任务时,使用的是FIFO 方式。
- 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
- 在既没有自己的任务,也没有可以窃取的任务时,进入休眠 。
工作窃取
ForkJoinPool与ThreadPoolExecutor有个很大的不同之处在于,ForkJoinPool存在引入了工作窃取设计,它是其性能保证的关键之一。工作窃取,就是允许空闲线程从繁忙线程的双端队列中窃取任务。默认情况下,工作线程从它自己的双端队列的头部获取任务。但是,当自己的任务为空时,线程会从其他繁忙线程双端队列的尾部中获取任务。这种方法,最大限度地减少了线程竞争任务的可能性。
ForkJoinPool的大部分操作都发生在工作窃取队列(work-stealing queues ) 中,该队列由内部类WorkQueue实现。它是Deques的特殊形式,但仅支持三种操作方式:push、pop和poll(也称为窃取)。在ForkJoinPool中,队列的读取有着严格的约束,push和pop仅能从其所属线程调用,而poll则可以从其他线程调用。
工作窃取的运行流程如下图所示 :
总结
Fork/Join是一种基于分治算法的模型,在并发处理计算型任务时有着显著的优势。其效率的提
升主要得益于两个方面:
- 任务切分:将大的任务分割成更小粒度的小任务,让更多的线程参与执行;
- 任务窃取:通过任务窃取,充分地利用空闲线程,并减少竞争。
在使用ForkJoinPool时,需要特别注意任务的类型是否为纯函数计算类型,也就是这些任务不应该关心状态或者外界的变化,这样才是最安全的做法。如果是阻塞类型任务,那么你需要谨慎评估技术方案。虽然ForkJoinPool也能处理阻塞类型任务,但可能会带来复杂的管理成本。
Callable
Runnable 的缺陷:
- 不能返回一个返回值
- 不能抛出 checked Exception
Callable的call方法可以有返回值,可以声明抛出异常。
Future
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
常用方法
- boolean cancel (boolean mayInterruptIfRunning) 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
- boolean isCancelled () 任务是否已经取消,任务正常完成前将其取消,则返回true
- boolean isDone () 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
- V get () throws InterruptedException, ExecutionException 等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常,ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException
- V get (long timeout, TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException
利用FutureTask创建Future
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
new Thread(futureTask).start();
Integer result = futureTask.get();
System.out.println(result);
}
static class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程正在计算");
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
使用案例:促销活动中商品信息查询
在维护促销活动时需要查询商品信息(包括商品基本信息、商品价格、商品库存、商品图片、商品销售状态等)。这些信息分布在不同的业务中心,由不同的系统提供服务。如果采用同步方式,假设一个接口需要50ms,那么一个商品查询下来就需要200ms-300ms,这对于我们来说是不满意的。如果使用Future改造则需要的就是最长耗时服务的接口,也就是50ms左右。
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> ft1 = new FutureTask<>(new T1Task());
FutureTask<String> ft2 = new FutureTask<>(new T2Task());
FutureTask<String> ft3 = new FutureTask<>(new T3Task());
FutureTask<String> ft4 = new FutureTask<>(new T4Task());
FutureTask<String> ft5 = new FutureTask<>(new T5Task());
//构建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(ft1);
executorService.submit(ft2);
executorService.submit(ft3);
executorService.submit(ft4);
executorService.submit(ft5);
//获取执行结果
System.out.println(ft1.get());
System.out.println(ft2.get());
System.out.println(ft3.get());
System.out.println(ft4.get());
System.out.println(ft5.get());
executorService.shutdown();
}
static class T1Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T1:查询商品基本信息...");
TimeUnit.MILLISECONDS.sleep(5000);
return "商品基本信息查询成功";
}
}
static class T2Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T2:查询商品价格...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品价格查询成功";
}
}
static class T3Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T3:查询商品库存...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品库存查询成功";
}
}
static class T4Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T4:查询商品图片...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品图片查询成功";
}
}
static class T5Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T5:查询商品销售状态...");
TimeUnit.MILLISECONDS.sleep(50);
return "商品销售状态查询成功";
}
}
}
Future的局限性
从本质上说,Future表示一个异步计算的结果。它提供了isDone()来检测计算是否已经完成,并且在计算结束后,可以通过get()方法来获取计算结果。在异步计算中,Future确实是个非常优秀的接口。但是,它的本身也确实存在着许多限制:
- 并发执行多任务:Future只提供了get()方法来获取结果,并且是阻塞的。所以,除了等待别无他法;
- 无法对多个任务进行链式调用:如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;
- 无法组合多个任务:如果你运行了10个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;
- 没有异常处理:Future接口中没有关于异常处理的方法;
CompletableFuture
很多时候业务逻辑处理存在串行[依赖]、并行、聚合的关系。如果要我们手动用 Fueture实现,是非常麻烦的。
CompletableFuture是Future接口的扩展和增强。CompletableFuture实现了Future接口,并在此基础上进行了丰富地扩展,完美地弥补了Future上述的种种问题。更为重要的是,CompletableFuture实现了对任务的编排能力。
应用场景
描述依赖关系:
- thenApply() 把前面异步任务的结果,交给后面的Function
- thenCompose()用来连接两个有依赖关系的任务,结果由第二个任务返回
描述and聚合关系:
- thenCombine:任务合并,有返回值
- thenAccepetBoth:两个任务执行完成后,将结果交给thenAccepetBoth消耗,
无返回值。 - runAfterBoth:两个任务都执行完成后,执行下一步操作(Runnable)。
描述or聚合关系:
- applyToEither:两个任务谁执行的快,就使用那一个结果,有返回值。
- acceptEither: 两个任务谁执行的快,就消耗那一个结果,无返回值。
- runAfterEither: 任意一个任务执行完成,进行下一步操作(Runnable)。
并行执行:
- CompletableFuture类自己也提供了anyOf()和allOf()用于支持多个CompletableFuture并行执行
创建异步操作
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor e
xecutor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Exe
cutor executor)
这几个方法区别:
- runAsync 方法以Runnable函数式接口类型为参数,没有返回结果,supplyAsync方法Supplier函数式接口类型为参数,返回结果类型为U;Supplier 接口的 get() 方法是有返回值的(会阻塞)
- 没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。
- 默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)。如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议要根据不同的业务类型创建不同的线程池,以避免互相干扰