微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

多线程常见面试题

目录

1. 为什么要使用多线程

2. 进程和线程的区别

3. Java实现线程的方式

4. 线程的状态流转

5. 线程中 sleep()和 wait()方法的区别和共同点

6. 什么是线程死锁?如何避免死锁?

7.  sleep()方法与yield()方法的区别

8. 守护线程是什么?

9. 线程不安全的原因

10. violatile 关键字的作用

11. synchronized 关键字的作用

12. yield()方法的作用

13. wait() 和 notify()

wait() 方法

notify() 方法

14. 线程池

15. 如何创建线程安全的单例模式?

1)饿汉模式

2)懒汉模式

16. 什么是阻塞队列?

生产者消费者模型

17. Java 内存模型是什么,哪些区域是线程共享的,哪些是不共享的?

18. 什么是乐观锁和悲观锁? 

19. Java 中堆和栈有什么不同?


1. 为什么要使用多线程

  • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 cpu 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能

2. 进程和线程的区别

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

  • 根本区别进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

总结:

  1. 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
  2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
  3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
  4. 线程的创建、切换及终止效率更高。

3. Java实现线程的方式

  • 使用继承Thread类的方式创建多线程
  • 实现Runnable接口的方式创建多线程。
  • 使用ExecutorService、Callable、Future实现有返回结果的多线程

4. 线程的状态流转

线程的生命周期及五种基本状态:

Java线程具有五中基本状态

1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待cpu调度执行,并不是说执行了t.start()此线程立即就会执行;

3)运行状态(Running):当cpu开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对cpu的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被cpu调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  • 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
  • 同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
  • 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时. join()等待线程终止或者超时. 或者I/O处理完毕时,线程重新转入就绪状态。

5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

5. 线程中 sleep()和 wait()方法的区别和共同点

区别

  • sleep() 方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待cpu的到来。睡眠不释放锁(如果有的话)。
  • wait() 方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
  • sleep() 方法没有释放锁,而 wait 方法释放了锁 。
  • sleep() 通常被用于暂停执行Wait 通常被用于线程间交互/通信
  • sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。wait() 方法调用后,线程不会自动苏醒,需要别的线程调用一个对象上的 notify() 或者 notifyAll() 方法

共同点

  • 两者都可以暂停线程的执行。

6. 什么是线程死锁?如何避免死锁?

死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免死锁?

只要破坏产生死锁的四个条件中的其中一个就可以了

  • 破坏互斥条件 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)
  • 破坏请求与保持条件 一次性申请所有的资源。
  • 破坏不剥夺条件 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
  • 锁排序法:(必须回答出来的点) 指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,在多线程条件下,如何避免死锁? 通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法
  • 使用显式锁中的reentrantlock.try(long,TimeUnit)来申请锁

7.  sleep()方法与yield()方法的区别

  • sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给优先级低的线程以运行的机会,而yield()方法只会给相同优先级或者更高优先级的线程以运行机会。
  • 线程执行sleep()方法后会转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内肯定不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后马上又被执行。
  • sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常。
  • sleep()方法比yield()方法(跟操作系统)具有更好的可移植性。

8. 守护线程是什么?

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

9. 线程不安全的原因

修改共享数据,原子性,可见性,代码顺序性。

10. violatile 关键字的作用

  • volatile 保证变量对所有线程的可见性:当volatile变量被修改,新值对所有线程会立即更新。或者理解为多线程环境下使用volatile修饰的变量的值一定是最新的。
  • jdk1.5以后volatile完全避免了指令重排优化,实现了有序性。

11. synchronized 关键字的作用

  • 原子性:确保线程互斥的访问同步代码
  • 可见性:保证共享变量修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
  • 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;

12. yield()方法的作用

yield() 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 cpu 占用而不能保证使其它线程一定能占用 cpu,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

13. wait() 和 notify()

wait() 方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间)
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

notify() 方法

notify 方法是唤醒等待的线程。

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执完,也就是退出同步代码块之后才会释放对象锁。

14. 线程池

使用线程池的好处:

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

15. 如何创建线程安全的单例模式?

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。

单例模式具体的实现方式, 分成 "饿汉" 和 "懒汉" 两种。

1)饿汉模式

类加载的时候创建实例。

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

2)懒汉模式

类加载的时候不创建实例. 第一次使用的时候才创建实例。

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        } 
        return instance;
    }
}

16. 什么是阻塞队列?

阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则.

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性: 

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型.

生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。

17. Java 内存模型是什么,哪些区域是线程共享的,哪些是不共享的?

 

18. 什么是乐观锁和悲观锁? 

19. Java 中堆和栈有什么不同?

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