并发编程不是一项孤立存在的技术,也不是脱离现实生活场景而提出的一项技术,而是一项综合性的技术,并且与现实生活场景有着紧密的联系。
异步编程是让程序并发运行的一种手段,它允许多个事件同时发生,当程序调用需要长时间运行的方法时,它不会堵塞当前的执行流程,程序可以继续运行。
- 并发:多线程操作;
- 异步:主线程中添加子线程,子线程成功与否,运行到什么阶段不影响主线程;
并发编程有三大核心问题(选自《深入理解高并发编程:核心原理与案例实战》):
-
分工问题
关于分工,就是一个比较大的任务被拆分成多个大小合适的任务,这些大小合适的任务被交给合适的线程去执行。分工注重的是执行的性能,现实生活场景如下:
如果你是一家上市公司的CEO,那么你的主要工作就是规划公司的战略方向和管理好公司,就管理好公司而言,涉及的任务就比较多了。
这时可以将管理好公司看作是一个比较大的任务,这个比较大的任务包括人员招聘与管理,产品设计、产品研发、产品运营、产品推广、税务和计算等。如果将这些任务都交给CEO一个人去做,那么就算CEO被累趴下就做不完的,工作任务示意图如下
若是公司CEO一个人做所有的工作任务是一种非常糟糕的方式,这将导致公司无法正常运营,那么该如何做呢?就是分工。
将人员招聘与管理交给人力资源部,将产品设计交给设计部,将产品研发交给研发部,将产品运营交给运营部,将产品推广交给市场部,将税务和计算交给财务部,这样,公司CEO的重点工作就变成了及时了解各部门的工作情况,统筹协调各部门的工作,并思考如何规划公司的战略发展,工作任务示例图如下
将管理好公司这个任务进行工作分工后,可以发现各部门之间的工作可以并行推进。如人力资源部在进行员工的绩效考核时,设计部和研发部正在设计和研发公司产品,与此同时,公司的运营人员正在和设计人员和研发人员沟通如何更好的完善公司的产品,而市场部正在加大力度宣传和推广公司产品,财务部正在统计各种财务报表,大家分工明确,工作稳步同时进行。
所以在现实生活中,安排合适的人去做合适的事情是非常重要的,在并发编程领域同样如此。
在并发编程中,需要将一个比较大的任务拆分成若干个比较小的任务,并将这些小任务交给不同的线程去执行,如下
在并发编程中,由于多个线程可以并发执行,所以在一定程度上能够提高任务的执行效率,提升性能。但注意该由主线程执行的任务不要交给子线程去执行,否则是解决不了问题的。如现实生活场景中CEO将规划公司未来的工作交给研发人员一样,不仅不能规划好公司未来,可能会与公司的价值背道而驰。
在java中,线程池、Fork/Join框架和Futrure接口都是实现分工的方式,在多线程设计模式中,生产者消费者模式是典型的分工实现方式。 -
同步问题
关于同步,就是指一个线程执行完自己的任务后,以何种方式来通知其他的线程继续执行任务,也可以理解为线程之间的协作。同步注重的是执行的性能,现实生活场景如下:
如果有一个项目由曹操、孙权和刘备共同开发,曹操是一名前端研发人员,需要等待孙权的接口任务完成后再渲染页面,而孙权又需要等待刘备的数据层服务开发完成后再写接口服务。
可见,任务之间是存在依赖关系的,前面的任务完成之后才能执行后面的任务。
在现实生活场景中,这种同步更多的是靠人与人之间的沟通来实现,任务同步示意图如下
由图可以看出,曹操、孙权和刘备的任务之间是有依赖关系的。
在并发编程中,同步机制指一个线程的任务执行完成后,通知其他线程继续执行任务的方式,如下
由图可以看出,多个线程之间的任务是有依赖关系的,线程1需要堵塞等待线程2执行完任务后才能开始执行,线程2需要堵塞等待线程3执行完任务后才能开始执行。线程3执行完会唤醒线程2继续执行,线程2执行完后会唤醒线程1继续执行任务。
这种线程之间的同步,可以使用if来表示
if(线程1的任务完成) {
执行当前任务
} else {
继续等待
}
在实际场景中,往往需要及时判断出依赖的任务是否完成,这是可以使用while循环来表示
while(线程1的任务未完成) {
继续等待
}
执行当前任务
在多线程设计模式中,生产者消费者模型也是典型的同步实现机制。如果队列已满,则生产者需要等待,如果队列不满,则需要唤醒生产者线程;如果队列为空,则消费者需要等待,如果队列不为空,则需要唤醒消费者。
// 生产者
while(队列已满) {
生产者线程等待
}
唤醒生产者线程
// 消费者
while(队列为空) {
消费者线程等待
}
唤醒消费者线程
在java中,synchronized、Lock、Semaphore、CountDownLatch、CyclicBarrier等工具都是实现同步机制。
- 互斥问题
关于互斥,就是指在同一时刻只允许一个线程访问临界区的共享资源。互斥注重的是多个线程执行任务时的正确性,现实生活场景如下:
如果人民公园只有一个单人的公共卫生间,众多游客都需要去卫生间,示意图如下
由图可以看出,当多个游客都想去卫生间时,由于卫生间只能容纳一个游客,所以其他游客需要等待前面的游客使用完之后,再依次有序的使用,这就是典型的互斥场景。
在并发编程中,分工和同步注重的时任务的执行性能,而互斥注重的是执行任务的正确性,也就是线程的安全问题。
如果在并发编程中,多个线程同时进入临界区访问同一个共享变量,则可能产生线程安全问题,这是由线程的原子性、可见性和有序性问题导致的,而在并发编程中解决原子性、可见性和有序性问题的核心方案是线程之间的互斥。
在java中,synchronized、Lock可以实现多个线程之间的互斥,示意如下
// 修饰方法
public synchronized void methodName(){
//省略具体方法
}
// 修饰代码块
public void methodName(){
synchronized(this){
//省略具体方法
}
}
或
public void methodName(){
synchronized(ClassName.class){
//省略具体方法
}
}
还有原子类、并发容器类等都实现了线程的互斥机制。
异步编程核心问题:
关于异步,就是采用多线程优化性能,将串行操作变成并行操作。异步注重的是减少线程等待,提高系统的性能,降低延时,现实生活场景如下:
在京东商城下单买了一个手机,示意图如下
当用户下单时,要经历的业务逻辑流程还是很长的,每一步都要耗费一定的时间,于是开始思考能不能将一些非核心业务从主流程中剥离出来,示意图如下
异步编程实现方式:
1、线程Tread
直接继承Thread类是创建线程最简单的方式,如下
public class ThreadTest {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("This is a thread -" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
每次创建一个线程,频繁的创建、销毁,浪费了系统资源,这时可以采用线程池,如下
@Bean(name = "executorService")
public ExecutorService executorService() {
return new ThreadPoolExecutor(20, 40, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000),
new ThreadFactoryBuilder().setNameFormat("defaultExecutorService-%d").build(),
(r, executor) -> log.error("defaultExecutor pool is full! "));
}
2、Future
上面方式可以创建多个线程并行处理,但有些业务不仅仅要执行过程,还需要执行结果。Future类位于java.util.concurrent包下,包含方法如下
public class CallableAndFuture {
public static ExecutorService executorService = new ThreadPoolExecutor(
4,
40,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024),
new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build(),
(r, executor) -> System.out.println("defaultExecutor pool is full! "));
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "异步处理,Callable 返回结果";
}
}
public static void main(String[] args) {
Future<String> future = executorService.submit(new MyCallable());
try {
System.out.println(future.get());
} catch (Exception e) {
} finally {
executorService.shutdown();
}
}
}
Future表示一个可能还没有完成的异步任务的结果,通过get方法获取执行结果,该方法会堵塞直到任务返回结果。
3、FutureTask
FutureTask 实现了 RunnableFuture 接口,则 RunnableFuture 接口继承了 Runnable 接口和 Future 接口,也能用来获得任务的执行结果。
public static ExecutorService executorService = new ThreadPoolExecutor(
4,
40,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024),
new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build(),
(r, executor) -> System.out.println("defaultExecutor pool is full! "));
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
System.out.println("子线程开始计算:");
Integer sum = 0;
for (int i = 1; i <= 100; i++)
sum += i;
return sum;
});
executorService.submit(futureTask);
try {
System.out.println(futureTask.get());
} catch (Exception e) {
} finally {
executorService.shutdown();
}
}
可见,Callable 和 Future 的区别是Callable 用于产生结果,Future 用于获取结果。
4、CompletableFuture
Future 类通过 get() 方法阻塞等待获取异步执行的运行结果,性能比较差。
CompletableFuture 类是基于异步函数式编程。相对阻塞式等待返回结果,CompletableFuture 可以通过回调的方式来处理计算结果,实现了异步非阻塞,性能更优。
public class CompletableFutureTest {
public static void main(String[] args) {
//任务1:洗水壶->烧开水
CompletableFuture<Void> f1 =
CompletableFuture.runAsync(() -> {
System.out.println("T1:洗水壶...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1:烧开水...");
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//任务2:洗茶壶->洗茶杯->拿茶叶
CompletableFuture<String> f2 =
CompletableFuture.supplyAsync(() -> {
System.out.println("T2:洗茶壶...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T2:洗茶杯...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T2:拿茶叶...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "龙井";
});
//任务3:任务1和任务2完成后执行:泡茶
CompletableFuture<String> f3 =
f1.thenCombine(f2, (__, tf) -> {
System.out.println("T1:拿到茶叶:" + tf);
System.out.println("T1:泡茶...");
return "上茶:" + tf;
});
//等待任务3执行结果
System.out.println(f3.join());
}
}
还需不断尝试和理解其用法和底层原理。
5、@Async
在springboot中提供了注解@Async,也能实现异步编程。具体使用不再说明。