面试题:线程基础5问

Posted 格子衫111

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试题:线程基础5问相关的知识,希望对你有一定的参考价值。

一:为何说只有 1 种实现线程的方法?

1.创建线程,本质上就一种,通过new Thread()实现;

但实现线程执行内容有两种方式,也是我们最常见的,1)继承Thread类;2)实现Runnable接口。

其他方式,例如线程池,Timer只是对new Thread()的封装。

2.实现Runnable接口相比继承Thread类的好处:

1)结构上分工更明确,线程本身属性和任务逻辑解耦;

2)某些情况下性能更好,直接把任务交给线程池执行,无需再次new Thread();

3)可拓展性更好,实现接口可以多个,而继承只能单继承。


二:如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?

1.正确停止线程的方法:

thread.interrupt(),通知线程中断;

2.线程内逻辑需配合响应中断:

1)正常执行循环中使用 Thread.currentThread().isInterrupted()判断中断标识;
2)若含有sleep()等Waiting操作,会唤醒线程,抛出interruptedException,抛出后中断标识会重置。对于中断异常,要么正确处理,重新设置中断标识;要么在方法上声明抛出异常以便调用方处理。

3.为什么用 volatile 标记位的停止方法是错误的

例如 生产-消费模式,含有阻塞put操作时,volatile 标记变量改变也无法唤醒阻塞中的生产者线程。

4.stop()、suspend() 和 resume()是已过期方法,

有很大安全风险,它们强行停止线程,有可能造成线程持有的锁或资源没有释放。


三:线程是如何在 6 种状态之间转换的?

在 Java 中线程的生命周期中一共有 6 种状态。

  • New(新创建)
  • Runnable(可运行)= running || ready
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed Waiting(计时等待)
  • Terminated(被终止)

如果想要确定线程当前的状态,可以通过 getState() 方法,并且线程在任何时刻只可能处于 1 种状态。

在这里插入图片描述
Java 中的 Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
在这里插入图片描述

四:wait/notify/notifyAll 方法的使用注意事项?

1. 为什么 wait 方法必须在 synchronized 保护的同步代码中使用?

1)拿生产者消费者举例,如果消费者未加 synchronized 保护,while 判断和 wait 方法无法构成原子操作,可能生产者先notify,消费者再wait,后续如无生产者再notify,消费者便有可能陷入无穷无尽的等待。

2)程序需采用while循环来应对“虚假唤醒”(非notify/notifyAll,中断或超时的唤醒),即便被虚假唤醒了,也会再次检查while里面的条件。

2. 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?

1)wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。

2)因为可能在Thread中持有多个对象锁,做复杂的业务操作,如果定义wait定义在Thread中,那么是不满足的。

3. wait/notify 和 sleep 方法的异同?

相同点:

1)都可以使线程阻塞;

2)都可以响应interrupt 中断,并抛出InterruptedException 异常

不同点:

1)wait必须写在 synchronized 保护的代码中,sleep不用;

2)在同步代码中,执行sleep方法时不会释放monitor锁,执行wait方法时会自动释放monitor锁;

3)sleep方法需要设置到期时间,到期自动恢复,wait方法不能设置时间,不会主动恢复,除非被中断或者唤醒;

4)wait/notify 是属于Object类的,sleep是属于Thread类的;

五:有哪几种实现生产者消费者模式的方法?

1.如何用 BlockingQueue 实现生产者消费者模式?
public static void main(String[] args) {

 BlockingQueue<Object> queue = new ArrayBlockingQueue<>(10);

 Runnable producer = () -> {
   while (true) {
         queue.put(new Object());
     }
  };

  new Thread(producer).start();
  new Thread(producer).start();

  Runnable consumer = () -> {
     while (true) {
          queue.take();
     }
  };
  new Thread(consumer).start();
  new Thread(consumer).start();
}
  1. 首先,创建了一个 ArrayBlockingQueue 类型的 BlockingQueue,命名为 queue 并将它的容量设置为 10;

  2. 其次,创建一个简单的生产者(继承Runnablel),while(true) 循环体中的queue.put() 负责往队列添加数据,同时创建两个生产者线程并启动;

  3. 然后,创建一个简单的消费者(继承Runnablel),while(true) 循环体中的 queue.take() 负责消费数据,同时创建两个消费者线程并启动。

