一个多线程异步执行面试题的多种解决方法

1.问题

题目如下:
在 main 函数启动一个新线程,运行一个方法,拿到这个方法的返回值后,退出主线程?

public class Homework03 {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        // 在这里创建一个线程或线程池,
        // 异步执行 下面方法
        int result = sum();

        System.out.println("异步计算结果:"+result);
        System.out.println("计算耗时:"+(System.currentTimeMillis() - start) +"  ms");
        
    }
    
    private static int sum() {
        return fibo(36);
    }
    
    private static int fibo(int a) {
        if(a<2){
            return 1;
        }
        return fibo(a-1) + fibo(a-2);
    }
    
}

题目的实际要求是,在main线程中,启动一个线程或者线程池,来执行一个斐波那契数列的求和运算,之后在计算完毕之后,将计算结果返回到主线程。
对于这个问题,实际上就是两个线程,main线程和计算线程之间的通讯问题。主线程在启动计算线程之后,需要进入等待或者阻塞状态,直到等待的变量状态改变,或者被阻塞的任务执行完毕,之后再运行获取结果的方法,拿到计算结果。
大致可以分为 共享内存、join、同步工具等多种解决方案。

2.解决方法

2.1 线程的Join方法

线程的join方法本身就是jvm实现的,让当前线程进行阻塞,等待被执行线程结束之后再执行的方法。代码如下:

public class AsyncRun02 {

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();

        sumThread.start();
        sumThread.join();
        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {

        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            result = sum();
        }
    }

}

2.2 共享volatile变量

可以让两个线程共享一个volatile的变量,计算线程在计算完成之后,更新这个volatile变量的状态为ture,那么main线程只需要在计算线程启动之后,不断轮询监控该变量的状态即可。代码如下:

public class AsyncRun03 {

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();

        sumThread.start();

        while (!sumThread.isSuccess()) {
            TimeUnit.MILLISECONDS.sleep(1);
        }

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {

        private Integer result;
        private volatile boolean success = false;

        public boolean isSuccess() {
            return success;
        }

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            result = sum();
            success = true;
        }
    }

}

2.3 synchronized锁

synchronized可以实现这个需求,但是需要一个前提,就是让计算线程先拿到锁,这之后main线程被synchronized阻塞。直到计算线程执行完毕,代码如下:

public class AsyncRun04 {

    private static final Object lock = new Object();

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        
        TimeUnit.MILLISECONDS.sleep(1);
        synchronized (lock) {

        }
        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {

        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            synchronized (lock) {
                result = sum();
            }
        }
    }
}

2.4 wait+notify/notifyAll

可以利用object对象的wait+notify/notifyAll方法来实现。在启动完计算线程之后将主线程wait,之后在计算线程中,执行完毕之后,调用notify/notifyAll方法来唤醒主线程继续执行。
代码如下:

public class AsyncRun05 {

    private static final Object lock = new Object();

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();

        sumThread.start();
        synchronized (lock) {
            lock.wait();
        }

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {

        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            synchronized (lock) {
                result = sum();
                lock.notifyAll();
            }
        }
    }
}

2.5 park/unpark

同理,也可以利用同步工具中的lockSupport的park/unpark方法来实现。在主线程启动计算线程之后执行park,之后再在计算线程执行完毕之后,调用主线程的unpark方法。diamagnetic如下:

public class AsyncRun07 {
    
    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.setMainThread(Thread.currentThread());
        sumThread.start();
        LockSupport.park();

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {

        private Thread mainThread;

        private Integer result;

        public Integer getResult() {
            return result;
        }

        public Thread getMainThread() {
            return mainThread;
        }

        public void setMainThread(Thread mainThread) {
            this.mainThread = mainThread;
        }

        @Override
        public void run() {
            result = sum();
            LockSupport.unpark(getMainThread());
        }
    }

}

2.6 ReentrantLock可重入锁

同样,参考synchronized,也可以通过可重入锁ReentrantLock,但是需要先让计算线程抢到锁,之后main线程会被阻塞直到计算线程执行完毕。
代码如下:

