Java面试-线程核心基础知识

Java面试-线程核心基础知识

在系统的复习了Java的线程相关基础知识后,将其中可能出现的一些面试题做一个总结如下:

  1. 有多少种实现线程的方法?
  2. 实现Runnable接口和继承Thread类哪种方式更好?
  3. 一个线程两次调用start()方法会出现什么情况?为什么?
  4. 既然start()方法会调用run方法,为什么我们选择调用start方法而不是直接调用run方法呢?
  5. 如何停止线程?
  6. 如何处理不可中断的阻塞
  7. 线程有哪几种状态?生命周期是什么?
  8. 用程序实现两个县城交替打印(0-100)的奇偶数
  9. 手写生产者消费者设计模式
  10. 为什么wait方法需要在同步代码块内使用,而slepp不需要
  11. 为什么线程通信的方法wait、notify、notifyAll被定义在Object类中?而slepp方法被定义在Thread类中?
  12. wait方法是属于Object对象的,那调用Thread.wait()会怎么样
  13. 如何选择用notify还是notifyAll
  14. notifyAll之后所又的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
  15. suspend和resume来阻塞线程可以吗?为什么?
  16. wait/notify、sleep异同
  17. 在join期间,线程处于哪种线程状态?
  18. 守护线程和普通线程的区别
  19. 我们是否需要给线程设置守护线程?
  20. run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
  21. 线程中如何处理某个未处理异常
  22. 什么是多线程的上下文切换
  23. 为什么多线程会带来性能问题?

下面,我将对上述的问题的问题一一的作出我认为较为正确的回答。

有多少种实现线程的方法?

  • 从Oracel官方文档可以得知,创建线程的方法有两类,一类是实现Runnable接口,另一类是继承Thread类

  • 准确的讲,创建线程只有一种方式那就是构造一个Thread类,而实现线程的执行单元run()方法有两种方式

    • 实现Runnable接口的run方法,并把Runnable的实例传给Thread
    • 继承Thread类,重写Thread的run方法
  • 通常不准确的说法包括通过线程池、定时器、匿名内部类、lambda表达式等方式方式创建线程都是对上述方式的调用

实现Runnable接口和继承Thread类哪种方式更好?

实现Runnable接口更好

  • 可以避免Java中的单继承的限制
  • 增强代码的健壮性,代码可以被多个线程共享
  • 适合多个相同的程序去处理同一个资源

一个线程两次调用start()方法会出现什么情况?为什么?

结果:第二次调用抛出IllegalThreadStateException

原因:

  • 线程在第一次执行start方法后由NEW状态转换为了Runnable状态,且转换不可逆
  • Java在调用start方法时会判断线程所处的状态是否为NEW状态,如果不是就会抛出IllegalThreadStateException

既然start()方法会调用run方法,为什么我们选择调用start方法而不是直接调用run方法呢?

  • 使用start方法调用时才会真正启动一个线程,并让线程从NEW状态转为RUNNABLE状态,从而经历完整的生命周期
  • 使用run方法调用时就只是一个普通的主线程的方法而已,并不会进入子线程

如何停止线程?

  • 用interrupt来请求,而不是用stop/volatile
  • 用interrupt好处是保证线程安全,将主动权交给被中断的线程
  • 想要停止线程,要请求方,被停止方,子方法被调用的相互配合
    • 请求方发出请求信号
    • 被停止方要适当的时候检查中断信号,并在可能抛出interruptedException的时候去处理这个信号,并进行处理
    • 如果是写子方法调用的,优先是在方法层抛出这个exception,以便于上层进行处理,或者收到中断信号后,再次将它设为中断状态(收到后,默认会清除中断状态)
  • 错误的停止方法:
    • stop:已经被弃用了,不能保证数据的完整性
    • volatile的boolean标识无法处理长时间阻塞的情况
      (生产者生产快,消费者消费慢,发送中断时标识位即使改变了,已经生产了的还是会继续被消费)

如何处理不可中断的阻塞

  • 根据不同的情况做不同的处理,不同情况下可能有相应的方法进行处理,在编写时使用可以响应中断的锁的方法
  • 如果不能进行处理,就让它苏醒后尽快感受到中断进行处理

线程有哪几种状态?生命周期是什么?

  • 线程有6种状态:NEW、RUNNABLE、TERMINATED、BLOCKED、WAITTING、TIMED_WAITTING

在这里插入图片描述

用程序实现两个线程交替打印(0-100)的奇偶数

synchronized实现
/**
 * 〈用程序实现两个线程交替打印0-100的奇偶数
 * 本类采用synchronized〉
 *
 * @author Chkl
 * @create 2020/2/29
 * @since 1.0.0
 */
public class waitnotifyPrintEvenSYyn {

    private static int count;
    private static Object lock = new Object();

    //建两个线程,一个只处理偶数,一个只处理奇数(位运算)
    //用synchronized做通信
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) == 0) {
                            System.out.println
                                    (Thread.currentThread().getName()
                                            + ":" + count++);
                        }
                    }
                }
            }
        }, "偶数").start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) != 0) {
                            System.out.println
                                    (Thread.currentThread().getName()
                                            + ":" + count++);
                        }
                    }
                }
            }
        }, "奇数").start();
        
    }
}
wait和notify优化实现
/**
 * 〈连个线程交替打印0-100的两个奇偶数〉
 * 用wait和notify实现
 *
 * @author Chkl
 * @create 2020/2/29
 * @since 1.0.0
 */
public class WaitNotifyprintEvenWait {
    //拿到锁,就打印
    //打印完,唤醒其他线程,就休眠

