三道被问烂了的多线程面试题,你知道多少?

1,850 阅读7分钟

先赞后看,养成习惯 🌹
欢迎微信关注 Java编程之道
每天进步一点点,沉淀技术分享知识。

三道被问烂了的多线程面试题

面试题一:利用多线程技术实现生产者-消费者模型

什么是生产者-消费者模型

通俗的讲,就是生产者在不断的生产,消费者也在不断的消费。生产者生成了东西通知消费者进行消费,没有东西了消费者则等到生产者生成。从而通过一个中间容器或通信手段来解决生产者和消费者的强耦合问题。

这也就引出一种简单的生产者-消费者实现方式

使用阻塞队列

生产者

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 14:24
 * @Version 1.0
 */
public class Product extends Thread {
    private LinkedBlockingDeque<String> stringLinkedBlockingDeque;

    Product(LinkedBlockingDeque<String> stringLinkedBlockingDeque) {
        this.stringLinkedBlockingDeque = stringLinkedBlockingDeque;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            int nextInt = new Random().nextInt(5000);
            try {
                stringLinkedBlockingDeque.put("物品" + i);
                System.out.println("生产者生产:" + i);
                sleep(nextInt);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }
}

消费者

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 14:23
 * @Version 1.0
 */
public class Consumer extends Thread {
    private LinkedBlockingDeque<String> stringLinkedBlockingDeque;

    Consumer(LinkedBlockingDeque<String> stringLinkedBlockingDeque) {
        this.stringLinkedBlockingDeque = stringLinkedBlockingDeque;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            int nextInt = new Random().nextInt(10000);
            try {
                String take = stringLinkedBlockingDeque.take();
                System.out.println(Thread.currentThread()+":消费者消费:"+take);
                sleep(nextInt);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }
}

测试类

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 14:30
 * @Version 1.0
 */
public class MainTest {
    public static void main(String[] args) {
        LinkedBlockingDeque<String> strings = new LinkedBlockingDeque<String>(4);
        new Product(strings).start();
        new Consumer(strings).start();
        new Consumer(strings).start();
        new Consumer(strings).start();
    }
}

测试结果

这里我们可以看到通过阻塞队列实现了一个多线程的生产者消费者模型,只要队列里面有物品,消费者就调用阻塞的take方法进行消费,如果没有物品则进行等待。三个消费者互不影响也不存在重复消费的情况。

使用线程协作

想要举一反三的通过多线程协作来实现生产者消费者模型,你必须彻底弄清除wait(),notify(),notifyAll()下面就通俗的告诉大家这三者是用来干什么的。

  • 你必须知道这三者都是Object类的方法,不是线程独自持有的方法。
  • 你必须知道wait()的作用是将当前线程置于预执行队列,并在wait()所在的代码处停止,等待唤醒通知。
  • 你必须知道wait(),notify()只能在同步代码块或者同步方法中执行,否则就会抛出异常。
  • 你必须知道wait()方法调用后悔释放出锁,线程与其他线程竞争重新获取锁。
  • 你必须知道notify(),会通知那些等待当前线程对象锁的线程,并使它们重新获取该线程的对象锁,如果等待线程比较多的时候,则有线程规划器随机挑选出一个呈wait状态的线程。
  • 你必须知道notifyAll会唤醒所有等待当前线程对象锁线程,具体哪一个线程将会第一个处理取决于操作系统的实现。

代码实现:

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 15:16
 * @Version 1.0
 */
public class Goods {
    private int id;
    private String name;
    public Goods(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

生产者

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 15:17
 * @Version 1.0
 */
public class Producer implements Runnable {
    private Goods goods;
    private Object lock;
    private Vector<Goods> container;

    Producer(Object lock, Vector<Goods> container) {
        this.lock = lock;
        this.container = container;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                goods = new Goods(1, "商品");
                if (container.size() < 5) {
                    container.add(goods);
                    System.out.println(Thread.currentThread().getName() + "生产商品");
                } else {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
    }
}

消费者

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 15:17
 * @Version 1.0
 */
public class Consumer implements Runnable {
    private Goods goods;
    private Object lock;
    private Vector<Goods> container;

    Consumer(Object lock, Vector<Goods> container) {
        this.lock = lock;
        this.container = container;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                if (!container.isEmpty()) {
                    container.remove(0);
                    System.out.println(Thread.currentThread().getName() + "消费商品");
                } else {
                    lock.notifyAll();
                }
            }
        }
    }
}


测试类

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 15:24
 * @Version 1.0
 */
public class MainTest {
    public static void main(String[] args) {
        Vector<Goods> goods = new Vector<Goods>(5);
        Object lock = new Object();
        Producer producer = new Producer(lock, goods);
        Consumer consumer = new Consumer(lock, goods);
        for (int i = 0; i < 5; i++) {
            Thread threadA = new Thread(producer, "生产者线程" + i);
            threadA.start();
        }
        for (int j = 0; j < 3; j++) {
            Thread threadB = new Thread(consumer, "消费者线程" + j);
            threadB.start();
        }
    }
}

执行结果: 代码解说(不清楚一定要看):

