聊聊多线程的执行流程

356 阅读4分钟

业务场景

多线程如何顺序执行?统计多线程执行时间?对多个线程执行结果进行合并或汇总处理?面试中可能都会被问到,为什么会问,当然是考察面试者的技术水平,但是,考察技术平并不是最终目的,而是看你在真实的业务场景中能否处理相应的问题。

技术是以业务为支撑的,针对不同的业务场景,能够给出相对较优的技术实现和解决方案,才是一个技术人员的价值所在。扎实的基础的好处在于,能够将自己的技术灵活地运用在各种场景中。

言归正传,今天笔者就来用代码案例来回答上面的问题,也相当于一个小小的总结吧。

多线程如何顺序执行

实现多线程的顺序执行,有两种方式

  • join方法
  • waitnotify方法

方式一: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

多线程通信问题,笔者之前也在《一道阿里面试题引发的思考》这篇文章中,谈到阿里的一道笔试题,考察的就是多线程通信的问题。

waitnotify相关的面试题其实都有一个通用的套路:就是根据一个标志变量判断是否等待,别的线程去唤醒,被唤醒后,执行本线程的任务后,改变标志变量的值,然后唤醒对应的线程。

一般都是配合锁进行线程等待唤醒的控制,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()方法可以实现,但是这样线程实际上是以串行方式在执行,比较简单,这里不做演示
  • CountDownLatchCyclicBarrier可以实现
  • 线程池可以实现,看后面多线程结果汇总的案例

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你会怎么优化上面的代码呢?