当前位置: 首页>后端>正文

Java基础(六)-多线程-3

  • 问: 说说你对JUC的了解
    JUC是java.util.concurrent的缩写.JUC是Java提供的并发包,其中包含了一些并发编程用到的基础组件.
    JUC这个包下的类基本上包含了我们在并发编程时用到的一些工具,大致可以分为以下几类:
  1. 原子更新
    Java从JDK1.5开始了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作. 在Atomic包里一共有12个类,四种原子更新方式分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段.
  2. 锁和条件变量
    java.util.concurrent.locks包下包含了同步器的框架AbstractQueuedSynchronizer,基于AQS构建的Lock以及与Lock配合可以实现等待/通知模式的Condition.JUC下的大多数工具类用到了Lock和Condition来实现并发.
  3. 线程池
    涉及到的类比如Executor,Executors,ThreadPoolExector,AbstractExecutorService,Futrue,Callable,ScheduledThreadPoolExecutor等等
  4. 阻塞队列
    涉及到的类比如: ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,LinkedBlockingDeque等
  5. 并发容器
    涉及到的类比如:
    ConcurrentHashMap,CopyOnWriteArrayList,ConcurrentLinkedQueue,CopyOnWriteArraySet等
  6. 同步器
    剩下一些在并发编程中时常会用到的工具类,主要用来协助线程同步. 比如 CountDownLatch,CyclicBarrier,Exchanger,Semaphore,FutureTask等

-问 说说你对AQS的理解
答: 抽象队列同步器AbstractQueuedSynchroizer
(简称AQS),用来构建锁或者其他同步组件的骨架类,减少了各功能组件实现的代码量,也解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作的顺序.在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是等待
AQS采用模板方法模式,在内部维护了n多的模板的方法的基础上,子类只需要实现特定的几个方法(不是抽象方法),就可以实现子类自己的需求
基于AQS实现的组件,诸如:
ReentrantLock可重入锁
Semaphore计数信号量
ReentrantReadWriteLock读写锁
拓展阅读
AQS内部维护了一个int成员变量来表示同步状态,通过内置的FIFO同步队列来控制共享资源的线程
我们可以猜测出,AQS其实主要做了这么几件事情:
同步状态(state)的维护管理
等待队列的维护管理
线程的阻塞与唤醒

通过AQS内部维护的int型的state,可以表示任意状态!
ReentrantLock用它来表示锁的持有者线程已经重复获取该锁的次数,而对于非锁的持有者线程来说,如果state大于0,意味着无法获取该锁,将该线程包装为Node,加入到同步等待队列里
Semaphore用它来表示剩余的许可数量,当许可数量为0时,对于未获取到许可但正在努力尝试获取许可的线程来说,会进入同步等待队列,阻塞,直到一些线程释放掉持有的许可(state+1),然后争用释放掉的许可.
FutureTask用它来表示任务的状态(未开始,运行中,完成,取消)
ReentrantReadWriteLock在使用时,稍微有些不同,int型state用二进制表示32位,前16位(高位)表示读锁,后面的16位表示写锁.
CountDownLatch使用state表示计数次数,state大于0,表示需要加入到同步等待队列并阻塞,直到state等于0,才会逐一唤醒等待队列里的线程

AQS通过内置的FIFO同步队列来控制获取共享资源的线程.CLH队列的FIFO的双端双向队列,AQS的同步机制就是依靠这个CLH队列完成的.队列的每个节点,都有前驱节点指针和后继节
点指针.


