菜鸟笔记
提升您的技术认知

Java并发面试题

Java并发面试题

1.什么是jmm

JMM即Java Memory Model(Java内存模型)。用来缓存一致性协议,定义数据读写的规则,解决主内存与线程工作内存数据不一致的问题。(Synchronized,voliate)

2.线程的状态

线程通常有五种状态,创建,就绪,运行,阻塞和死亡态。

  • 创建态:创建了一个线程对象
  • 就绪态:其他线程调用了该线程的start方法。该状态的线程位于可运行线程池中,等待获得CPU。
  • 运行态:线程获取了CPU,执行程序代码。
  • 阻塞态:线程由于某种原因放弃CPU使用权,暂时停止运行。
  • 死亡态:线程执行完程序代码或因异常而退出。

其中阻塞态主要有以下三种:

  • 等待阻塞:线程执行wait方法,释放占用的资源进入等待池中,若未设置wait时间则必须依靠其他线程的notify或notifyall方法唤醒。
  • 同步阻塞:线程在想要获取某个对象的同步锁时,该对象的同步锁正在被其他线程调用,则该线程进入该锁的“锁池”中等待。
  • 其他阻塞:运行的线程执行sleep、join方法,或发出了IO请求,jvm会把线程设置为阻塞状态。

3.sleep、wait、join、yield

sleep(线程休眠)与wait(线程等待)

  • sleep是Thread类的静态方法,wait则是Object类的本地方法。
  • sleep执行后不会释放lock,但wait会释放。
  • sleep休眠指定时间后自动唤醒,wait需要其他线程通知唤醒,多用于多线程间的通信

yield(线程礼让)与join(强制执行)

  • yield执行后线程让出CPU并进入就绪态,依然重新参与CPU的竞争
  • 调用某线程的join方法的线程会进入阻塞队列,直到某线程执行完成或中断

4.什么是线程安全

一般是指内存安全,如果多个线程对一个共享对象进行操作,无需添加额外的同步控制和协调操作,每个线程都能获取正确的结果,我们就说这个对象是线程安全的。

因为线程是共享所属进程的堆内存的,这也是会出现线程不安全情况的原因。

5.多线程的一般实现

  • 继承Thread类,重写run方法,new Thread().start()
  • 实现Runnable接口,重写run方法,new Thread(对象).start
  • 实现Callable接口,重写call方法,创建执行器(ExecutorService),提交线程对象,获取返回结果(Future(?)),关闭服务

通常使用实现接口的方式,避免单继承的局限性。

6.守护线程

线程分为用户线程和守护线程,守护线程是为所有非守护线程提供服务的,虚拟机必须确保用户线程执行完毕,不去管守护线程的运行状态。它依赖整个进程而运行,终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它,因为它不靠谱。例如GC线程就是守护线程,程序运行期间一直在为其他线程提供服务支持,结束时立即正常关闭。

7.ThreadLocal底层原理

每一个 Thread 对象均含有一个ThreadLocalMap类型的成员变量 threadLocals ,存储本线程中所有ThreadLocal对象及其对应的值。

ThreadLocalMap是由一个个Entry对象组成的,Entry 继承自 WeakReference<ThreadLocal<?>> ,一个 Entry 由 ThreadLocal 对象和 Object 构 成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该 key就会被垃圾收集器回收。核心方法有set,get、remove和setInitialValue。

  • set方法
    • 获取当前线程对象,获取其ThreadLocalMap对象
    • 若map对象不为空,则存入Entry,为空则新建一个map(createmap(Thread.currentThread,value))并存入Entry。
  • get方法
    • 获取当前线程对象,获取其ThreadLocalMap 对象。
    • 若map存在,再以当前ThreadLocal对象为key,获取对应的value,若map为空或获取的value为空,则设置初始值并返回。
  • setInitialValue,设置初始值,如果不被重写,默认值为null。
  • remove
    • 获取当前线程对象,获取其ThreadLocalMap 对象。
    • 若map不为空则remove该ThreadLocal对应的Entry。

