业务场景
多线程如何顺序执行?统计多线程执行时间?对多个线程执行结果进行合并或汇总处理?面试中可能都会被问到,为什么会问,当然是考察面试者的技术水平,但是,考察技术平并不是最终目的,而是看你在真实的业务场景中能否处理相应的问题。
技术是以业务为支撑的,针对不同的业务场景,能够给出相对较优的技术实现和解决方案,才是一个技术人员的价值所在。扎实的基础的好处在于,能够将自己的技术灵活地运用在各种场景中。
言归正传,今天笔者就来用代码案例来回答上面的问题,也相当于一个小小的总结吧。
多线程如何顺序执行
实现多线程的顺序执行,有两种方式
join方法wait和notify方法
方式一:join
如果我们查看join方法的底层,其实也是使用wait和notify实现的。那么到底wait谁,notify谁呢?
一般来说,如果我们在main方法定义一个t1线程,然后执行t1.join(),那么main线程就会等待t1线程执行完后再执行,利用这个特性,我们就可以实现多个线程的顺序执行。
看下面的案例,多线程会按顺序打印ABC
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("A");
});
t1.start();
t1.join();
Thread t2 = new Thread(()->{
System.out.println("B");
});
t2.start();
t2.join();
Thread t3 = new Thread(()->{
System.out.println("C");
});
t3.start();
t3.join();
}
方式二:wait和notify
多线程通信问题,笔者之前也在《一道阿里面试题引发的思考》这篇文章中,谈到阿里的一道笔试题,考察的就是多线程通信的问题。
wait和notify相关的面试题其实都有一个通用的套路:就是根据一个标志变量判断是否等待,别的线程去唤醒,被唤醒后,执行本线程的任务后,改变标志变量的值,然后唤醒对应的线程。
一般都是配合锁进行线程等待唤醒的控制,synchronized内部一般使用obj.wait()和obj.notify()进行等待唤醒操作,Lock与之相似,使用condition.await()和condition.signal()进行等待唤醒操作。
需求:三个线程分别打印ABC,按照ABC的顺序交替打印,各自打印五次。
public class SequenceThreadDemo {
public static void main(String[] args) {
PrintSequence ps = new PrintSequence();
new Thread(()->{
for (int i = 0; i < 5; i++) {
ps.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
ps.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
ps.printC();
}
},"C").start();
}
}
class PrintSequence{
private Lock lock = new ReentrantLock();
//flag=1代表线程1打印A,flag=2代表线程2打印B,flag=3代表线程3打印C
private int flag = 1;
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
//打印A
public void printA(){
lock.lock();
try{
if(flag!=1){
try {
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"--A");
flag = 2;
condition2.signal();
}finally {
lock.unlock();
}
}
//打印B
public void printB(){
lock.lock();
try{
if(flag!=2){
try {
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"===B");
flag = 3;
condition3.signal();
}finally {
lock.unlock();
}
}
//打印C
public void printC(){
lock.lock();
try{
if(flag!=3){
try {
condition3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+">>>>>C");
flag = 1;
condition1.signal();
}finally {
lock.unlock();
}
}
}
统计多线程执行时间
- join()方法可以实现,但是这样线程实际上是以串行方式在执行,比较简单,这里不做演示
CountDownLatch和CyclicBarrier可以实现- 线程池可以实现,看后面
多线程结果汇总的案例
CountDownLatch统计多线程执行时间
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
long start = System.currentTimeMillis();
for (int i = 0; i < 3; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"===> 在执行");
Thread.sleep((int)(Math.random()*100)); //睡眠0-100毫秒的随机数,模拟业务处理时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
}
latch.await();
long time = System.currentTimeMillis()-start;
System.out.println(time);
}
CyclicBarrier统计多线程执行时间
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
CyclicBarrier c = new CyclicBarrier(4);
long start = System.currentTimeMillis();
for (int i = 0; i < 3; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"===> 在执行");
Thread.sleep((int)(Math.random()*100)); //睡眠0-100毫秒的随机数,模拟业务处理时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
}
c.await();
long time = System.currentTimeMillis()-start;
System.out.println(time);
}
对多线程结果汇总
在实际的业务中,可能是对请求的多个接口返回的数据进行合并汇总,或者是对查询多条sql的返回的数据进行合并汇总,包装到一个Response对象里面,以json格式返回给客户端。
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
List<Future<Integer>> futures = new CopyOnWriteArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
Future<Integer> future = service.submit(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return (int) (Math.random() * 100); //模拟业务处理返回的结果;
});
futures.add(future);
}
futures.forEach((future)-> {
try {
//实际开发中这里是把结果封装到一个类中,最后以转换成json的格式返回
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
System.out.print("执行时间:");
System.out.println(System.currentTimeMillis()-start);
service.shutdown();
}
CompletableFuture
JDK1.5出来之后,Future可以获得线程执行的结果,可谓是一大进步。但是如果你再去看看近几年很火的JavaScript,事件驱动和异步回调早已经用得淋漓尽致,而Future虽然可以获得线程的执行结果,但是没有一个回调机制来处理结果,Future想要获得结果,要不轮询,要不阻塞。这样一对比,还是一种落后。
Guava和Netty框架的作者们于是重新扩展了Future接口,真正地实现了异步编程,Java也吸收了他们的经验,在JDK1.8终于推出了自家的异步APICompletableFuture,当然,Java8可谓是一次全新的跨越,Lambda表达式,Stream API,Optional等等,函数式编程也开始被越来越多地被用到实际的项目中。
如果用CompletableFuture你会怎么优化上面的代码呢?