public class AsyncRun08 {

    private static final ReentrantLock lock = new ReentrantLock(true);

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();

        TimeUnit.MILLISECONDS.sleep(1);
        lock.lock();
        try {
            System.out.println("***");
        } finally {
            lock.unlock();
        }

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {


        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                result = sum();
            } finally {
                lock.unlock();
            }
        }
    }

}

2.7 ReentrantLock配合Condation

当然,既然使用了ReentrantLock,那么还有一种与wait/notify等价的方法,就是结合Condation,通过Condation的await和signalAll/signal方法。
在启用了计算线程之后,通过Condation的await方法阻塞,待计算线程执行完毕再执行signal方法。
代码如下:

public class AsyncRun09 {

    private static final ReentrantLock lock = new ReentrantLock(true);
    private static final Condition c1 = lock.newCondition();

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();

        lock.lock();
        try {
            c1.await();
        } finally {
            lock.unlock();
        }

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {


        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                result = sum();
                c1.signalAll();
            } finally {
                lock.unlock();
            }
        }
    }
}

2.8 CountDownLatch

在java的并发包中,有很多同步的工具可以来实现这个场景,定义一个CountDownLatch,需要倒计时的线程为1,当main线程启动线程之后,让CountDownLatch执行await方法,计算线程在计算完毕之后,执行countdown方法。mian线程则会继续执行。
代码如下:

public class AsyncRun06 {

    private static final CountDownLatch latch = new CountDownLatch(1);

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();

        sumThread.start();
        latch.await();

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {

        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            result = sum();
            latch.countDown();
        }
    }

}

2.9 CyclicBarrier

同理,CyclicBarrier也可以在这个场景使用。主线程启动计算线程之后,执行await,之后计算线程执行完之后,也执行await,这个内存屏障设为2,则正好解除屏障,继续执行。
代码如下:

public class AsyncRun10 {

    private static final CyclicBarrier barrier = new CyclicBarrier(2);

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        barrier.await();

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {
        
        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            try {
                result = sum();
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

2.10 Semaphore

Semaphore也能很好的实现,Semaphore初始为0,在主线程中执行acquire,自然会被阻塞,等到计算线程执行完毕,执行release。
代码如下:

public class AsyncRun11 {

    private static final Semaphore semaphore = new Semaphore(0);

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        semaphore.acquire();

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {
        
        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            try {
                result = sum();
                semaphore.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.11 Phaser

Phaser如果要实现这个场景,则设置Phaser为2,在主线程和计算线程中都执行arriveAndAwaitAdvance。
代码如下:

public class AsyncRun12 {

    private static final Phaser phaser = new Phaser(2);

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        phaser.arriveAndAwaitAdvance();

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {


        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            try {
                result = sum();
                phaser.arriveAndAwaitAdvance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.12 Exchanger

Exchanger是最适合两个线程通信的工具。尤其是交替执行的两个线程。主线程和计算线程都通过exchange方法,同时被阻塞住,然后交换数据。
代码如下:

public class AsyncRun13 {

    private static final Exchanger exchanger = new Exchanger<>();

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        Integer result = exchanger.exchange(0);

        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {


        @Override
        public void run() {
            try {
                int result = sum();
                exchanger.exchange(result);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

2.13 BlockingQueue

BlockingQueue的take方法,也是有阻塞效果的,那么对于这一类的Queue,或者List,可以在主线程启动起算线程之后,通过take方法进行阻塞。而一旦计算线程执行完毕,将数据加入到queue,则阻塞被解除。采用ArrayBlockingQueue实现。
代码如下:

public class AsyncRun14 {

    private static final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1);

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        Integer result = queue.take();

        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {


        @Override
        public void run() {
            try {
                int result = sum();
                queue.add(result);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.14 SynchronousQueue

SynchronousQueue是一个特殊的Queue,只能允许1个长度,本质上与BlockingQueue的原理一致。

public class AsyncRun15 {

    private static final SynchronousQueue queue = new SynchronousQueue<>();

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        Integer result = queue.take();

        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {


        @Override
        public void run() {
            try {
                int result = sum();
                queue.add(result);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

2.15 List/Set非阻塞集合

在使用了阻塞的集合实现之后,同样,非阻塞的集合也能实现,这个与通过volatile共享变量类似。需要主线程轮询集合的状态,是否isEmpty。如下采用ArrayList实现。
代码如下:

public class AsyncRun16 {

    private static final ArrayList list = new ArrayList<>();

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        while (list.isEmpty()) {
            TimeUnit.MILLISECONDS.sleep(1);
        }
        Integer result = list.get(0);
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {
        
        @Override
        public void run() {
            try {
                int result = sum();
                list.add(result);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.16 Thread.isAlive

由于本题的特殊性,也可以不用第三变量,直接判断计算线程的状态,isAlive方法,在isAlive方法中轮询sleep,待计算线程执行完毕。
代码如下:

public class AsyncRun19 {

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        SumThread sumThread = new SumThread();
        sumThread.start();
        do {
            TimeUnit.MILLISECONDS.sleep(1);
        } while (sumThread.isAlive());

        int result = sumThread.getResult();
        System.out.println("异步计算结果:" + result);
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread extends Thread {

        private Integer result;

        public Integer getResult() {
            return result;
        }

        @Override
        public void run() {
            result = sum();
        }
    }
}

2.17 Callable

由于Runnable是没有返回值的,那么java再解决这个问题的时候引入了Callable,我们也可以利用Callable来实现。
代码如下:

public class AsyncRun01 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();

        ExecutorService executorService = Executors.newSingleThreadExecutor();

        Callable task = new Callable() {
            @Override
            public Integer call() throws Exception {
                return sum();
            }
        };
        Future future = executorService.submit(task);
        System.out.println("异步计算结果:" + future.get());
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
        executorService.shutdown();

    }

    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }
}

2.18 FutureTask

当然,还可以通过FutureTask来拿到线程异步执行的返回结果。
代码如下:

public class AsyncRun17 {


    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        ExecutorService service = Executors.newCachedThreadPool();

        SumThread sumThread = new SumThread();
        FutureTask futureTask = new FutureTask<>(sumThread);
        service.submit(futureTask);
        service.shutdown();
        System.out.println("异步计算结果:" + futureTask.get());
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread implements Callable {

        @Override
        public Integer call() throws Exception {
            return sum();
        }
    }
}

2.19 CompleteFuture

jvm1.8中引入了CompleteFuture,也能在这个场景中来使用。
代码如下:

public class AsyncRun18 {
    
    private static int sum() {
        return fibo(36);
    }

    private static int fibo(int a) {
        if (a < 2) {
            return 1;
        }
        return fibo(a - 1) + fibo(a - 2);
    }

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        ExecutorService service = Executors.newCachedThreadPool();

        CompletableFuture future = new CompletableFuture<>();
        SumThread sumThread = new SumThread(future);
        service.submit(sumThread);
        service.shutdown();

        System.out.println("异步计算结果:" + future.get());
        System.out.println("计算耗时:" + (System.currentTimeMillis() - start) + "  ms");
    }

    static class SumThread implements Runnable {

        private CompletableFuture future;

        public SumThread(CompletableFuture future) {
            this.future = future;
        }

        @Override
        public void run() {
            future.complete(sum());
        }
    }
}

3.总结

本文共列举了19种方法来实现异步执行线程并得到其执行结果的需求。这都是java线程通信的常用方法,每种方法的使用大同小异。采用轮询是最不可取的方法,这样会导致主线程浪费CPU资源,增加服务器不必要的开销。
类似于茴香豆的茴有几种写法,多少种并不是关键,我们需要的是掌握这每一种线程通信方法的使用场景,在业务开发中灵活应用。

你可能感兴趣的:(一个多线程异步执行面试题的多种解决方法)