8.ThreadLocal的内存泄漏

当业务执行完成,ThreadLocalRef消失,ThreadLocal的强引用为0,会被GC回收,此时由于当前线程还未执行完成,所以Entry对象的Value仍然保留着一个强引用,线程执行完成后很多时候不会被销毁,而是回到线程池,这也是ThreadLocal内存泄漏的根本原因。

解决方式:

  • 使用完成及时remove
  • 线程运行结束就销毁

另外,Entry中的Key使用弱引用还有一个好处,当ThreadLocal被回收,Key被置位Null时,即使Current Thread还在运行,它在调用set/get方法的时候,会对Key为Null进行处理,将Value也置为Null。因此弱引用比强引用多一层保障。

9.并发的特性

  • 原子性

原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要 不都不执行。

  • 可见性

参考JMM,实际上就是每个线程都有自己的工作内存,对于共享变量,线程的工作内存中的该变量都是对主内存中的拷贝。要做到其中一个线程对自己工作内存中的变量的修改立即被其他线程知晓。

  • 有序性

虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按 照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。

10.volatile和synchronized关键字

  • synchronized保证原子性、可见性和有序性,而volatile只保证可见性和有序性。
  • synchronized可修饰方法和类,而volatile只能用来修饰变量
  • volatile禁止指令重排序,同时其修饰的变量对所有线程可见,即一个线程修改该值后会强制将修改的值立即写入主存,所有线程工作内存中的缓存失效,会重新去主存中读取。synchronized关键字是通过指定其内容同一时刻只能有一个线程对其进行操作来实现的。

11.如何避免死锁

死锁要满足以下条件:

  • 互斥:一个资源同一时刻只能被一个进程使用
  • 请求和保持:进程在请求新的资源时,不释放已经请求到的资源
  • 不可剥夺:进程在执行完成前,其所请求到的资源不可被其他进程剥夺
  • 循环等待:有限个进程都持有其他进程想请求的资源形成的一个环形队列

如何避免:

  • 银行家算法
  • 破坏四个条件之一即可

12.线程池的作用

  • 降低资源消耗;减少了线程创建、销毁时的开销
  • 提高响应速度;请求来时可直接分配线程运行,无需创建线程再去执行
  • 提高线程的可管理性:使⽤线程池可以统⼀对线程进行分配、调优、监控

13.线程池常用参数

  • corePoolSize:核心线程数,也就是正常情况下创建的线程数,这些线程在创建后不会销毁,而是一种常驻线程
  • maxnumPoolSize:最大线程数,当核心线程数全部被占用,当前任务仍然较多,就会创建新的线程,但线程数不会超过这个值
  • keepAliveTime:表示超出核心线程数的线程的空闲存活时间
  • workQueue:用来存放待执行的任务,当队列被放满但还有任务进入,就会创建新的线程
  • ThreadFactory:线程工厂,用来创建线程
  • Handler:任务拒绝策略
    • 关闭线程池后,线程池中未完成的任务在执行完成后,向线程池提交任务,但线程池已关闭
    • 达到最大线程数,线程池没有能力继续处理任务了

14.线程池工作策略

线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:

  1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊缓冲队列。
  3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数 量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等 于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被 终⽌。这样,线程池可以动态的调整池中的线程数

15.线程池阻塞队列

1.一般的队列只能作为一定长度的缓冲区,如果超过了缓存长度就无法被保留,阻塞队列可以通过阻塞的方式保留住当前想要继续入队的任务。

2.在创建新的线程时,是要获取全局锁的,这个时候其他的线程就要阻塞,影响了整体效率。

16.线程池中线程复用原理

线程池将线程和任务解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时一个线程对应一个任务的限制。

在线程池中,线程不是通过new Thread.start来启动任务的,而是通过封装Thread类,让每个线程执行一个“循环任务”,不断从阻塞队列中判断是否有新的任务需要执行,如果有则直接执行,也就是调用任务的run方法,将run方法当一个普通方法来执行;通过这种方式就可以使用固定线程将所有任务的run方法串联起来。

