juc 相关知识点
并行一定比串行执行的速度快吗?
不一定
这是因为线程创建、销毁会消耗CPU,有可能创建销毁的时间超过了计算本身时间(虽然都在使用线程池)
同时也存在上下文切换的开销,需要保护现场与恢复现场,消耗CPU资源
线程本身也占用内存,一个线程占用1MB
一个系统内线程资源是有限的
为什么要需要多线程?
1. 为了充分利用多核cpu计算能力,实现并行计算
2. 提高程序处理数据的速度,比如实现异步化
LockSupport
实现线程间的阻塞和唤醒,使用它不用关注是等待线程先进行还是唤醒线程先运行
原子类
与AQS一样,依赖于CAS与volatile来保证原子操作,但原子类是采用的是不断自旋重试的方式
Java中CAS是借用Unsafe类,其只是接口调用,底层采用的是依赖硬件
来保证冲突检测与操作是原子性的
AQS
AbstractQueuedSynchronizer即抽象队列同步器。定义了一套多线程访问共享资源的同步基础模板,如常用的ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier都是基于它。
- 依赖的数据结构:
1.volitale修饰的 int state变量:表示共享资源可用状态
2.CLH双端队列:存放被暂停执行线程
3.ConditonObject:条件队列,配合双端队列实现线程间的同步协作
- 不同的同步器对
state变量
使用有不同的含义
ReentrantLock
的state
用来表示是否有锁资源Semaphore
的state
用来表示可用信号的个数CountDownLatch
的state
用来表示计数器的值
- 依赖的基础类
1.Unsafe类(提供原子操作,执行对state变量的原子操作)
2.LockSupport类(暂停线程执行、唤醒线程继续执行)
- 大致原理流程
提供了一套线程阻塞唤醒机制,也就是如果共享资源是空闲的,那么线程就可以获取到执行权;否则就将此线程放在队列中暂停等待,直到共享资源是空闲时才被唤醒。
-
state两种模式
-
独占模式:同一时间只有一个线程能拿到锁执行,锁的状态只有0和1两种情况。
-
共享模式:同一时间有多个线程可以拿到锁协同工作,锁的状态大于或等于0。
-
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态,如果能则线程可以并发执行
ReentrantLock执行过程
AQS使用一个volatile修饰的int类型的成员变量state来表示同步状态,通过CAS修改同步状态的值。当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源,此线程会并将state加1,表示此线程可以获得共享资源的使用权。如果 state不为0,则说明当前有其他线程正在操作共享变量,这时此线程要加入同步队列队尾阻塞等待,前面线程操作完成后将state变成0,会唤醒同步队列中的队首线程(第二个线程)再去尝试将state由0变成1。
锁使用方式
一共三种:
// 获取不到锁直接返回
适用场景:获取锁执行,获取不到则不执行(部分执行)
boolean tryLock = lock.tryLock();
// 在指定时间内尝试获取锁,超时则直接返回false
适用场景:获取不到锁的线程在指定时间内不断尝试获取锁,超时则不再执行(部分执行)
lock.tryLock(1, TimeUnit.MINUTES);
// 阻塞式,获取不到锁的线程一直阻塞等待释放锁。
适用场景:获取锁执行,获取不到则等待锁释放后抢到锁再执行(所有线程全部都执行)
lock.lock();
synchronized
mark word内容如下
每个对象都有一个Mark Word,其指向ObjectMonitor(监视器锁),其底层是c/c++语言写的,本质上是类似于与AQS,是一个同步器
锁监视器有几个关键属性:
1. 锁的重入次数、
2. 获取当前对象的监视器相应线程ID、
3. 存放wait状态的线程队列、
4. 存放block状态的线程队列
注:像Object中的wait(),notify()是要依配合synchronized使用的,其中依赖的是ObjectMonitor的_WaitSet
。
对于同步代码块
1. 当一个线程遇到 monitorenter 指令时,会判断锁监视器中的锁计数器是否等于0,如果等于0,表示线程们可以获取到该锁监视器;发现不是0就进入同步队列中并等待。
2. 当遇到monitorexit指令时,会将锁计数器进行减1。当锁计数器等于0时,表示当前线程执行完同步代码,并通知队列的其他线程去获取锁监视器。
对于同步方法
使用了ACC_SYNCHRONIZED标识该方法是阻塞同步方法
加锁、解锁详细过程
线程知识查漏补缺
多线程问题
1. 创建与销毁线程消耗CPU资源
2. 线程一般都是大于CPU核心数,同一时刻只有一个线程占用cpu,那么这些线程上下文切换需要,保护恢复线程工作,难免会出现一系列的数据在寄存器、缓存区中的数据来回拷贝,也是很消耗性能
3. 线程本身也占用了内存
线程中断
一个线程被另外一个线程通知要中断了
Thread.interrupt():通知线程中断
- 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
- 如果线程处于正常活动状态,那么会将该线程的
中断标志
设置为 true。至于什么时候被中断,那么是线程本身的事情。
Thread.interrupted():判断中断标识是否为true,并清除当前中断状态
Thread.isInterrupted():判断中断标识是否为true
import java.util.concurrent.TimeUnit;
public class InterruptExample {
private static class MyThread2 extends Thread {
@Override
public void run() {
// 判断中断标识是否为true,如果为true则表示线程已经被中断
while (!interrupted()) {
System.out.println("ing");
}
System.out.println("Thread end");
}
}
public static void main(String[] args) throws InterruptedException {
MyThread2 myThread2 = new MyThread2();
myThread2.start();
TimeUnit.SECONDS.sleep(1);
// 仅仅是将中断标识改为true
myThread2.interrupt();
}
线程状态转化
实现线程的几种方式及其原理:
- 继承Thread方式
这种方式本质上是实现了Runnable中的run方法,该run()是内核线程得到cpu时间片后的一个回调方法
也就说代码实现是:线程Thread调用start方法后会做两件事,一是为Java用户线程分配内存空间,其次是通知操作系统创建内核线程与当前Java中的用户线程进行绑定,此时内核线程进入了就绪状态,接着等待调度器分配给时间片,一旦被分配了cpu时间片就会回调线程任务(Runnable)的run方法,Java中线程状态变为运行状态。
- Thread + 实现Runable类run方法的方式
Runable作为Thread成员变量。
线程Thread调用start方法后会进入就绪状态,等待调度器分配时间片。当线程获取到cpu时间片就会执行当前线程任务(Runnable)的run方法,
- Thread、Callable + FutureTask类方式
FutureTask继承了Runable类,作为Thread的成员变量;Callable 作为FutureTask的成员变量。
FutureTask实现了Future类,可以获取线程计算的结果
线程Thread调用start方法后会进入就绪状态,等待调度器分配时间片。当线程被分配了cpu时间片就会执行当前线程的run方法,该run方法会调用其成员变量FutureTask实例的run方法。
与上边不同点的是:FutureTask类中的run方法会调用其成员变量中Callable实例的call方法
FutureTask内部维护了一个任务状态,所有的状态都是围绕这个任务来进行的,随着任务的进行,状态也在不断的更新。
FutureTask同CompletableFuture一样实现了Future多个方法,其中是可以通过get方法阻塞获取结果(本质上使用LockSupport.park方法进行阻塞),并且该方法在遇到异常时进行抛出的
-
FutureTask实现原理:本质上使用LockSupport.park、unpark+阻塞队列+任务状态实现
1. 线程调用get方法获取数据时会进入for循环,同时会根据任务状态进行判断,如果任务状态不是NORMAL、EXCEPTIONAL,第一次则会将当前线程添加到阻塞队列中,第二次for使用LockSupprot.park方法,暂停当前线程的执行 2. 当执行FutureTask中的run方法时,实际上会调用Callable中call方法,call方法为真实的任务逻辑,当call方法执行完后,无论是否异常,都会唤醒阻塞暂停的线程,只不过异常时会把异常对象返回,并将当前执行的状态改为EXCEPTIONAL;正常会把call方法返回值返回,并将当前执行的状态改为NORMAL 3. 被阻塞的线程此时被唤醒,继续执行for循环,当发现状态是NORMAL、EXCEPTIONAL时会跳出for循环,同时返回结果(返回值或者异常对象)
-
使用线程池的方式,提前创建好线程
并行流使用的是ForkJoinPool线程池、CompletableFuture可以自定义线程池,适用的场景更多
-
Thread中的yield方法
作用: 主动让出CPU资源,然后再次参与时间片的竞争。相当于一个人完成了一项非常重要的任务,然后就休息了一段时间后继续做任务 使用场景: 适用于一些不是很重要的任务中,又担心它会占用过多的CPU资源
提问1:当线程调用LockSupport.park或其他暂停线程执行的方法后,然后被唤醒继续执行,那么这时会立即执行吗?
答:不会,被唤醒后会进如就绪状态,重新等待调度器分给CPU时间片
提问2:new Thread()会创建操作系统线程吗?
答:不会,只会创建java层面的用户线程,内核线程要等到调用start方法后才会创建的
守护线程
守护线程就像影子,当人不在的时候,影子也会随之消失。像ForkJoinPool使用的线程就是守护线程,当所有的用户执行结束后,守护线程也就停止了
线程join方法
在一个线程A中,如果线程B调用threadB.join方法,意思是暂停A线程,先让B线程执行完。当B线程执行完后,会调用notifyAll()通知所有等待的线程继续执行,然后A线程就可以执行啦
本质上join方法最终执行的是wait(0)方法,跟调用Object.wait方法一样是调用wait(0),目的是让主线程暂停执行,当子线程执行结束后,会调用notifyAll通知所有的线程执行
线程调度的2种方式
1. 协同式:线程的执行CPU时间由线程本身来控制。
弊端:如果某个进程中的线程迟迟不让出CPU执行时间,则会导致整个系统会崩溃
2. 抢占式:由操作系统来分配执行CPU时间 --HotSport虚拟机使用的抢占式
Callable+FutureTask
FutureTask实现了Future接口,FutureTask 内部维护了一个任务状态,所有的状态都是围绕这个任务来进行的。
原理:outcome用来存储执行结果、任务的一系列状态
当调用 get() 方法获取数据,如果任务没有执行完成,会将当前线程放入队列并调用LockSupport.park暂停线程执行;当被唤醒后就可以拿到执行结果或抛出异常
当任务调用run方法时,一旦call方法执行完成后会将`子线程的结果或异常`放在成员变量outcome中,这时会唤醒队列中被暂停执行的线程继续执行任务
优点:可以获取子线程执行任务的结果
缺点:
1.使用get()方法会调用LockSupport.park一直阻塞主线程,直到get拿到数据【如下图】。弊端:阻塞主线程(NIO)
2.采用轮询方式不断主动查询任务是否完成。弊端:会消耗cpu资源(BIO)
import java.util.concurrent.*;
/**
* @description:
* @author: party-abu
* @create: 2023-03-17 12:34
*/
public class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(2);
return "ok";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.submit(futureTask);
// 1.futureTask.get()方法会一直阻塞main线程,直到get拿到数据。弊端:阻塞主线程
// System.out.println("futureTask.get() = " + futureTask.get());
// System.out.println("main-after = ");
// 2.采用轮询方式不断主动查询任务是否完成。弊端:会消耗cpu资源
while (true) {
boolean done = futureTask.isDone();
if (done) {
System.out.println("done = " + "任务已完成");
break;
} else {
System.out.println("任务还未完成");
TimeUnit.SECONDS.sleep(4);
}
}
}
}
CompletableFuture
实现的接口有Future、CompletionStage。Future可以获取返回值,CompletionStage可以实现在一个执行结果上进行多次流式调用
特点
真正意义上的异步操作,不阻塞主线程,通过函数回调主动通知
与并行流区别
并行流使用的是ForkJoinPool线程池,固定cpu核数-1个线程数,适用计算型任务;并且ForkJoinPool线程池的线程都是守护线程,意味着主线程退出,守护线程就会被销毁
CompletableFuture配合使用自定义线程池,更加灵活
@Test
public void testCF01() {
ExecutorService executorService = Executors.newFixedThreadPool(20);
List<Integer> partition = Arrays.asList(1, 2);
// 使用completableFuture能更灵活的使用线程池
partition.stream().map(item -> {
return CompletableFuture.supplyAsync(() -> {
// // 操作数据
return item + 1;
}, executorService);
}).collect(Collectors.toList()).stream().map(CompletableFuture::join)
.collect(Collectors.toList())
.forEach(System.out::println);
System.out.println("=============");
// 使用并行流
partition.stream().parallel().map(item -> {
// // 操作数据
return item + 1;
}).collect(Collectors.toList())
.forEach(System.out::println);
}
作用
主线程让子线程异步化处理,也可以使多个子任务并行处理。提高响应时间
函数回调:
当子线程任务执行完了便可以将结果传递到当前线程(或其他线程)相应的函数进行处理,这样主线程不用一直等待子线程执行结束了才能做其他事。异步处理响应更加高效
- 异步任务结束时,会自动回调某个对象的方法
- 异步任务出错时,会自动回调某个对象的方法
- 主线程设置好回调后,不再关心异步任务的执行
使用场景
使用分批 + CompletableFuture异步
/**
* 查询数据优化方式(分片+异步)
*/
@Test
public void test16() {
// 初始数据
List<Integer> list = Arrays.asList(1, 2);
// 1.分片处理
List<List<Integer>> partition = Lists.partition(list, 10);
// // 2.1 使用parallelStream,用的是forkJoinPool线程池
// partition.parallelStream().forEach(item -> {
// // 操作数据
// allItem.add(data);
// });
// 2.2 使用CompletableFuture,用的默认是forkJoinPool线程池,可以使用自定义线程池
partition.stream().map(item -> {
return CompletableFuture.runAsync(() -> {
// // 操作数据
// allItem.add(data);
});
}).collect(Collectors.toList()).forEach(CompletableFuture::join);
}
/**
* 远程调用(多个异步操作并行处理获取结果)
*/
@Test
public void test17() {
CopyOnWriteArrayList<Object> datas = new CopyOnWriteArrayList<>();
CompletableFuture<Void> voidCompletableFuture01 = CompletableFuture.runAsync(() -> {
List<Object> data = new ArrayList<>();
datas.add(data);
});
CompletableFuture<Void> voidCompletableFuture02 = CompletableFuture.runAsync(() -> {
List<Object> data = new ArrayList<>();
datas.add(data);
});
CompletableFuture<Void> voidCompletableFuture03 = CompletableFuture.runAsync(() -> {
List<Object> data = new ArrayList<>();
datas.add(data);
});
try {
CompletableFuture.allOf(voidCompletableFuture01,voidCompletableFuture02,voidCompletableFuture03).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
不阻塞主线程,使用函数回调处理结果
CompletableFuture.supplyAsync(() -> {
try {
System.out.println("需要等待2秒");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "ok";
}, executorService).whenComplete((s, e) -> {
if (e == null) {
System.out.println("s = " + s);
// 1.保存数据到数据库里
}
}).exceptionally(e -> {
System.out.println(e);
return null;
});
join与get方法
CompletableFuture也是实现了Future接口多个方法,其中join和get方法作用是一样的,都会阻塞当前线程的执行
相同点:最终会调用LockSupport.park方法暂停当前线程的执行
区别:join()方法不会检查编译时异常,get()方法会检查编译时异常
异常处理
1. 可从异常中恢复过来继续执行。
exceptionally:是个consumer接口,专注与异常处理,要求返回一个特殊的值
handle(res,ex): 是个function接口,能拿到结果和异常,并要返回一个特殊的值
2. 抛异常会阻塞后面流程:
whenComplete: 是个function接口,能拿到结果和异常
exceptionally 等价于 try catch
whenComplete | handle 等价于 try catch finally
Async与不带Async
whenComplete()与whenCompleteAsync()方法区别?也就是带Async方法与不带的有什么区别?
whenComplete使用上边执行任务的同一个线程
whenCompleteAsync使用线程池的其他线程,有可能跟上边执行任务使用的是同一个线程
注意事项
1. 要使用自定义线程池,默认的使用ForkJoinPool线程池,该池子固定cpu核数-1个线程数,所有的默认的cf和并行流都是用的这个池子,同时使用会导致性能下降活饥饿
2. get获取方法要指定超时时间
3. 使用AbortPolicy,如果最大线程池满了,直接让主线程抛异常,达到迅速抛异常,使用其他拒绝策略会出现接口响应过慢
4. 不同的业务要使用不同的线程池,防止某个业务拿不到线程资源导致饥饿
5.
案例演示
任务个数不知道多少
public class CompletableFutureTest2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
List<String> nums = Arrays.asList("a", "b", "c", "d", "e", "f");
// 1.分片处理
List<List<String>> partitionNums = Lists.partition(nums, 3);
// 2.使用多线程处理分片的数据
partitionNums.stream()
// 先用CompletableFuture包装映射为CompletableFuture
.map(item -> CompletableFuture.runAsync(() -> {
// 批量操作集合,比如保存数据库中
System.out.println("item = " + item);
}, executorService))
// 阻塞等地它们都完成任务
.collect(Collectors.toList()).forEach(CompletableFuture::join);
}
}
任务个数知道多少
public class TestTask2 {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<Void> task1 =
CompletableFuture.runAsync(() -> {
//自定义业务操作
System.out.println("业务一");
});
CompletableFuture<Void> task6 =
CompletableFuture.runAsync(() -> {
//自定义业务操作
System.out.println("业务二");
});
CompletableFuture<Void> headerFuture = CompletableFuture.allOf(task1, task6);
try {
headerFuture.get();
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
System.out.println("all done. ");
}
}
也可以使用CountDownLatch+线程池使用
public class TestTask {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(8, 16, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)
, new CustomizableThreadFactory("订单"));
List<String> tasks = Arrays.asList("1", "3", "4", "5");
CountDownLatch countDownLatch = new CountDownLatch(tasks.size());
for (int i = 0; i < tasks.size(); i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
try {
System.out.println("执行任务" + finalI);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
}
});
}
System.out.println("准备执行main任务");
countDownLatch.await();
threadPoolExecutor.shutdown();
System.out.println("执行完main任务");
}
}
使用allof还是join
两者都会阻塞主线程,当子线程执行完会唤醒子线程。allof相对于join阻塞唤醒次数较少,性能更高
List<CompletableFuture<Integer>> collect = partition.stream().map(item -> {
return CompletableFuture.supplyAsync(() -> {
// 操作数据
int newNum = item + 1;
System.out.println(newNum);
return newNum;
}, executorService);
}).collect(Collectors.toList());
CompletableFuture.allOf(collect.toArray(new CompletableFuture[0])).join();
partition.stream().map(item -> {
return CompletableFuture.supplyAsync(() -> {
// 操作数据
int newNum = item + 1;
System.out.println(newNum);
return newNum;
}, executorService);
}).collect(Collectors.toList()).forEach(CompletableFuture::join);
Happens-Before
先行发生原则:表达的是前面一个操作对后面一个操作一定是可见的,无论两个操作是否在同一个线程。
比如:A操作 Happens-Before B操作,那么A操作做的事情对于B操作来说一定是可见的。
CountDownLatch
原理
1. new CountDownLatch(3)使用构造函数时会初始化state为指定的值。这里是3,表示的是3个线程并发执行
2. 当main线程执行await()时,会判断state是否等于0,不为0时则`添加到同步队列`中,并调用LockSupport.park()`暂停执行main线程`
3. 每当子线程执行countDown时,会将state减一。当执行的最后一个子线程发现state减少到为0时,会调用LockSupport.unPark()唤醒同步队列中的main线程恢复执行
注意事项
- 子线程是并行执行的
package com.abu.thread;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @description:
* @author: party-abu
*/
public class CDDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (long i = 0; i < countDownLatch.getCount(); i++) {
long finalI = i;
executorService.execute(() -> {
System.out.println(finalI);
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("main");
}
}
ReentrantLock
整体结构
执行流程
1. 当执行Lock()时,会判断state是否为0,如果为0则cas操作设置state由0变为1,如果设置成功则表示获取锁,并设置为排他锁;如果失败,则添加当前线程到同步队列队尾,并且并调用LockSupport.park()暂停执行当前线程
2. 如果同一个线程有方法重入(lock()方法又调用了lock()方法),会继续判断state是为0,如果不是,判断当前线程是否为获取锁的线程,如果是则将state+1
3. 调用unLock()方法时,会将state - 1,直至当stete减少为0时,会调用LockSupport.unPark()唤醒同步队列中首节点的第二个线程恢复执行。但是要想真正的执行也要参与竞争到锁才能可以
具体流程
核心代码
public final void acquire(int arg) {
// 1.tryAcquire:尝试将state更改由0变成1;或如果是当前线程获取锁则将锁计数器+1
if (!tryAcquire(arg) &&
// 2.addWaiter:添加当前线程到CLH队列中
// 3.acquireQueued:暂停当前线程执行,被唤醒时竞争尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
公平锁与非公平锁
公平锁:先看队列中是否有阻塞等待的线程,如果有则去队列中排队
非公平锁:会立即尝试获取锁,好处是如果当前共享资源可用,那就不用排队了直接执行,无需阻塞。坏处会导致队列其他线程可能会等待很久
- 公平锁
- 非公平锁
ConditionObject
条件队列,是AQS同步队列的一个内部类,结构是FIFO的单向链表,可以理解为synchronized中的wait,notify一样的功效。但一个AQS同步队列可以绑定多个条件队列哦
依赖与LockSupport类进行暂停与唤醒线程执行,举例LinkedBlockingQueue队列来说
- await方法:
1.添加当前线程到条件对列尾部,2.完全释放锁(state变成0),3.调用LockSupport.park暂停当前线程执行。
- signal方法: 将条件队列中对头线程出队添加到CLH队尾部,并唤醒它,从await方法中的isOnSyncQueu循环退出,执行acquireQueued方法尝试获取锁,当获取锁成功时会从调用await方法继续执行。
- signalAll方法: 将条件队列中的线程依次添加到CLH队尾部,并唤醒它们参与锁竞争(和signal方法一样)
Thread.sleep、wait、LockSupport.park、ConditionObject.await类区别
相同点:都是暂停当前线程执行,不会消耗cpu资源
不同点:
sleep:必须指定时间,时间过后会自动醒来继续执行,不会释放锁监视器
wait:会释放锁,要由另外一个线程调用notify唤醒。要在synchronize代码中使用
park:暂停当前线程执行,可以在线程内任何位置使用
await:释放锁,暂停当前线程执行
Semaphore
作用:限制同时并发执行的线程数量
1. new Semaphore(20)使用构造函数时会初始化state为指定的值。这里是20,表示的初始化是20凭证
2. 当线程调用acquire()时,会将凭证减去1。当state小于0时,会将当前线程添加到CLH队尾,并且暂停当前线程的执行;否则执行业务逻辑
3. 当线程调用release()时,会将凭证加上1,同时会调用LockSupport.unPark()唤醒CLH队列中队首线程恢复执行,尝试获取凭证
注意事项
并发执行的线程数:初始化state 除于 acquire中的凭证数
案例:它可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。
假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制
代码演示
public class SemaphoreTest {
private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
s.acquire();
System.out.println("save data");
s.release();
} catch (InterruptedException e) {
}
}
});
}
threadPool.shutdown();
}
}
CyclicBarrier
让一组线程都达到屏障点暂停执行,先执行CyclicBarrier构造函数中的线程任务,再唤醒这组线程任务。
1. 构造函数会初始化一个count计数器,表示要暂停的线程数
2. 子线程执行await()会将count减1,并将当前线程添加到同步队列并暂停执行。直至count=0时,先执行构造函数中的线程任务,然后再唤醒同步队列暂停的线程 继续执行
案例
import java.util.concurrent.*;
/**
* @description:
* @author: party-abu
*/
public class CDDemo2 {
public static void main(String[] args) throws InterruptedException {
// 初始化count=3
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
System.out.println("last");
});
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 3; i++) {
final int num = i;
executorService.execute(() -> {
try {
System.out.println("num = " + num);
// 每次await将count减1并将当前线程添加到同步队列并暂停执行。当count=0时,执行构造函数中的线程任务,然后再唤醒同步队列暂停的线程
cyclicBarrier.await();
System.out.println("num = after " + num);
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
}
}
阻塞队列
ArrayBlockingQueue、LinkedBlockingQueue底层使用的是ReentrantLock+Condition
举例ArrayBlockingQueue
1. ArrayBlockingQueue、LinkedBlockingQueue都是FIFO的队列
2. 队列满了不能放,队列空了不能取
3. 成功放入则唤醒取操作,成功取到则唤醒放操作
4. 有界阻塞队列容量固定,无界阻塞队列并不是真的无大小容量限制,而是Integer.MAX_VALUE(21亿左右)
put过程:
1. 先获取到锁
2. 获取到锁成功后,如果已满则添加到生产者同步队列里并暂停执行
3. 如果成功放入容器后,就会随机唤醒一个take线程执行拿操作。但是这个唤醒的take线程也要参与竞争获取到锁才能真正执行
take过程:
与put过程类型
1. 先获取到锁
2. 获取到锁成功后,如果容器没有元素则添加到同步队列里并暂停执行
3. 如果成功拿到一个元素后,就会随机唤醒一个put线程执行取操作。但是这个唤醒的put线程也要参与竞争获取到锁才能真正执行
LinkedBlockingQueue中头节点不保存数据,put、take操作中真正唤醒消费者在finally中
public void put(E e) throws InterruptedException {
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;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
// 当放入第一个元素后,就会唤醒一个消费者
signalNotEmpty();
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 当拿走完元素,就唤醒一个生产者
if (c == capacity)
signalNotFull();
return x;
}
吞度量优先级
SynchronousQueue > LinkedBlockingQueue > ArrayBlockingQueue
Synchronous不会真正存储元素,内部使用
CAS
无锁操作,吞吐量更高LinkedBlockingQueue比ArrayBlockingQueue吞吐量高,是因为其使用了锁分离思想,put、take操作各使用一把锁,put从链表尾部添加数据,take从头部取数据,一定程度上避免了不必要的锁竞争
ArrayBlockingQueue只使用了一把锁,锁竞争相对激烈
CopyOnWriteArrayList
在set操作中使用锁进行同步,原理是不直接操作原容器,而是复制成一个新的容器,往新的容器中添加数据,当写完之后将新容器替换旧容器;在get操作时能够立即知晓这个容器的改变,是因为容器使用volitale修饰的
get操作不用加锁,因为读与读没有并发安全问题
volatile
保证可见性:
使用缓存一致性协议,当线程向主内存进行写共享数据,会将其他cpu中副本数据置为无效状态;当其他线程需要读这个副本数据时发现数据无效,那么会从主内存重新加载到本地内存
禁止指令重排:
通过插入内存屏障指令来禁止处理器重排序
线程池
实现思想
将之前new Thread start()的工作单元和执行单元在一块的逻辑分开来,体现思想是预分配与循环使用,http中的keep-alive即长连接也是这种设计思想
主要依赖数据结构是:
BlockingQueue
:负责存储任务即Runnable HashSet
:负责存储线程对象
好处
线程创建、销毁需要消耗cpu资源,避免线程创建销毁带来的性能消耗;
同时线程默认情况下占用1MB内存,过多导致系统会崩溃
允许任务`并行`处理
但是线程上下文切换带来保存和恢复现场的性能消耗是避免不了的。
多线程的使用场景?
使用多线程一方面是为了提高处理任务效率,另一方面是使得CPU能同时处理多个事件。
-
为了不阻塞主线程,启动其他线程来做事情,这样耗时时间完全取决于耗时最长的那个子线程,显著地提高了处理效率
比如:主线程处理监听请求,子线程处理任务【异步】
-
当一个线程任务要等待另外一个线程任务处理完再执行,使用多线程能充分利用CPU阻塞等待的时间去执行任务
实现原理
通过在池子中初始化一些线程,然后让这些线程不断从队列中获取并执行任务
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//当正在运行的线程数 < 核心线程数,立即创建一个核心线程执行任务(快速让核心线程打满)
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 当正在运行的线程数 >= 核心线程数,添加到队列中
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);
}
// 队列满了则创建非核心线程执行任务;正在运行的线程大于了最大线程数则执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
线程池状态
RUNNING: -1 << COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
SHUTDOWN: 0 << COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
STOP : 1 << COUNT_BITS,即高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会尝试中断正在运行的任务;
TIDYING : 2 << COUNT_BITS,即高3位为010, 任务是空的
TERMINATED: 3 << COUNT_BITS,即高3位为011, terminated()方法已经执行完成
shutdown()与shutdownNow()
shutdown():
1. 会将线程池状态改为SHUTDOWN
2. 对所有的线程发送中断信息,尝试中断所有`空闲`的线程,直至所有的线程终止。
3. 也就是说这时正在忙碌的线程不被中断,会继续执行队列中的任务
4. 不再接收新任务了(只有当状态是running时,execute()才会接收新任务)
shutdownNow():
1. 会将线程池状态改为STOP
2. 对所有的线程发送中断信息,尝试中断所有`空闲`的线程,直至所有的线程终止。
3. 清空阻塞队列中的所有任务
4. 不再接收新任务了(只有当状态是running时,execute()才会接收新任务)
异同点:
相同点:
1. 都会对所有的线程设置中断信号
2. 都不再接收新任务了
不同点:
1. shutdownNow:会对队列里所有的任务排除掉,并且状态改为STOP
2. shutdown:状态改为SHUTDOWN
源码如下:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown();
} finally {
mainLock.unlock();
}
tryTerminate();
}
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
// 清空阻塞队列中的所有任务,同时返回队列里的任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
注:这里中断线程本身使用的是interrupt方法,该方法只会将线程的状态改为中断标识,并不会立即中断线程的
重要方法
execute方法:主要负责是线程和任务的流转过程
addWorker方法:主要负责创建新线程,并启动线程执行
runWorker方法:执行第一个任务后,然后循环从getTask()方法取任务执行;直至当线程返回的任务为空,退出循环,并将该线程进行销毁
理解难点
- addWorker创建线程
addWorker()方法中线程t.start()方法启动线程执行怎么会调用runWorker()方法?
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
final Thread thread;
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
}
Worker本身是一个Runable实现类,在使用new Worker(firstTask)时,会将this即当前Worker实例作为新创建出来线程的执行方法体,当成员变量thread调用start方法时,本质上就会执行Worker类中的run方法,即调用runWorker(this);
本质上Worker相当于封装了一层,包含了要执行的任务和新创建的线程
-
对核心线程、非核心线程理解
只是逻辑上的概念,线程池上并没有区分这两个类型的线程。 当队列中没有任务线程会阻塞暂停,会通过ConditionObject调用await方法放入到队列中; 当队列中有任务时,唤醒的线程顺序是按照放入队列中的先后顺序
核心流程
当线程池对象调用execute(Runnable command)时,会调用addWorker()方法【会把第一个任务Runnable 作为成员变量给Worker对象,同时会创建以当前Worker为主的线程对象并将此对象赋值Worker的成员变量Thread thread】,当调用Worker对象中线程对象start()方法时即调用runWorker()方法。当前线程开始执行第一个任务,然后循环通过getTask方法获取队列中的任务一一执行,当队列中没有任务时就会线程就会暂停执行,非核心线程超过指定的时间久就会被删除
线程复用原理
processWorkerExit方法
新创建的线程一开始执行完第一个任务后,就会不断地通过getTask方法获取队列中的任务执行
- 当工作的线程大于核心线程数、或允许核心线程超时被删除,当poll在指定时间拿不到数据返回null后,会退出while循环,走finally方法当前线程会被剔除掉。(遇到异常时会抛出RuntimeException也会走finally方法会被剔除掉,但是会重新添加一个非核心线程)
- 否则从队列里若获取不到任务就会将当前线程暂停,线程是不会消耗cpu资源的(依赖于依赖于阻塞队列)
常用的线程池
newCachedThreadPool():有空闲线程就执行,没有空闲线程就创建新线程执行。
适用于大量且执行时间短的任务
newSingleThreadExecutor():自始至终只有一个人(线程)夜以继日的干活,来了任务线程在忙执行不了那就在队列等着。
适用于串行执行任务
newFixedThreadPool():固定线程数执行任务,不适合耗时时间长的任务执行任务,因为会导致任务队列存在很多任务甚至出现oom,只适合Cpu密集型的任务(偏计算型)
newScheduledThreadPool():延时 或 定期执行
合理的配置线程数
从以下几个角度
(一)初步判断,理论值
1. CPU密集型、IO密集型、混合密集型
CPU密集型:应该配置较少的核心线程数,cpu核数+1;
IO密集型:充分利用cpu阻塞等待的时间,让cpu忙碌起来应该配置多一些核心线程数,cpu核数*2
2. 任务的依赖性:是否依赖其他系统资源,如数据库连接。因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。
3. 任务执行长短
(二)通过压测、动态调整核心线程数与最大线程数来调整
阻塞队列
作用:用来缓冲存放并发的任务
ArrayBlockingQue:底层是数组,需要初始化大小
LindedBlockingQue:单向链表,默认是Integer.maxVal,相对于ArrayBlockingQue队列吞吐量更高(生产者,消费者各使用一把锁)
SynchronousQueue:不是基于锁来实现放入与取出任务,因此具有更高的吞吐量性质
拒绝策略
1. CallerRunsPolicy:让主线程执行此任务,但这样会降低提交任务的执行速度,降低了系统的整体性能;好处是但能保证提交的任务都能被执行
2. AbortPolicy:抛出RejectedExecutionException异常
3. DiscardPolicy:该线程不会做任何处理
4. DiscardOldestPolicy:删除任务队列最前面的任务,然后重新添加到队列中执行此任务(长此以往)
5. 自定义实现,如记录日志或持久化存储不能处理的任务
submit与execute方法的异同
@Test
public void test01() throws ExecutionException, InterruptedException, TimeoutException {
Future<?> submit = threadPool.submit(() -> {
TimeUnit.SECONDS.sleep(4);
return "张三";
});
// get请求会阻塞主线程获取线程池处理的结果
Object result = submit.get(1, TimeUnit.MINUTES);
System.out.println(result);
}
不同点:
1. submit方法底层依赖FutureTask类,执行任务本质执行的futuretask的run方法,当遇到异常会将异常吞掉,自然不会走到移除并创建新的线程逻辑
2. execute方法会把异常抛出,如果遇到异常会创建一个非核心线程放入池子里。
动态修改线程池核心数
可使用setCorePoolSize、setMaximumPoolSize方法可以重新配置线程数。但是要注意一点,如果只修改核心线程数,是不起作用的,因为getTask方法有逻辑校验如果当前正在运行的线程数大于了最大线程数,会返回Null,将当前线程进行remove掉,所以在修改时把最小最大都设置成一样的
队列长度不能够修改,是因为队列中容量是final修饰的
tomcat与jdk原生线程池有什么不同呢?
Tomcat线程池主要是为了处理IO密集型,但原生Jdk线程池使用的主要是解决cpu密集型,所以在原生线程池进行扩展
1. 在自定义ThreadPoolExecutor中,遇到异常的线程会线程池中被删除,然后创建一个非核心线程添加到线程池中。所以在使用自定义线程池时应该避免很多的异常出现;(completedAbruptly为true时表示的是线程出现异常了,false表示非核心线程超时后正常退出)
tomcat线程池中的线程遇到异常时是不会被销毁的,不会走processWorkerExit方法(待研究todo)
2. 创建线程、执行任务的顺序不同
- jdk原生线程池处理任务的顺序是:先创建核心线程执行任务,其次再放队列,然后创建非核心线程执行任务
- tomcat线程池处理任务的顺序是:先创建核心线程执行任务,其次创建非核心线程执行任务,然后添加任务到队列中,最后再执行拒绝策略。这样是为了保证响应时间优先
3. tomcat线程池ThreadPoolExecutor非jdk中ThreadPoolExecutor,它是继承了jdk中的线程池,使用jdk中的execute逻辑,有几点不同:
3.1:通过`自定义队列TaskQueue`改变了执行策略以实现优先使用完线程数,然后再放任务都队列中。
3.2:在当队列满的情况下不会立即执行拒绝策略,而是会将此任务重新放入队列中,尝试再次执行任务。当再次执行任务时如果还失败才会执行拒绝策略
3.3:默认最小线程数minSpareThreads = 10,默认最大线程数maxThreads = 200,默认队列长度是Integer.MAX_VALUE
参考:tomcat线程池流程
spring中@Async的坑
Spring中@Async注解开启异步线程执行任务
1. 当IOC容器没有添加线程池时,Spring使用的线程池是ThreadPoolTaskExecutor,自动装配类是TaskExecutionAutoConfiguration,其coreSize = 8,maxSize = Integer.MAX_VALUE,queueCapacity = Integer.MAX_VALUE。
2. 有IOC添加了一个自定义的线程池时,使用的线程池是SimpleAsyncTaskExecutor,这时当任务来一个执行创建一个新的线程处理,来一个创建一个新的新的线程处理....最终会导致资源耗尽
3. 当指定@Async指定了value具体的线程池时就会使用指定的线程执行任务
Spring提供的线程池
SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。@Async默认线程池
SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地
ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
ThreadPoolTaskScheduler:可以使用cron表达式
ThreadPoolTaskExecutor :最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装
如何自定义线程池命名
// 方式一
new ThreadPoolExecutor(1, 2, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)
, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread();
thread.setName("订单线程池");
return thread;
}
});
// 方式二
new ThreadPoolExecutor(1, 2, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)
, new CustomizableThreadFactory("订单线程池"));
线程池启动前优化
可以开启核心线程进行提前预热:prestartAllCoreThreads()
防止核心线程一直存在,也允许让核心线程超时被移除:allowCoreThreadTimeOut
如何实现线程池监控
重写线程池的beforeExecute、afterExecute、terminated方法逻辑,可以在每个任务执行前、执行后和线程池关闭前执行一些代码来进行监控
- 使用匿名内部类构造的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
2,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("beforeExecute");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("afterExecute");
}
@Override
protected void terminated() {
System.out.println("terminated");
}
};
如果线上宕机了,队列中的任务没有执行完怎么办?
这种情况下不使用额外手段一定会导致任务丢失的
解决方案:
任务执行前记录数据到库中,初始化其状是未处理,执行完后将状态改为已完成。然后开启定时任务重新提交处理未处理的任务到线程池中
案例
案例1:
10 个机器,1000 个请求并发,平均每个服务承担 100 个请求。服务器是 4 核的配置。怎么配置线程数?
那么如果是 CPU 密集型的任务,我们应该尽量的减少上下文切换,所以核心线程数可以设置为 5,队列的长度可以设置为 100,最大线程数保持和核心线程数一致。
如果是 IO 密集型的任务,我们可以适当的多分配一点核心线程数,更好的利用 CPU,所以核心线程数可以设置为 8,队列长度还是 100,最大线程池设置为 10。
当然,上面都是理论上的值。
我们也可以从核心线程数等于 5 开始进行系统压测,通过压测结果的对比,从而确定最合适的设置。
同时,我觉得线程池的参数应该是随着系统流量的变化而变化的。
所以,对于核心服务中的线程池,我们应该是通过线程池监控,做到提前预警。同时可以通过手段对线程池响应参数,比如核心线程数、队列长度进行动态修改。
上面的回答总结起来就是四点:
CPU密集型的情况。
IO密集型的情况。
通过压测得到合理的参数配置。
线程池动态调整。
案例2:
多线程获取组装数据
public class PoolTest {
private static ThreadPoolExecutor threadPoolExecutor;
static {
threadPoolExecutor = new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(100), new CustomizableThreadFactory("某业务线程池"), new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolExecutor.prestartAllCoreThreads();
threadPoolExecutor.allowCoreThreadTimeOut(true);
}
public static void main(String[] args) {
Future<String> submit = threadPoolExecutor.submit(() -> {
int tryCount = 0;
int maxTryCount = 3;
while (tryCount < maxTryCount) {
try {
System.out.println("error");
break;
} catch (Exception e) {
tryCount++;
if (tryCount == 2) {
System.out.println("日志记录");
}
}
}
return "ok";
});
}
/**
* 获取结果CF+自定义线程池
*/
@Test
public void hasResult01() {
List<Integer> nums = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
nums.add(i);
}
List<List<Integer>> partition = Lists.partition(nums, 15);
List<Integer> collect = partition.stream()
.map(part -> CompletableFuture.supplyAsync(() -> {
part.add(1);
return part;
}, threadPoolExecutor))
.peek(CompletableFuture::join).map(cf -> {
try {
return cf.get(3, TimeUnit.MINUTES);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
})
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(collect.size());
}
/**
* 获取结果 线程池+countdownLatch
*
* @throws InterruptedException
*/
@Test
public void hasResult02() throws InterruptedException {
List<Integer> nums = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
nums.add(i);
}
List<List<Integer>> partition = Lists.partition(nums, 15);
List<Integer> integersFromDb = new CopyOnWriteArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(partition.size());
for (List<Integer> numList : partition) {
threadPoolExecutor.execute(() -> {
try {
// System.out.println("numList = " + numList);
integersFromDb.addAll(numList);
countDownLatch.countDown();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
countDownLatch.await();
System.out.println(integersFromDb.size());
}
/**
* 不获取结果
*/
@Test
public void noResult01() {
List<Integer> nums = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
nums.add(i);
}
List<List<Integer>> partition = Lists.partition(nums, 15);
partition.stream().map(part -> {
return CompletableFuture.runAsync(() -> {
part.add(1);
}, threadPoolExecutor);
}).collect(Collectors.toList()).forEach(CompletableFuture::join);
}
/**
* 不获取结果
*/
@Test
public void noResult02() {
List<Integer> nums = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
nums.add(i);
}
List<List<Integer>> partition = Lists.partition(nums, 15);
for (List<Integer> numList : partition) {
threadPoolExecutor.execute(() -> {
try {
System.out.println("numList = " + numList);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
@Test
public void testThreadExecute() throws InterruptedException {
Thread thread = new Thread(new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return "ok";
}
}));
thread.start();
thread.join();
}
最佳实践
1. 给线程池起名字
2. 不同业务使用不同的线程池,防止线程出现饥饿
3. 线程池不要使用耗时的任务,耗时任务应该使用mq进行异步化
同步异步、阻塞非阻塞
同步、异步
- 同步在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
- 异步调用更像一个消息传递,在发出之后,这个调用就直接返回了。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
并发、并行
并发:多个任务快速交替的执行,从线程角度来看,是cpu时间片快速切换到多个线程,让线程交替工作的
并行:在同一时刻多个任务一块工作,从线程角度来看,不同的cpu各自执行各自的任务,互不打扰
同步、异步、并发、并行区别
同步、异步可以理解为是结果
并发、并行可以理解为是过程实现方式
阻塞与非阻塞
- 阻塞模式下,当被调用者尚未处理完数据,调用者不能做其他事情
- 非阻塞模式下,当被调用者尚未处理完数据,调用者可以做其他事情
BIO、NIO、AIO理解
-
同步阻塞(blocking-IO)简称BIO
同步并阻塞,在服务器中实现的模式为一个连接一个线程。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。
-
同步非阻塞(new-blocking-IO)简称NIO
同步并非阻塞,在服务器中实现的模式为一个请求一个线程,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器**轮询**到有连接IO请求时才会启动一个线程进行处理。
-
异步非阻塞(asynchronous-non-blocking-IO)简称AIO
用户程序只需将发送操作的请求发给内核,内核立即返回接收成功ok,然后内核将数据准备好,数据拷贝完成后会给用户程序发送成功信息,中间用户程序不用等待
IO模型如下
一次完整的IO操作过程如下:
1. 应用程序进程向操作系统发起IO调用请求
2. 操作系统准备数据,把IO外部设备的数据,加载到内核缓冲区
3. 操作系统拷贝数据,即将内核缓冲区的数据,拷贝到进程缓冲区
阻塞IO模型:当用户进程发起IO操作后,用户进程在等待内核进程数据准备,数据拷贝过程中一直处于阻塞等待状态,不能干其他事情
非阻塞IO模型:当用户进程发起IO操作后,`用户进程采用不断循环的方式查看数据是否准备好`,数据拷贝过程中用户进程一直处于阻塞等待状态,不能干其他事情
IO复用模型:当用户进程发起IO操作后,用户进程需等待内核进程中数据是否准备好,当准备好后会主动通知用户进程,`但此过程中用户进程一直处于阻塞状态,不能做其他事情`,数据拷贝过程中用户进程也是一直处于阻塞等待状态,不能干其他事情
信号驱动IO模型:当用户进程发起IO操作后,用户进程只需等待内核进程中数据是否准备好,当准备好后会主动通知用户进程,`但此过程中用户进程不会被阻塞,可以做其他事情`。但数据拷贝过程中用户进程一直处于阻塞等待状态,不能干其他事情
AIO模型:当用户进程发起IO操作后,用户进程可以得到立即响应,可以干其他事情,数据等待与数据拷贝过程中不会被阻塞,当处理完数据后会主动通知用户进程处理完毕,完全异步化
一个经典生活的例子:
- 小明去吃同仁四季的椰子鸡,就这样在那里排队,等了一小时,然后才开始吃火锅。(BIO)
- 小红也去同仁四季的椰子鸡,她一看要等挺久的,于是去逛会商场,每次逛一下,就跑回来看看,是不是轮到她了。于是最后她既购了物,又吃上椰子鸡了。(NIO)
- 小华一样,去吃椰子鸡,由于他是高级会员,所以店长说,你去商场随便逛会吧,等下有位置,我立马打电话给你。于是小华不用干巴巴坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了美味的椰子鸡(AIO)
异步非阻塞思想
前端提交一个请求,后端接收到了直接响应提交成功(并不是真正处理完成,只是响应接收到本次提交请求)。后端真正处理完后再主动通知前端处理成功,或者让用户去页面主动查询此次提交结果
实现同步的方式(深入理解JAVA虚拟机)
- 互斥同步(同上述BIO原理一致)
使用锁【synchronize,ReentrantLock】,使用诸如此类的本地锁,阻塞唤醒的开销是比较大的,因为一个Java线程映射对应一个操作系统的内核线程。线程上线文切换需要保护和恢复现场的工作,免不了涉及一系列数据在各种寄存器、缓存中的来回拷贝,CPU从用户态切换到内核态,代价比较大。这里是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁
- 非阻塞同步(同上述NIO原理一致)
使用乐观并发策略,使用前提是数据操作与冲突检测要是原子性的。如Java中的Unsafe类,实现了冲突检测与操作的原子性,是依赖于底层硬件设备实现的
-
无锁方式
重入方法、线程本地存储【ThreadLocal】、栈封闭(比如方法的局部变量,是在线程的局部变量表中)
jvm对synchronized优化
在jdk1.6时,虚拟机会对synchronized优化掉很大一部分不必要的加锁
无锁:使用乐观并发控制
,不在程序里加锁,依赖数据库层面对事务要求顺序访问
偏向锁:偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时
,那么该线程在后续访问时无须同步,仅需要判断一下就可以直接逻辑
轻量级锁:当有多个线程对共享资源产生竞争访问时,偏向锁就会升级为轻量级锁,其他线程会通过CAS自旋的形式尝试获取锁,不会阻塞
重量级锁:就是阻塞线程执行,存在用户态与内核态切换
如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,
那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了
锁其他优化
1. 锁粗化:在遇到连续对同一个锁不断请求和释放时,会整合扩展到更大范围锁的一次请求,从而减少对锁的请求同步的次数,这个操作叫作锁的粗化。
2. 锁消除:对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
@Test
public void test01() {
// 未优化前
for (int i = 0; i < 100; i++) {
synchronized (this) {
System.out.println(i);
}
}
// 优化后,进行锁粗化
synchronized (this) {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
实现异步选择mq还是线程池?
-
存在CPU竞争,而MQ不会消耗本机的CPU。
-
如果是与数据库交互情况下,要考虑线程数不宜过大,过大会并发线程会过大,可能占用更多的数据库连接,导致
数据库连接资源紧张
-
当业务系统处于高并发,MQ可以将消息堆积在Broker实例中,而多线程会创建大量线程,甚至触发拒绝策略。
-
使用MQ引入了中间件,增加了项目复杂度和运维难度。
总的来说,规模比较小的项目可以使用多线程实现异步,大项目建议使用MQ实现异步。
synchronized与ReentrantLock锁区别
ReentrantLock
1. 可以配合多个条件队列(ConditionObject)实现阻塞唤醒线程
2. 可以让长时间阻塞等待的线程响应中断
3. 允许限时等待
synchronized
1. 在jdk1.6及其之后有锁优化:无锁、偏向锁、轻量级锁、重量级锁,锁消除、锁粗化
2. 可自动释放锁(正常执行与异常执行)
两者如何选择呢?
尽量使用synchronized,这是底层提供的API,能自动释放锁,有效避免了死锁问题
如何先线程按指定顺序执行?
线程真正被执行是操作系统调度器决定的
1. Thread.join方法
2. 使用Executors.newSingleThreadPool(),本质上使用的是LinkedBlockingQueue,FIFO
3. 使用CompletableFuture.thenRunAsync().thenRunAsync()
线程安全篇章
线程不安全的原因?
1. 由于为了平衡cpu与内存的速度差异,引入了高速缓存,但高速缓存带来了数据不一致的问题,造成数据可见性的问题
2. 由于操作系统是分时复用CPU资源,线程通过获取cpu时间片时,由于上下文切换导致了原子性的问题
3. 由于JIT编译、CPU为了提高性能,对指令进行重排,导致了有序性的问题
为什么引入Java内存模型?
不同的操作系统中CPU遵循了一些协议,在读写操作时根据协议来进行操作,从而避免的上述的问题。Java虚拟机是一个软件,它也要屏蔽各种硬件与操作系统的内存访问差异带来的一系列的问题,决定也提供一个能保证一致性的内存访问效果的内存模型。所以引入了Java内存模型
Java内存模型内存访问有哪些要求?
1. Java内存模型规定了所有的变量(java的共享变量)都存储在主内存中,每条线程还有自己的工作内存
2. 线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行而不能直接读写主内存中的数据。
3. 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
Java中有哪些手段可实现线程安全?
1. 互斥同步,即使用synchronize,ReentrantLock
2. 非阻塞同步,使用cas方式,保证冲突检测与操作数据是原子性逻辑
3. 无同步,比如栈封闭、线程本地存储、可重入的方法
参考
08.004-自定义线程池-线程池-实现_哔哩哔哩_bilibili
Java并发面试题 - 大白菜博客 (cmsblogs.cn)
万长文字 | 16张图解开AbstractQueuedSynchronizer
《Java并发编程艺术》