Java面试常见问题-并发篇


  1. 通常创建线程有几种方式?

    • 创建线程的常用四种方式:

      • 继承 Thread 类
      • 实现 Runnable 接口
      • 实现 Callable 接口(JDK1.5>=)
      • 线程池方式创建

      通过继承 Thread 类或者实现 Runnable 接口、Callable 接口都可以实现多线程,不过实现 Runnable 接口与实现 Callable 接口的方式基本相同,只是 Callable 接口里定义的方法返回值,可以声明抛出异常而已。因此将实现 Runnable 接口和实现 Callable 接口归为一种方式。

    • 采用实现 Runnable、Callable 接口的方式创建线程的优缺点

      • 优点
        线程类只是实现了 Runnable 或者 Callable 接口,还可以继承其他类。这种方式下,多个线程可以共享一个 target 对象。所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
      • 缺点
        编程稍微复杂一些,如果需要访问当前线程,则必须使用 Thread.currentThread() 方法
    • 采用继承 Thread 类的方式创建线程的优缺点

      • 优点
        编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获取当前线程
      • 缺点
        因为线程类已经继承了 Thread 类,Java 语言是单继承的,所以就不能再继承其他父类了。
  2. 说说线程的生命周期

    先来看一张图:
    invalid image(图片无法加载)
    这六个状态就对应线程的生命周期。下图为线程对应状态以及状态出发条件:
    invalid image(图片无法加载)

  3. 说说 synchronized 的使用和原理

    • 线程同步中的锁对象

      • 对于普通同步方法,锁是当前实例对象
      • 对于静态同步方法,锁是当前类 Class 对象
      • 对于同步方法块,锁是 Synchronized 括号里配置的对象(对象为普通对象则锁定的是该对象,对象为 Class 对象则锁定的是 Class 对象)

    JVM 基于进入和退出 Monitor 对象来显示方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的。
    monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法的结束处和异常处,JVM 保证每个 monitorenter 必须有对应的 monitorexit 与之配对。

  4. synchronized 和 ReentrantLock 区别

    • synchronized 是关键字,ReentrantLock 是 JUC 下面的一个类。
    • JDK 1.5 之前同步锁只有 synchronized。
    • 都是可重入的同步锁。
    • synchronized 只有非公平锁,ReentrantLock 默认为非公平锁,但是可以手动设置为公平锁。
    • ReentrantLock 需要手动释放锁 try{--objectLock.lock();}---finally--{objectLock.unlock();},synchronized 隐形释放(方法或者代码块执行完、异常)。
    • ReentrantLock 可中断,synchronized 不可中断,一个线程引用锁的时候,别的线程只能阻塞等待。
    • ReentrantLock 和 synchronized 持有的对象监视器不同。
    • ReentrantLock 能够将 wait/notify/notifyAll 对象化。synchronized 中,锁对象的 wait 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件。
    • synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。
    • ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。
  5. 什么是线程安全?

    按照《Java 并发编程实战》(Java Concurrency in Practice) 的定义就是:线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,这里的状态反映在程序中其实可以看作是数据。
    通俗易懂的说法:当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替执行,我们在主程序中不需要做任何同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

  6. 线程安全需要保证几个基本特征

    • 原子性:
      简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
    • 可见性:
      是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
    • 有序性:
      是保证线程内串行语义,避免指令重排等。
  7. 说一下线程之间是如何通信的?

    线程之间的通信有两种方式:共享内存和消息传递。

    • 共享内存

      在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
      invalid image(图片无法加载)
      例如上图线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:

      1. 线程 A 把本地内存 A 更新过的共享变量刷新到主内存中去;
      2. 线程 B 到主内存中去读取线程 A 之前更新过的共享变量。
    • 消息传递

      在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify(),或者 BlockingQueue。
      invalid image(图片无法加载)

  8. 说说你对 volatile 的理解

    invalid image(图片无法加载)

  9. 说一下 volatile 和 synchronized 的区别?

    • volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
    • volatile 仅能使用在变量级别。synchronized 则可以使用在变量、方法、和类级别的。
    • volatile 仅能实现变量的修改可见性,不能保证原子性。而 synchronized 则可以保证变量的修改可见性和原子性。
    • volatile 不会造成线程的阻塞。synchronized 可能会造成线程的阻塞。
    • volatile 标记的变量不会被编译器优化。synchronized 标记的变量可以被编译器优化。
  10. Thread 调用 run 方法和调 start 方法的区别?

    • 调用 run 方法不会再启一个线程,跟着主线程继续执行,和调普通类的方法一样;
    • 调用 start 方法表示启动一个线程。

    面试扩散

    下面代码将输出什么内容?不清楚的建议自己去试试。

     public class ThreadDemo {
         public static void main(String[] args) {
             Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("test start");
                }
             });
             thread.start();
             thread.start();
         }
     }
    
  11. 说一下 Java 创建线程池有哪些方式?

    • 通过 java.util.concurrent.Executors 来创建以下常见线程池:
      invalid image(图片无法加载)
    • 也可以通过 java.util.concurrent.ThreadPoolExecutor 来创建自定义线程池,其中核心的几个参数:
      int corePoolSize, //核心线程数量
      int maximumPoolSize, //最大线程数
      long keepAliveTime, //超时时间,超出核心线程数量以外的线程空余存活时间
      TimeUnit unit, //存活时间单位
      BlockingQueue<Runnable> workQueue, //保存执行任务的队列
      ThreadFactory threadFactory,//创建新线程使用的工厂
      RejectedExecutionHandler handler //当任务无法执行的时候的处理方式
      
  12. 线程池原理:

    invalid image(图片无法加载)

  13. 说说 ThreadLocal 底层原理是什么,怎么避免内存泄漏?

    invalid image(图片无法加载)

    推荐阅读:ThreadLocal 面试六连问,中高级必问

  14. 说说你对 JUC 下并发工具类

    • Semaphore

      是一种新的同步类,它是一个计数信号。从概念上讲,信号量维护了一个许可集合。

      • 如有必要,在许可可用前会阻塞每一个 acquire() 方法,然后再获取该许可。
      • 每个 release() 方法,添加一个许可,从而可能释放一个正在阻塞的获取者。
      • 但是,不使用实际的许可对象,Semaphore 只对可用许可的数量进行计数,并采取相应的行动。

      信号量常常用于多线程的代码中,比如数据库连接池。
      invalid image(图片无法加载)

    • CountDownLatch

      字面意思是减小计数(CountDown)的门闩(Latch)。它要做的事情是,等待指定数量的计数被减少,意味着门闩被打开,然后进行执行。
      CountDownLatch 默认的构造方法是 CountDownLatch(int count),其参数表示需要减少的计数,主线程调用 await() 方法告诉 CountDownLatch 阻塞等待指定数量的计数被减少,然后其它线程调用 CountDownLatch 的 CountDown() 方法,减小计数(不会阻塞)。等待计数被减少到零,主线程结束阻塞等待,继续往下执行。

    • CyclicBarrier

      字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
      CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞,直到 parties 个线程到达,结束阻塞。

  15. CyclicBarrier 和 CountdownLatch 有什么区别?

    • CyclicBarrier 可以重复使用,而 CountdownLatch 不能重复使用。

      CountdownLatch 其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作。可以向 CountdownLatch 对象设置一个初始的数字作为计数值,任何调用这个对象上的 await() 方法都会阻塞,直到这个计数器的计数值被其他的线程减为 0 为止。所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次 —— 计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
      CountdownLatch 的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个 CountdownLatch 对象的 #await() 方法,其他的任务执行完自己的任务后调用同一个 CountdownLatch 对象上的 countDown() 方法,这个调用 #await() 方法的任务将一直阻塞等待,直到这个 CountdownLatch 对象的计数值减到 0 为止。 CyclicBarrier 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环的 barrier。

嘿手大叔 2024年12月24日 11:17 收藏文档