17.ReentrantLock的公平锁与非公平锁

无论是公平锁还是非公平锁,底层都会使用AQS来进行排队,它们的区别在于:线程在使用lock方法加锁时,如果是公平锁,会先检查AQS队列中是否有线程在排队,如果有线程在排队,则把该线程也加入排队队列中;如果是非公平锁,则不会去检查是否有线程在排队,直接竞争锁。

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队。当锁释放时,都会唤醒排在最前面的线程,所以非公平锁只是体现在加锁阶段,而没有体现到被唤醒阶段。

ReentrantLock是可重入锁,无论是非公平锁还是公平锁,都是可重入的。

18.ReentrantLock中的tryLock()和lock()方法

  1. tryLock()方法表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回FALSE
  2. lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值

19.Sychronized的偏向锁、轻量级锁、重量级锁

  1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了
  2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个 线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
  3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运 ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

20.Sychronized和ReentrantLock的区别

  1. sychronized是⼀个关键字,ReentrantLock是⼀个类
  2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
  3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
  4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态
  6. sychronized底层有⼀个锁升级的过程

21.AQS如何实现可重入锁

可重入是多线程并发编程中一个比较重要的概念,简单来讲就是正在运行的某段函数或代码因为抢占资源或者中断导致其运行过程被中断,那么等中断的处理结束以后,重新进入到这段代码里面时再运行时,其运行结果不会改变,就称这个函数或代码是可重入的。

可重入锁是指当一个线程拿到了关于互斥锁的资源,在锁释放之前,再去竞争同一把锁的时候,不需要等待,只需要去记录重入次数。在多线程并发编程中,绝大部分锁都是可重入的,如ReentrantLock和Synchronized,但是也有不支持重入的,如jdk1.8中的读写锁StampedLock。

锁的可重入性主要为了解决死锁问题的,当一个线程拿到某同步锁之后,在释放之前再去申请锁时,可能会出现自己等待自己释放锁的一个情况,会导致死锁。

  • AQS是一个Java线程同步的框架,是JDK中很多锁工具的核心实现框架。
  • 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。这个线程队列就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或放行的。在不同场景下,有不同的意义。
  • 在可重入锁这个场景下,state用来表示加锁的次数。0表示无锁,每加一个锁,state的值就+1,释放锁state的值-1。

22.CountDownLatch和Semaphore

CountDownLatch表示计数器,可以设置一个初始值,当线程调用CountDownLatch的await()方法时将被阻塞,其他线程可以调用CountDowmnLatch中的countDown()方法将CountDowmnLatch中的值-1,当值减为0时,所有await的线程都将被唤醒。对应的底层原理是,调用await的线程将利用AQS进行排队,当数字被减为0后,会依次唤醒AQS中排队的线程。

Semaphore表示信号量,可以设置许可的个数,表示最多允许多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可则线程阻塞,并通过AQS来进行排队,线程可利用release()方法来释放许可,释放许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲的许可。

23.什么是CAS?

并发编程中会经常遇到一个问题,read-write问题

int state = 0;

if(state == 0){
  
	state == 1;
}

由于读state的值和修改state的值不是原子性操作,因此在并发情况下可能会出现问题,可以通过加锁来解决,但是会降低性能,这时候就可以用CAS解决。

CAS指unSafe类下的方法compareAndSwap(比较并交换),方法的四个参数依次是此对象、比较对象的偏移地址、预期值、期望更改后的值。CAS机制会比较传入对象的偏移地址对应的值是否跟预期值一样,如果一样就直接修改内存地址中的值为期望更改的值,否则返回false。这是一个原子性的操作,不会存在线程安全的问题。

compareAndSwap是一个native方法,实际上它最终还是会面临同样的读写问题,在多核cpu环境下,底层实现依然会使用lock指令对缓存和总线加锁,从而保证比较和交换这两个操作的原子性。

应用场景:

  • JUC里Atomic的原子性实现。
  • 实现多线程对共享资源竞争的互斥性质,如AQS、CurrentHashMap等。