  • Object lock = new Object(); 这样写的目的是让你知道synchronized是通过获取一个对象作为锁,而不是把线程当作锁。
  • 生产者进行生产,生产满五个过后挂起当前线程并释放锁(lock锁对象),让其他线程(包括消费者线程)竞争。
  • 消费者进行消费,当容器为空的时候(通过唤醒等待lock锁对象的线程)通知其他生产线程执行run方法。

面试题二:利用多线程交替打印0-100

这种题目呢其实也不算稀奇了,一般的Java基础书籍上都有介绍,但是对于很多初学者来说,学习Java已经很头疼了,在那个阶段想看懂多线程的知识,必定是一件要命的事情。

今天我就帮大家好好捋一捋...

实现一:使用wait/notify

直接上代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/2/27 15:06
 * PACKAGE_NAME
 * @Description:
 */
public class PrintThread {
    public static int num = 1;
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Thread1 thread1 = new Thread1();
        thread1.setName("线程一");
        Thread2 thread2 = new Thread2();
        thread2.setName("线程二");
        executorService.submit(thread1);
        executorService.submit(thread2);
        executorService.shutdown();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            print();
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            print();

        }
    }

    static void print() {
        synchronized (PrintThread.class) {
            while (true) {
                try {
                    PrintThread.class.notify();
                    if(num<=100){
                        if (Thread.currentThread().getName().equals("线程一")) {
                            if(num%2==0){
                                System.out.println(Thread.currentThread().getName() + "->" + num++);
                            }
                        }else{
                            System.out.println(Thread.currentThread().getName() + "->" + num++);
                        }
                    }
                    PrintThread.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

问题:

  1. synchronized (PrintThread.class)是什么意思?

答:PrintThread.class当前类作为一个锁对象(类锁),每个线程想进入synchronized方法必须获取当前锁对象。其次,方便理解wait和notify具体操作的是锁对象。

  1. 为什么要先notify()?

答:如果一个线程抢占了资源然后立马wait()又挂起这就陷入了死循环。先使用notify()是为了保证两个线程可以相互唤醒。

  1. 为什么不用notifyAll()?

答:就当前场景notify()就足够了,你喜欢也可以使用notifyAll()。

实现二:使用Lock锁

上代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 15:06
 * PACKAGE_NAME
 * @Description:
 */
public class PrintThread {
    public static int num = 1;
    public static ReentrantLock reentrantLock = new ReentrantLock();
    public static Condition condition = reentrantLock.newCondition();

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Thread1 thread1 = new Thread1();
        thread1.setName("线程一");
        Thread2 thread2 = new Thread2();
        thread2.setName("线程二");
        executorService.submit(thread1);
        executorService.submit(thread2);
        executorService.shutdown();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            print();
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            print();

        }
    }

    static void print() {
        while (true) {
            reentrantLock.lock();
            try {
                condition.signal();
                if (num <= 100) {
                    if (Thread.currentThread().getName().equals("线程一")) {
                        if (num % 2 == 0) {
                            System.out.println(Thread.currentThread().getName() + "->" + num++);
                        }
                    } else {
                        System.out.println(Thread.currentThread().getName() + "->" + num++);
                    }
                }
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            reentrantLock.unlock();
        }
    }
}


该实现大同小异,相比于上面的更好理解。reentrantLock 及对代码块进行枷锁解锁操作。condition 及操作条件提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。

面试题三:利用多线程一个做加法一个做除法,最后求和计算

看到这个题目你可能想的最多的是我如何获取线程计算后得值?

常规操作

看代码

/**
 * @Auther: 爱唠嗑的阿磊
 * @Company: Java编程之道
 * @Date: 2020/7/11 16:07
 * @Description:
 */
public class ThreadsCount {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = 98;
        int d = 1;
        int sum = 0;
        ThreadA threadA = new ThreadA(a, b);
        ThreadB threadB = new ThreadB(c, d);
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future<Integer> first = executorService.submit(threadA);
        Future<Integer> two = executorService.submit(threadB);
        try {
            Integer aa = first.get();
            Integer bb = two.get();
            sum = aa + bb;
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}


class ThreadA implements Callable<Integer> {
    int a = 0;
    int b = 0;

    public ThreadA(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        return a * b;
    }

}

class ThreadB implements Callable<Integer> {
    // 处理d=0的异常
    int c = 0;
    int d = 0;

    public ThreadB(int c, int d) {
        this.c = c;
        this.d = d;
    }

    @Override
    public Integer call() throws Exception {
        return c / d;
    }

}

这是使用线程池来实现的,调用线程池submit方法可以获取一个Future,调用Future的get方法可以阻塞获取线程返回值。除此之外以可以实现Callable接口获取返回值,有兴趣的同学可以自己试一试,比较简单就不贴代码。

但是!!!你万万没想到,我其实要说的是另外一种实现方式,更加的🐂皮Plus!你绝对没用过!

骚操作

使用CompletionService实现。

它将Executor和BlockingQueue的功能融合在一起,你可以将Callable任务提交给他来完成,然后使用类似队列的take和poll方法来获取已经完成的任务结果。其结果会被封装成一个Future类。

上代码

import java.util.concurrent.*;

/**
 * @Auther: Xianglei
 * @Company: xxx
 * @Date: 2020/3/15 18:07
 * PACKAGE_NAME
 * @Description:
 */
public class ThreadsCount {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = 98;
        int d = 1;
        int sum = 0;
        ThreadA threadA = new ThreadA(a, b);
        ThreadB threadB = new ThreadB(c, d);
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        ExecutorCompletionService<Integer> integerExecutorCompletionService = new ExecutorCompletionService<Integer>(executorService);
        integerExecutorCompletionService.submit(threadA);
        integerExecutorCompletionService.submit(threadB);
        try {
            for (int i = 1; i <=2; i++) {
                Future<Integer> take = integerExecutorCompletionService.take();
                sum = sum + take.get();
                System.out.println("第"+i+"次计算:"+sum);
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}


class ThreadA implements Callable<Integer> {
    int a = 0;
    int b = 0;

    public ThreadA(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        return a * b;
    }

}

class ThreadB implements Callable<Integer> {
    // 处理d=0的异常
    int c = 0;
    int d = 0;

    public ThreadB(int c, int d) {
        this.c = c;
        this.d = d;
    }

    @Override
    public Integer call() throws Exception {
        return c / d;
    }

}

测试结果 说句题外话:这种实现方式比较适合于前端需要进行页面渲染,而渲染的数据来至于后来返回,这样做可以实现在线程池中并行处理,最后将获取到的结果立刻返回(显示)出来,可以是前端用户获得一个更加动态和更高响应性的界面。


还有更多线程池的骚操作,我后面在分析线程池这一块的时候再跟大家详细唠。今天暂时就聊这么多了,这也是我在面试美团、神策遇到的考题。希望大家好好理解一下,有疑问欢迎大家反馈交流哦!


更多精彩好文尽在:Java编程之道 🎁

欢迎各位好友前去关注!🌹