10道经典多线程面试题(附解析)

171 阅读13分钟

   金不三,银不四,寒冬虽在持续,面试还得继续。工匠特意总结了10道经典多线程面试题并附上详细解析,有需要的小伙伴可点赞加收藏,希望大家都能拿到心仪的offer,跨过寒冬,到达自己的春天

1. T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

  线程问题通常会在第一轮或电话面试阶段被问到,此题目的是检测面试者对join方法是否熟悉。问题比较简单,可以用join方法实现,具体实现如下:

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new T1());
        Thread t2 = new Thread(new T2());
        Thread t3 = new Thread(new T3());

        t1.start();
        t1.join(); // 等待 t1 执行完成
        t2.start();
        t2.join(); // 等待 t2 执行完成
        t3.start();
    }

    static class T1 implements Runnable {
        @Override
        public void run() {
            // T1 执行的任务
        }
    }

    static class T2 implements Runnable {
        @Override
        public void run() {
            // T2 执行的任务
        }
    }

    static class T3 implements Runnable {
        @Override
        public void run() {
            // T3 执行的任务
        }
    }
}

  在上面的示例中,创建了三个线程 t1、t2 和 t3,并分别使用 T1、T2 和 T3 类来实现线程的具体任务。在 main 方法中,首先启动 t1 线程,并调用 join() 方法等待 t1 线程执行完成,然后启动 t2 线程,并再次调用 join() 方法等待 t2 线程执行完成,最后启动 t3 线程,以保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行。

2. 在java中wait和sleep方法的不同?

  wait() 方法是 Object 类的一部分,它使线程暂停执行,并且会释放该线程持有的对象的锁。当一个线程执行 wait() 方法时,它会进入到该对象的等待池中,等待其他线程通知它继续执行。其他线程可以通过调用该对象的 notify()notifyAll() 方法来唤醒该线程

  sleep() 方法是 Thread 类的一部分,它使当前线程暂停执行指定的时间,并且不会释放该线程持有的任何锁。当一个线程执行 sleep() 方法时,它会进入到 TIMED_WAITING 状态,等待指定的时间过去后重新进入到 RUNNABLE 状态,可以继续执行。

总的来说:wait() 适用于线程间协作,它需要被其他线程唤醒;而 sleep() 适用于暂停执行一段时间后继续执行。在多线程编程中,使用 wait()notify() 可以更加有效地实现线程间的协作

3. 用Java编程一个会导致死锁的程序,你将怎么解决?

  要想写出该题的示例代码,首先得理解什么是死锁死锁是指两个或多个线程互相等待对方释放资源,导致程序无法继续执行的状态。 如下代码:

public class DeadlockDemo {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock 1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock 2");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock 2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock 1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

  上述程序中,两个线程分别获取了 lock1lock2 两个锁,并且在持有一个锁的情况下尝试获取另一个锁,这可能会导致死锁。如果线程1 先获取 lock1线程2 先获取 lock2,那么它们将无法释放持有的锁,并且一直等待对方释放锁,导致死锁

为了避免死锁,我们可以采取以下几种方法:

  1. 避免多个线程持有多个锁,并且在持有一个锁的情况下尝试获取另一个锁。
  2. 统一锁的获取顺序。比如,我们可以规定所有线程必须先获取 lock1,再获取 lock2。
  3. 给锁设置超时时间。如果一个线程在等待锁的过程中超过了一定的时间,那么它就放弃等待并释放已经持有的锁,这样可以避免死锁。
  4. 使用 tryLock() 方法代替 synchronized 关键字来获取锁。tryLock() 方法可以尝试获取锁,并在获取失败时立即返回,避免线程长时间阻塞。

4. Java中你怎样唤醒一个阻塞的线程?

   在回答此问题时,面试者可以先不急于回答,先理清楚或者向面试官咨询,线程因为什么情况而阻塞的,如果线程因为遇到了IO阻塞,我并且不认为有一种方法可以中止线程。下面我总结了几种情况,供大家参考:

  1. 使用 Object 类的 notify() 方法:该方法会唤醒在此对象监视器上等待的单个线程,如果多个线程在等待,则会随机选择其中一个线程进行唤醒。使用该方法需要满足以下条件:

    • 必须在同步块或同步方法中调用,即线程必须先获得对象的锁才能调用该方法。
    • 调用 notify() 方法的线程必须要释放对象的锁才能使等待的线程获得锁并继续执行。
  2. 使用 Object 类的 notifyAll() 方法:该方法会唤醒在此对象监视器上等待的所有线程。使用该方法需要满足上述两个条件