2.如何用 Condition 实现生产者消费者模式?
public class MyBlockingQueueForCondition {

   private Queue queue;
   private int max = 16;
   private ReentrantLock lock = new ReentrantLock();
   private Condition notEmpty = lock.newCondition();
   private Condition notFull = lock.newCondition();

   public MyBlockingQueueForCondition(int size) {
       this.max = size;
       queue = new LinkedList();
   }

   public void put(Object o) throws InterruptedException {
       lock.lock();
       try {
           while (queue.size() == max) {
               notFull.await();
           }
           queue.add(o);
           notEmpty.signalAll();
       } finally {
           lock.unlock();
       }
   }

   public Object take() throws InterruptedException {
       lock.lock();
       try {
           while (queue.size() == 0) {
               notEmpty.await();
           }
           Object item = queue.remove();
           notFull.signalAll();
           return item;
       } finally {
           lock.unlock();
       }
   }
}

自行实现一个简易版的 BlockingQueue:

1)首先,定义了一个队列(Queue )变量 queue 并设置最大容量(ma变量)为 16;

2)其次,定义了一个 ReentrantLock 类型的 Lock 锁,并在 Lock 锁的基础上创建两个 Condition,一个是 notEmpty,另一个是 notFull,分别代表队列没有空和没有满的条件;

3)最后,声明了 put 和 take 这两个核心方法。

put:使用lock() 方法加锁,队列满了,使用notFull.await()进行等待,未满, 生产后,使用notEmpty.signalAll()通知,finally,使用unlock() 释放锁;

take:使用lock() 方法加锁,队列为0,使用notEmpty.await()进行等待,大于0, 消费后,使用notFull.signalAll()通知,finally,使用unlock() 释放锁。

3.如何用 wait/notify 实现生产者消费者模式?
class MyBlockingQueue {

   private int maxSize;
   private LinkedList<Object> storage;
    
   public MyBlockingQueue(int size) {
       this.maxSize = size;
       storage = new LinkedList<>();
   }

   public synchronized void put() throws InterruptedException {
       while (storage.size() == maxSize) {
           wait();
       }
       storage.add(new Object());
       notifyAll();
   }

   public synchronized void take() throws InterruptedException {
       while (storage.size() == 0) {
           wait();
       }
       System.out.println(storage.remove());
       notifyAll();
   }
}

实现原理和Condition 类似 , 自行实现简易版BlockingQueue:

用 LinkedList 装数据,核心在于put和 take 两个方法的编写,

put:list 满了,使用wait()进行等待,未满, 生产后,使用 notifyAll() 通知;

take:list 为0,使用wait()进行等待,大于0, 消费后,使用 notifyAll() 通知。

思考: 为何多线程的代码大部分都用while而不用if?

消费者举例:

如果是if,执行 wait() 时会自动释放
monitor锁,可能导致两个线程同时进来一起休眠。当被生产者唤醒时,两个线程一起被唤醒。一个抢到锁,正常消费,消费完毕,另外个线程又拿到锁,再往下执行(去消费),but,东西都被第1个线程消费完了,本身是不满足条件的,还执行个鬼啊。

所以,换成while的话,它会继续去走一遍判断,然后再次休眠,避免抛出NoSuchElementException 异常

以上是关于面试题:线程基础5问的主要内容,如果未能解决你的问题,请参考以下文章

面试做java的要问些啥问题

经典Python面试题之基础80问 Python开发

线程面试题

java3年左右面试题

Android 面试中常问到的那些 Handler 面试题

19年BAT常问面试题汇总:JVM+微服务+多线程+锁+高并发性能