Java基础(六)-多线程-3,第1张
CLH队列示意图
  • 问: LongAdder解决了什么问题,它是如何实现的?
    答: 高并发下计数,一般最先想到的应该是AtomicLong/AtomicInt,AtomicXXX使用硬件级别的指令CAS来更新计数器的值,这样可以避免加锁,机器直接支持的指令,效率也很高。但是AtomicXXX中的CAS操作在出现线程竞争时,失败的线程会白白地循环一次,在并发很大的情况下,因为每次CAS都只有一个线程能成功,竞争失败的线程会非常多。失败次数越多,循环次数就越多,很多线程的CAS操作越来越接近自旋锁(spin lock)。 计数操作本来是一个很简单的操作,实际需要耗费的CPU时间应该是越少越好,AtomicXXX在高并发计数时,大量的cpu时间都浪费在自旋上,很浪费,也降低了实际的计数效率。
    LongAdder是jdk8新增的用于并发环境的计数器,目的是为了在高并发情况下,代替AtomicLong/AtomicInt,成为一个用于高并发情况下的高效的通用计数器。说LongAdder比在高并发时比AtomicLong更高效,依据是LongAdder是根据锁分断来实现的,它里面维护一组按需分配的计数单元,并发计数时,不同的线程可以在不同的计数单元上计数,这样减少了线程竞争,提高了并发效率。本质上是用空间换时间的思想,不过在实际高并发情况中消耗的空间可以忽略不计。
    现在,在处理高并发计数时,应该优先使用LongAdder,而不是继续使用AtomicLong,当然,线程竞争很低的情况下进行计数,使用Atomic还是更简单直接,并且效率稍微高一些。其他情况,比如序号生成,这种情况下需要准确的数值,全局唯一的AtomicLong才是正确的选择,此时不应该使用LongAdder。

  • 问 介绍下ThreadLocal和它的应用场景
    答: ThreadLocal顾名思义是线程私有的局部变量存储容器,可以理解成每个线程都有自己专属的存储容器,它用来存储线程私有变量,其实它只是一个外壳,内部真正存取是一个Map。每个线程可以通过set()和get()存取变量,多线程间无法访问各自的局部变量,相当于在每个线程间建立了一个隔板。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。总之一句话,ThreadLocal存储的变量属于当前线程。
    ThreadLocal经典的使用场景是为每个线程分配一个JDBC连接Connection,这样就可以保证每个线程的都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程正在使用的Connection,另外ThreadLocal还经常用于管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是同一个Session。

  • 请介绍下线程池
    答: 系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互. 这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池.
    与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或者call()方法.当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待下一个Runnable对象的run()或call()方法.
    从Java5开始,Java内建支持线程池. Java 5新增了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂来创建线程池. 创建出来的线程池,都是通过ThreadPoolExecutor类来实现的.

  1. newCachedThreadPool(): 创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中.
  2. newFixedThreadPool(int nThreads): 创建一个可重用的,具有固定线程数的线程池.
  3. newSingleThreadExecutor(): 创建一个只有单线程的线程池,它相当于调用newFixedThread Pool()方法时传入参数为1
  4. newScheduledThreadPool(int corePoolSize): 创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务.corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内.
  5. newSingleThreadScheduledExecutor(): 创建只有一个线程的线程池,它可以在指定延迟后执行线程任务.
  6. ExecutorService newWorkStealingPool(int parallelism): 创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争.
  7. ExecutorService newWorkStealingPool(): 该方法是前一个方法的简化版本. 如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于为前一个方法传入4作为参数.
  • 问 介绍下线程池的工作流程


    Java基础(六)-多线程-3,第2张
    线程池的工作流程
  • 线程池有哪些状态?
    线程池一共有五种状态: 分别是:

  1. RUNNUNG : 能接受新提交的任务,并且也能处理阻塞队列中的任务
  2. SHUTDOWN: 关闭状态, 不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务. 在线程池处于RUNNING状态时,调用shutdown()方法会使线程池进入到该状态
  3. STOP : 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程. 在线程池处于RUNNING或SHUTDOWN状态时,调用shutdownNow()方法会使线程池进入到该状态
  4. TIDYING: 如果所有的任务都已终止了,workerCount(有效线程数)为0.线程池进入该状态后会调用terminated()方法进入TERMINATED状态
  5. TERMINATED: 在terminated()方法执行完后进入该状态,默认terminated()方法中什么也没有做.进入TERMINATED的条件如下:
    线程池不是RUNNING状态;
    线程池状态不是TIDYING状态或TERMINATED状态
    如果线程池状态是SHUTDOWN并且workerQueue为空
    workerCount为0
    设置TIDYING状态成功


    Java基础(六)-多线程-3,第3张
    线程池转换过程
  • 谈谈线程池的拒绝策略
    当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
    AbortPolicy:丢弃任务并抛出 RejectedExecutionException异常
    DiscardPolicy: 也是丢弃任务,但是不抛出异常
    DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)
    CallerRunsPolicy: 由调用线程处理该任务
  • 线程池的队列大小如何设置?
  1. CPU密集型任务
    尽量使用较小的线程池,一般为CPU核心数+1. 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换
  2. IO密集型任务
    可以使用稍大的线程池,一般为2*CPU核心数.IO密集任务CPU使用率并不高,因此可以让CPU在等候IO的时候有其他线程去处理别的任务,充分利用CPU时间
  3. 混合型任务
    可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理. 只要分完之后两个任务的时间相差不大,那么就会比串行执行来的高效. 因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义. 因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失.
  • 线程池有哪些参数,各个参数的作用是什么?
    线程池主要有如下6个参数
    corePoolSize(核心工作线程数): 当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或大于corePoolSize时.
    maximumPoolSize(最大线程数):线程池所允许的最大线程个数.当队列满了, 且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务.另外,对于无界队列,可忽略该参数
    keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数.
    workQueue(队列): 用于传输和保存等待执行任务的阻塞队列
    threadFactory(线程创建工厂):用于创建新线程. threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:Pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
    handler(拒绝策略): 当线程池和队列都满了,再加入线程池会执行此策略.

https://www.xamrdz.com/backend/33n1924756.html

相关文章: