京东的这道面试题你会吗?

详解一道京东面试题

  • 跟多精彩请关注公众号“xhJaver”,京东java工程师和你一起成长

多线程并发执行?线程之间通信?
这是我偶尔听到我同事做面试官时问的一道题,感觉很有意思,发出来大家和大家讨论下

面试题目描述

现在呢,我们有三个接口,就叫他A,B,C吧,这三个接口都是查询某个人征信信息的,必须同时返回true,我们才认为这个人的征信合格,如果其中某一个返回false的话,就表明这个人的征信不合格,如果是你,你会怎么设计怎么写这个代码呢?

第一次思考

首先,一定是并发执行,假如说A接口执行3秒,B接口执行5秒,C接口执行8秒的话

  • 串行执行: 3+5+8 = 16秒
  • 并发执行: 8=8秒
    (时间最久的那个接口执行的时间就是这三个接口的执行总时间)

熟悉的感觉,多线程执行任务,我在第二章文章实战!xhJaver竟然用线程池优化了。。。有提过怎么写,感兴趣的读者可以回去看一下,不过我在这里再写一下,话不多说来看下代码

并发代码

建议用PC端查看,所有代码都可直接复制运行,代码中重要的点都有详细注释

  1. 首先,我们先定义这三个接口

public class DoService {
    //设置A B C接口的返回值,b接口设置的是false
    private static Boolean flagA = true;
    private static Boolean flagB = false;
    private static Boolean flagC = true;

    public static   boolean A(){
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            System.out.println("a被打断  耗时" + (System.currentTimeMillis() - start));
               e.printStackTrace();
        }
        System.out.println("a耗时  "+(System.currentTimeMillis() - start));
        return flagA;
    }

    public static boolean B() {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            System.out.println("b被打断  耗时" + (System.currentTimeMillis() - start));
            e.printStackTrace();
        }
        System.out.println("b耗时  "+(System.currentTimeMillis() - start));
        return flagB;
    }

    public static boolean C()  {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(8000L);
        } catch (InterruptedException e) {
            System.out.println("c被打断  耗时" + (System.currentTimeMillis() - start));
            e.printStackTrace();
        }
        System.out.println("c耗时  "+(System.currentTimeMillis() - start));
        return flagC;
    }
}

  1. 其次 我们先创造一个Task 任务类

public class Task implements Callable<Boolean> {
 
    private String taskName;
    private Integer i;
    public  Task(String taskName,int i){
        this.taskName =taskName;
        this.i = i;
    }
    
    @Override
    public Boolean call() throws Exception {
        // 标记 返回值,代表这个接口是否执行成功
        Boolean flag = false;
        //记录接口名字
        String serviceName = null;
        //根据i的值来判断调用哪个接口
        if (i==1){
            flag   =    DoService.A();
            serviceName="A";
        }
        if (i==2){
            flag   =    DoService.B();
            serviceName="B";
        }
        if (i==3){
            flag   =    DoService.C();
            serviceName="C";
        }
        System.out.println("当前线程是: "+Thread.currentThread().getName()+"正在处理的任务是: "+this.taskName+"调用的接口是: "+serviceName);
        return flag;
    }
}
  1. 最后,我们定义一个测试类

class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个包含三个线程的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        //事先准备好储存结果的list集合
        List< Future<Boolean> > list = new ArrayList<>();

        //开始计时
        long start = System.currentTimeMillis();

        for (int i=1;i<4;i++){
            Task task = new Task("任务"+i,i);
            //将每个任务提交到线程池中,并且得到这个线程的执行结果
            Future<Boolean> result = executorService.submit(task);
            list.add(result);
        }

        //记得把线程池关闭
        executorService.shutdown();

        //定义一个变量 0
        int count = 0;
        System.out.println("等待处理结果。。。");
        for (int i=0;i<list.size();i++){
            Future<Boolean> result = list.get(i);
            //得到处理的结果 线程阻塞,如果线程没有处理完就一直阻塞
            Boolean flag = result.get();
            //如果这个接口返回true,那么count就++
            if (flag){
                count++;
            }
        }
        System.out.println("线程池+结果处理时间:"+ (System.currentTimeMillis() - start));
        //如果count数量为3,那么三个就都为true,代表这个人征信没问题
        if (count==3){
            System.out.println("合格");
        }else {//否则,就是有问题
            System.out.println("不合格");
        }
    }
}

  1. 我们看下输出结果
等待处理结果。。。
a耗时  3000
当前线程是: pool-1-thread-1正在处理的任务是: 任务1调用的接口是: A
b耗时  5000
当前线程是: pool-1-thread-2正在处理的任务是: 任务2调用的接口是: B
c耗时  8000
当前线程是: pool-1-thread-3正在处理的任务是: 任务3调用的接口是: C
线程池+结果处理时间:8008
不合格
  • 我们运行的时候会发现,它的输出结果的顺序如下1 2 3 4 5
    多线程执行结果1.png

我们图中的2,3,4是再线程池内开了三个线程执行的,他们之间相隔一段时间才出现的,因为每个接口都有执行时间

程序运行后,“标记2”是3秒后出现,“标记三”是5秒后出现,“标记4”是8秒后出现

其实4和5相差时间很短,几乎是同时出现的,因为4执行完了就是主线程继续执行了

线程池+结果处理的时间一共是8秒,而每个接口分别执行的时间是3秒,5秒,8秒,达到了我们所说的,多线程处理多个接口,总共耗时时间是耗时最长的接口的时间

和京东面试官探讨

波哥说(我爱叫他波哥,东北人,说话则逗,幽默的人简直就是人间瑰宝,其实我也蛮有趣的,就是没人发现),你这程序不行啊,有个缺点,假如说,你这个A接口,耗时三秒,他返回了false,那么你另外两个线程也不用执行了,这个人的征信已经不合格了,你需要判断下,如果某一个线程执行的任务返回了false,那么就及时中断其他两个线程

灵光乍现

上一次的代码已经实现了多线程执行任务,可是这线程间通信怎么办呢?怎么才能根据一个线程的执行结果而打断其他线程呢?我想到了以下几点

  1. 共享变量 public static volatile boolean end = true;

    • 这个共享变量就代表是否结束三个线程的执行
      如果为true的话,代表结束,false的话代表不结束线程执行
  2. 计数器 public static AtomicInteger count =new AtomicInteger(0);

    • 每当每个线程执行完的话,如果返回true,计数器就+1,当计数器变为3的时候,就代表这个人征信没问题
  3. 中断方法 interrupt()

    • 我们会单独开个线程一直循环检测这个变量,当检测到为true的时候,就会调用中断方法中断这三个线程
  4. 阻塞线程 countDownLatch

    • 我们程序往下执行需要获取结果,获取不到这个结果的话,就要一直等着。我们可以用这个线程阻塞的工具,一开始给他设置数量为1,当满足继续向下执行的条件时,调用countDownLatch.countDown();,在主线程那里countDownLatch.await();一下这样当检测到数量为0的时候,主线程那里就继续往下执行了,话不多说,来看代码

代码优化

建议用PC端查看,所有代码都可直接复制运行,代码中重要的点都有详细注释

  1. 首先,还是创建接口

public class DoService {

    private static Boolean flagA = true;
    private static Boolean flagB = false;
    private static Boolean flagC = true;

    public static   boolean A(){
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            System.out.println("a被打断  耗时" + (System.currentTimeMillis() - start));
               e.printStackTrace();
        }
        System.out.println("a耗时  "+(System.currentTimeMillis() - start));
        return flagA;
    }

    public static boolean B() {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            System.out.println("b被打断  耗时" + (System.currentTimeMillis() - start));
            e.printStackTrace();
        }
        System.out.println("b耗时  "+(System.currentTimeMillis() - start));
        return flagB;
    }

    public static boolean C()  {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(8000L);
        } catch (InterruptedException e) {
            System.out.println("c被打断  耗时" + (System.currentTimeMillis() - start));
            e.printStackTrace();
        }
        System.out.println("c耗时  "+(System.currentTimeMillis() - start));
        return flagC;
    }
}
  1. 创建任务

public class Task implements Runnable {

    private String name ;
    public Task( String name){
        this.name = name;
    }
    
    @Override
    public void run() {
        boolean flag = false;
        String serviceName = null;
        if(this.name.equals("A")){
            serviceName = "A";
             flag = DoService.A();
        }
        if(this.name.equals("B")){
            serviceName = "B";
            flag = DoService.B();
        }
        if(this.name.equals("C")){
           serviceName = "C";
           flag = DoService.C();
       }
        //如果有一个为false
        if (!flag){
            //就把共享标志位置为false
            Test.end = false;
        }else {
            //计数器加一,到三的话就是三个都为true
            Test.count.incrementAndGet();
       }
        System.out.println("当前线程是: "+Thread.currentThread().getName()+"正在处理的任务是: "+this.name+"调用的接口是: "+serviceName);
    }
}

  1. 创建测试类

class Test {
    //设置countDownLatch 里面计数为1,
    // 只调用一次countDownLatch.countDown就可以继续执行 countDownLatch.await();
    //后面的代码了,接触阻塞
    public static CountDownLatch countDownLatch = new CountDownLatch(1);

    //默认都为true,有一个线程为false了,那么就变为false
    public static volatile boolean end = true;
    //计数器,数字变为3的时候代表三个接口都返回true,线程安全的原子类
    public static AtomicInteger count =new  AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        long start = System.currentTimeMillis();

        //创建三个任务,分被调用A B C 接口
        Task taskA = new Task("A");
        Task taskB = new Task("B");
        Task taskC = new Task("C");

        //创建三个线程
        Thread tA = new Thread(taskA);
        Thread tB = new Thread(taskB);
        Thread tC = new Thread(taskC);

        //开启三个线程
        tA.start();
        tB.start();
        tC.start();

        //在开启一个线程,这个线程就是单独循环扫描这个共享变量的
        new Thread(new Runnable() {
            @Override
            public void run() {
                //此线程一直循环判断这个结束变量,如果为false的话,就代表有一个接口返回false,跳出,重点其他线程
                while (true){
                    if (!end ){
                        //当这个共享变量为false时i表示,其他线程可以中断了,所以就打断他们执行
                        tA.interrupt();
                        tB.interrupt();
                        tC.interrupt();
                        //如果某个线程被打断的话,就表明不合格
                        System.out.println("不合格");
                        //countDownLatch 计数器减一
                        countDownLatch.countDown();
                        break;
                    }
                    if (Test.count.get()==3){
                        System.out.println("合格");
                        //countDownLatch 计数器减一
                        countDownLatch.countDown();
                        break;
                    }
                }
            }
        }).start();
        System.out.println(Thread.currentThread().getName()+"主线程开始挂起");
        //阻塞主线程继续执行,等待其他线程计算完结果在执行下去,countDownLatch中的计数为0时,就可以继续执行下去
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+" 主线获得结果后继续执行"+(System.currentTimeMillis() - start));
    }

}
  1. 我们看下输出结果
main主线程开始挂起
a耗时  3024
当前线程是: Thread-0正在处理的任务是: A调用的接口是: A
b耗时  5000
当前线程是: Thread-1正在处理的任务是: B调用的接口是: B
c被打断  耗时5001
不合格
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at com.xhj.concurrent.executor_05._02.DoService.C(DoService.java:41)
    at com.xhj.concurrent.executor_05._02.Task.run(Task.java:30)
    at java.lang.Thread.run(Thread.java:748)
c耗时  5003
当前线程是: Thread-2正在处理的任务是: C调用的接口是: C
main 主线获得结果后继续执行5014

  • 我们运行的时候会发现


    中断其他线程

由图可见,我们首先就把主线程挂起,等待其他四个线程的处理结果,三个线程分别处理那三个接口,另外一个线程循环遍历那个共享变量,当检测到为false时,及时打断其他线程,这样的话,就解决了上面的那个问题

  • 跟多精彩请关注公众号“xhJaver”,京东java工程师和你一起成长

往期推荐

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351