  3. 使用 Lock 接口的 Condition 类的 signal() 方法:该方法会唤醒在此条件下等待的单个线程,如果多个线程在等待,则会随机选择其中一个线程进行唤醒。使用该方法需要满足以下条件:

    • 必须先通过 Lock 接口的 newCondition() 方法创建一个 Condition 对象
    • 调用 signal() 方法的线程必须先获得该 Condition 对象关联的锁才能调用该方法。
    • 调用 signal() 方法的线程必须要释放锁才能使等待的线程获得锁并继续执行
  4. 使用 Lock 接口的 Condition 类的 signalAll() 方法:该方法会唤醒在此条件下等待的所有线程。使用该方法需要满足上述三个条件

当然,特殊情况,也可以使用中断来唤醒一个线程。当线程处于阻塞状态时,如果它被中断了,就会抛出一个 InterruptedException 异常,可以通过捕获该异常来唤醒线程并进行相应的处理。下面是一个简单的示例代码:

public class MyThread extends Thread {
    private Object lock;

    public MyThread(Object lock) {
        this.lock = lock;
    }

    public void run() {
        synchronized (lock) {
            try {
                lock.wait();
                // 如果线程被中断,抛出 InterruptedException 异常
            } catch (InterruptedException e) {
                System.out.println("线程被中断,唤醒线程并进行相应的处理");
            }
            // 执行相应的逻辑
        }
    }
}

// 在另一个线程中中断等待线程
myThread.interrupt();

   在上述代码中,MyThread 类的 run() 方法中使用了 lock.wait() 方法等待线程被唤醒。当另一个线程中断 MyThread 线程时,lock.wait() 方法会抛出 InterruptedException 异常,此时就可以在 catch 块中进行相应的处理。需要注意的是,在 catch 块中只能唤醒线程并进行相应的处理,不能直接终止线程的执行,否则会出现逻辑错误。

5. 怎么检测一个线程是否持有对象监视器

   第一次看到这个问题的时候,我也是一脸懵逼,估计被文档,也是只能回去等通知的结果:在Java中,可以使用 Thread.holdsLock(Object obj) 方法来检测一个线程是否持有指定对象的监视器(也称为锁)

   该方法的作用是判断当前线程是否持有指定对象的锁。如果当前线程持有指定对象的锁,则返回 true;否则返回 false

下面是一个简单的示例代码:

public class MyThread extends Thread {
    private Object lock;

    public MyThread(Object lock) {
        this.lock = lock;
    }

    public void run() {
        synchronized (lock) {
            if (Thread.holdsLock(lock)) {
                System.out.println("当前线程持有对象的监视器");
            } else {
                System.out.println("当前线程未持有对象的监视器");
            }
            // 执行相应的逻辑
        }
    }
}

  注意这是一个static方法,这意味着该方法只能检测当前线程是否持有指定对象的监视器,不能检测其他线程是否持有该对象的监视器

6. Thread.sleep(0)的作用是什么

  在Java中,Thread.sleep(0) 的作用是让当前线程放弃时间片,以便让其他线程有机会运行。由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

  需要注意的是,调用 Thread.sleep(0) 并不能保证当前线程会立即放弃时间片,因为这取决于操作系统的调度策略和当前运行的其他线程的状态。如果其他线程都处于阻塞状态,当前线程可能会继续执行,直到时间片用完或者其他就绪状态的线程出现

7. synchronized和ReentrantLock的区别

  SynchronizedReentrantLock都是Java中用于实现线程同步的工具。它们的共同点是都能够确保多个线程访问共享资源时的安全性,避免出现竞争条件和数据不一致等问题。但是,它们在实现机制和特点上也有一些区别:

  1. 实现机制:SynchronizedJava中的关键字,是Java语言内置的一种锁机制,操作的应该是对象头中mark word;而ReentrantLock是Java提供的一个类,是基于AQS(AbstractQueuedSynchronizer)的一种可重入锁。
  2. 锁的获取方式:在使用Synchronized时,线程进入同步块或同步方法时会自动获取锁,在执行完同步代码块或方法后会自动释放锁。而在使用ReentrantLock时,需要手动调用lock()方法获取锁,以及unlock()方法释放锁。
  3. 锁的粒度:Synchronized锁的粒度较粗,只能对整个方法或代码块进行同步,而ReentrantLock可以实现更细粒度的锁控制,可以对多个线程访问共享资源的不同部分进行锁定,提高并发性能。
  4. 可重入性:ReentrantLock是可重入锁,允许线程多次获取同一把锁,而Synchronized也是可重入锁,但是需要注意锁的嵌套使用,可能会导致死锁的问题。
  5. 等待可中断:在获取锁时,如果锁已经被其他线程占用,ReentrantLock可以通过tryLock(long timeout, TimeUnit unit)方法实现等待一段时间后自动放弃获取锁,避免死锁的问题。而Synchronized不支持锁的等待可中断。

总的来说,Synchronized适用于简单的线程同步问题,使用方便、效率高,而ReentrantLock适用于更复杂的线程同步问题,提供了更多的灵活性和可操作性。但是,需要注意的是,在使用ReentrantLock时需要手动释放锁,否则容易出现死锁等问题。

8. ConcurrentHashMap的并发度是什么

