JUC并发面试题

Java中有几种方式可以创建线程执行任务?

  • 第一种,继承Thread类,缺点是占用了继承的名额,Java中的类是单继承的
  • 第二种,实现Runnable接口,接口可以多实现,可以使用匿名内部类的方式或者Lambda表达式来替换
  • 第三种,实现Callable接口,实现call()方法,得使用Thread+FutureTask配合,这种方式支持拿到结果
  • 第四种,利用线程池来创建线程

以上几种方式,底层都是基于Runnable。

并发和并行

并行就是两条平行线,同时执行,比如两个线程分别在两个不同的CPU执行,这就是并行。

并发通常会带数量,比如1000万的并发,值的是一个系统能同时处理1000万请求,而不是1000万个系统。

或者说一个CPU按时间片切换执行不同的线程,这个就是并发。

如何理解volatile关键字

在并发领域中,存在三大特性:原子性、有序性、可见性。volatile关键字用来修饰对象的属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,所以同时也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。

ReentrantLock分公平锁和非公平锁,那底层是如何实现的?

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于线程在使用lock()方法加锁时:

  1. 如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队
  2. 如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

ReentrantLock中tryLock()和lock()方法的区别

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

CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。
对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。

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

Sychronized的锁升级过程是怎样的?

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

Sychronized和ReentrantLock的区别

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

线程池有哪些状态?每种状态的效果是怎样的?

  1. RUNNING: Accept new tasks and process queued tasks,线程池正常运行,既接受新任务,也会处理队列中的任务
  2. SHUTDOWN:Don't accept new tasks, but process queued tasks,当调用线程池的shutdown()方法时,线程池就进入SHUTDOWN状态,线程池不会接受新任务了,但是会继续处理队列中的任务
  3. STOP:Don't accept new tasks, don't process queued tasks, and interrupt in-progress tasks,当调用线程池的shutdownnow()方法时,线程池就进入STOP状态,线程池既不会接受新任务了,也不会处理队列中的任务,并且正在运行的线程也会被中断
  4. TIDYING:All tasks have terminated, workerCount is zero, the thread transitioning to state TIDYING will run the terminated() hook method,当线程池中没有线程在运行后,线程池的状态就会自动变为TIDYING,并且会调用terminated(),该方法是空方法,留给程序员进行扩展。
  5. TERMINATED:terminated() has completed,terminated()方法执行完之后,线程池状态就会变为TERMINATED

线程池的底层工作原理

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

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

线程池中的线程是如何保活和回收的?

我们知道线程池的作用就是提高线程的利用率,需要线程时,可以直接从线程池中获取线程直接使用,而不用创建线程,那线程池中的线程,在没有任务执行时,是如何保活的呢?

线程池中的线程会不断地的从阻塞队列中获取任务,调用的是workQueue.poll()方法或take(),这两个方法都会阻塞式的从队列中获取元素,区别式poll()方法可以设置一个超时时间,take()不能设置超时时间,所以这也间接的使得线程池中的线程阻塞等待从而达到保活的效果。

当然并不是线程池中的所有线程都需要一直保活,比如只有核心线程需要保活,非核心线程就不需要保活,那非核心线程是怎么回收的呢?

底层是这样的,当一个线程处理完当前任务后,就会开始去阻塞队列中获取任务,只不过,在调用poll或take方法之前,会判断当前线程池中有多少个线程,如果多余核心线程数,那么当前线程就会调用poll()并设置超时时间来获取阻塞队列中的任务,这样一旦时间到了还没有获取到任务,那么线程就不会阻塞了,并且没有业务执行,那么线程就会运行结束,也就是回收了。

为什么不建议使用Executors来创建线程池?

当我们使用Executors创建FixedThreadPool时,使用的队列为LinkedBlockingQueue,是一个无界阻塞队列,如果使用该线程池执行任务,如果任务过多就会不断的添加到队列中,任务越多占用的内存就越多,最终可能耗尽内存,导致OOM。

除开有可能造成OOM之外,我们使用Executors来创建线程池也不能自定义线程的名字,不利于排查问题,所以建议直接使用ThreadPoolExecutor来定义线程池,这样可以灵活控制。

固定大小的线程池:Executors.newFixedThreadPool()方法创建的线程池具有固定的线程数。如果任务数量超过线程池的最大容量,那么任务将会被放入队列中等待执行。如果队列中的任务堆积过多,可能会导致内存溢出或者应用程序崩溃。

  1. 缓存线程池Executors.newCachedThreadPool()方法创建的线程池可以根据需要动态地创建新的线程,但是没有限制线程的数量。这意味着当任务数量非常大时,线程池可能会创建大量的线程,从而消耗大量的系统资源。方法创建的线程池允许线程数增长到Integer.MAX_VALUE,可能会创建过多线程,导致系统资源耗尽。
  2. 单线程线程池Executors.newSingleThreadExecutor()方法创建的线程池只有一个工作线程,适用于需要保证任务按照顺序执行的场景。然而,如果该线程由于异常终止而结束,线程池会创建一个新的线程来替代它。这可能会导致无限制地创建新线程,从而耗尽系统资源。
  3. 固定大小线程池Executors.newFixedThreadPool()

newFixedThreadPoolnewSingleThreadExecutor创建的线程池具有无限的任务队列,可能会导致无界队列持续增长,消耗大量内存。

线程池的五种状态是如何流转的?

线程池有五种状态:

  1. RUNNING:能接受新任务,也能处理阻塞队列中的任务。
  2. SHUTDOWN:不接受新任务,但能处理阻塞队列中的任务。
  3. STOP:不接受新任务,不处理阻塞队列中的任务,中断正在处理的任务(注意:一个任务能不能被中断得看任务本身)。
  4. TIDYING:所有任务都已终止,workerCount(有效线程数)为0,线程池即将转换到TERMINATED状态,一旦达到此状态,就会调用线程池的terminated()。
  5. TERMINATEDterminated()方法已经被执行就会转变为TERMINATED。


 

这五种状态并不能任意转换,只会有以下几种转换情况:

  1. RUNNING -> SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()
  2. (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()触发,如果先调shutdown()紧着调shutdownNow(),就会发生SHUTDOWN -> STOP
  3. SHUTDOWN -> TIDYING:队列为空并且线程池中没有线程时自动转换
  4. STOP -> TIDYING:线程池中没有线程时自动转换(队列中可能还有任务)
  5. TIDYING -> TERMINATED:terminated()执行完后就会自动转换


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值