    private static int count;
    private static Object lock = new Object();

    static class TurningRunning implements Runnable {
        @Override
        public void run() {
            while (count <= 100) {
                synchronized (lock) {
                    System.out.println
                            (Thread.currentThread().getName() + ":" + count++);
                    lock.notify();
                    if (count <= 100) {
                        try {
                            //如果任务未结束,让出当前的锁
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new TurningRunning(), "偶数").start();
        try {
            //休眠100毫秒,保证偶数先行
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new TurningRunning(), "奇数").start();


    }

}

手写生产者消费者设计模式

什么情况下需要这种设计模式

任务队列中,生产者和消费者存在步调不一致时

扫描二维码关注公众号,回复: 9547106 查看本文章

使用wait/notify的实现

/**
 * 〈用wait/notify实现生产者和消费者〉
 *
 * @author Chkl
 * @create 2020/2/29
 * @since 1.0.0
 */

public class ProducerCustomerModel {

    public static void main(String[] args) {

        EventStorage storage = new EventStorage();
        Producer producer = new Producer(storage);
        Consumer consumer = new Consumer(storage);

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


    }

}

//生产者
class Producer implements Runnable {
    private EventStorage storage;

    public Producer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.put();
        }
    }
}

//消费者
class Consumer implements Runnable {
    private EventStorage storage;

    public Consumer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.take();
        }
    }
}

//阻塞队列
class EventStorage {
    //    最大值
    private int maxSize;
    //    数据存储队列
    private LinkedList<Date> storage;

    public EventStorage() {
        maxSize = 10;
        storage = new LinkedList<>();
    }

    //    添加方法
    public synchronized void put() {
//        当队列满了就调用wait方法释放锁,等待消费后唤醒
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        添加到队列
        storage.add(new Date());
        System.out.println("仓库有了" + storage.size() + "个产品");
//        添加完成后提醒消费者消费
        notify();
    }

    public synchronized void take() {
//        当队列空了调用wait方法释放锁等待生成
        if (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        进行消费
        System.out.println("拿到了" + storage.poll() + ",现在还剩下" + storage.size());
//        消费后提醒生产者进行生产
        notify();
    }

}

为什么wait方法需要在同步代码块内使用,而slepp不需要

  • wait方法定义在同步代码块中为了让通信变得可靠,防止死锁或者永久等待的发生

    如果不把wait和notify方法都放在同步块里,可能在执行wait之前,线程突然切出去了,到一个将要执行notify的线程,把notify的都执行完了之后再切回将执行wait的线程执行完wait之后,不再有线程唤醒它,造成永久等待

  • sleep方法主要针对单个线程,与其他线程没有太多关联,不需要同步

为什么线程通信的方法wait、notify、notifyAll被定义在Object类中?而slepp方法被定义在Thread类中?

  • wait(),notify(),notifyAll()是锁级别的操作,而锁是属于某一个对象的。每一个对象的对象头中都有几个字节是存放锁的状态的,所以锁是绑定在对象中,而不是线程中。如果定义在Thread中,如果每一个线程持有多把锁,就不能灵活地使用了。

  • sleep()是针对于单个线程的操作,所以在Thread类中

wait方法是属于Object对象的,那调用Thread.wait()会怎么样

  • 不应该调用Thread.wait(),Thread不适合作为锁对象
  • 当线程结束的时候,会自动的调用notify方法,会干扰设计的整个流程

如何选择用notify还是notifyAll

根据业务需求选择

  • notify()随机的唤醒一个线程
  • notifyAll()唤醒所有的wait状态的线程

notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?

陷入等待状态,等待这把锁被释放后再次竞争

suspend和resume来阻塞线程可以吗?为什么?

不可以,由于安全问题,这两个方法都弃用。

wait/notify、sleep异同

  • 相同

    • 都会发生阻塞
    • 都会响应中断
  • 不同

    • wait/notify必须在同步方法中执行,sleep不要求
    • wait/notify会释放锁,sleep不释放锁
    • sleep必须指定时间,wait可传可不传
    • wait/notify属于Object类,sleep属于Thred类

在join期间,线程处于哪种线程状态?

waiting

守护线程和普通线程的区别

  • 普通线程会影响JVM的退出,当普通线程没有全部结束JVM不会退出,守护线程不会
  • 普通线程的作用是执行我们所写的逻辑,守护线程的作用是服务于普通线程

我们是否需要给线程设置守护线程?

不需要设置,并且设置了可能会很危险。当只剩下这一个线程时,JVM认定为是守护线程就直接停掉了,造成线程错误结束

run方法是否可以抛出异常?

  • 不可以抛出,在方法签名中说明了不能往外抛异常

线程中如何处理某个未处理异常

实现UncaughtExceptionHandler接口生成一个全局异常处理器

再将处理器配置在Thread中

Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));

什么是多线程的上下文切换

上下文切换可以认为是操作系统内核对CPU上进程(包括线程)进行以下活动:

  • 挂起一个进程,将这个进程在CPU中的状态(上下文)存储在内存中的某处
  • 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
  • 跳转到程序计数器所指的位置(进程被中断的代码行),以恢复该进程

为什么多线程会带来性能问题?

  • 调度上,频繁的上下文切换
  • 协作上,Java内存模型,为了数据的正确性往往会使用禁止编译器优化,使缓存失效

暂时复习到这里,后续的在复习了之后再补充

发布了20 篇原创文章 · 获赞 15 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_41170102/article/details/104596620
今日推荐