  ConcurrentHashMap的并发度(concurrency level)是指哈希表中用于控制并发的segment数组的长度,也就是哈希表的分段锁的数量。每个segment都是一个独立的哈希表,包含一个锁,通过将不同的键值对映射到不同的segment上,实现了多线程并发访问时的线程安全。

  ConcurrentHashMap默认的并发度为16,也就是说,它会创建16个``segment。当并发度为n时,ConcurrentHashMap可以支持n个线程同时进行写操作,以及更多个线程同时进行读操作。通过增加并发度,可以提高ConcurrentHashMap的并发性能,但同时也会增加内存开销和锁竞争的概率。

9.谈谈 AQS 框架是怎么回事儿

  AQS(AbstractQueuedSynchronizer)是一个用于实现锁和其他同步器的基础框架,它提供了一种方便且可扩展的方式来实现各种同步器,如 ReentrantLockSemaphoreCountDownLatch 等。其核心思想是使用一个等待队列来管理线程的阻塞和唤醒,通过维护一个 volatile 类型的 state 变量来表示锁和同步状态的状态,以及使用模板方法来定义具体的同步操作。

AQS 的实现包括以下几个重要部分:

  1. 等待队列:AQS 通过维护一个基于双向链表的等待队列来管理线程的阻塞和唤醒。
  2. state 变量:AQS 通过维护一个 volatile 类型的 state 变量来表示锁和同步状态的状态。
  3. 模板方法:AQS 定义了一些模板方法,如 tryAcquire()、tryRelease()、tryAcquireShared()、tryReleaseShared() 等,这些方法由具体的同步器来实现。
  4. Node 节点:AQS 中的等待队列是由一系列 Node 节点组成的,每个节点代表一个等待线程。
  5. acquire() 和 release() 方法:acquire() 方法用于获取锁或者同步状态,如果获取失败会将当前线程加入等待队列中。而 release() 方法用于释放锁或者同步状态,并唤醒等待队列中的线程。
  6. ConditionAQS 还提供了 Condition 接口来支持条件变量,用于让等待线程在某个条件满足时被唤醒。

  核心实现是一个基于双向链表的等待队列,它通过将等待线程封装成一个个节点,来管理线程的阻塞和唤醒。在 AQS 中,每个节点表示一个等待线程,每个节点包含一个状态和一个等待队列中的前驱和后继节点。当一个线程尝试获取锁或者同步状态失败时,它会创建一个节点,并将该节点添加到等待队列的尾部,然后阻塞线程。当锁或者同步状态被释放时,AQS 会从等待队列中取出头节点,将其唤醒,并尝试让该节点重新获取锁或者同步状态。

10.用Java实现阻塞队列。

  这是一个相对高频的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用Java线程写程序;第二,可以检测侯选者对并发场景的理解。以下是使用Java实现阻塞队列的示例代码:

import java.util.LinkedList;
import java.util.Queue;

public class BlockingQueue<T> {
    private Queue<T> queue = new LinkedList<>();
    private int capacity;

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void enqueue(T element) throws InterruptedException {
        while (queue.size() == capacity) {
            wait();
        }
        if (queue.isEmpty()) {
            notifyAll();
        }
        queue.offer(element);
    }

    public synchronized T dequeue() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        if (queue.size() == capacity) {
            notifyAll();
        }
        return queue.poll();
    }
}

  这个阻塞队列使用Java的内置LinkedList作为基础队列数据结构,并且实现了enqueue和dequeue方法。enqueue方法用于向队列中添加元素,如果队列已满,该方法将会阻塞直到有空间。dequeue方法用于从队列中取出元素,如果队列为空,该方法将会阻塞直到有元素可取。

  该实现使用Java的内置锁机制,并且使用了wait和notifyAll方法来进行阻塞和唤醒线程。注意到使用wait和notifyAll方法的时候,必须使用synchronized关键字来获得该对象的锁,以确保线程安全