JAVA高并发五 JDK并发包

1 多线程的团队协作:同步控制

1.1 synchronized的功能扩展:重入锁

  • 重入锁可以完全替代synchronized关键字。jdk5前版本重入锁性能远高于synchronized,jdk6开始,两者的性能差异并不是很大。
  • 开发人员需要手动指定何时加锁、何时释放锁。如果一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。如果释放次数过多,则会抛出异常,如果释放少,则会导致临界资源没有释放其他线程无法进入临界区。
  • 中断响应 
    • synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。
    • 重入锁可以在等待锁的过程中,取消对锁的请求。被中断的线程会放弃任务直接退出,释放资源。EX:参考73页
  • 锁申请等待限时 
    • 可以使用tryLock进行限时等待锁。
  • 公平锁 
    • 通过ReentrantLock的构造函数,传入true表示公平锁。公平锁的一大特点就是不会产生饥饿现象。如果使用synchroinzed则产生的锁为非公平锁。公平锁需要维护一个有序队列,因此实现成本高,性能也相对非常低下,因此默认情况下锁都是非公平的。
  • 方法: 
    • lock:获得锁,如果锁已经被占用,则等待
    • lockInterruptibly:获得锁,但优先响应中断
    • tryLock:无等待尝试获得锁,成功获得锁返回true,失败返回false
    • tryLock(long time,TimeUnit unit):在给定时间内尝试获得锁
    • unlock:释放锁
  • 在重入锁的实现中,主要包含了三个要素: 
    1. 原子状态:使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有
    2. 等待队列:所有没有请求到锁的线程,进入等待队列进行等待。
    3. 阻塞原语:park()和unpark(),用来挂起和恢复线程。3.1.2 重入锁的好搭档:Condition条件

1.2 重入锁的好搭档:Condition条件

         同wait和notify的作用大致相同,配合重入锁使用。 
- 方法: 
- await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。 
- awaitUninterruptibly()方法与await()方法基本相同,但是它并不会在等待过程中响应中断。 
- signal()方法用于唤醒一个在等待中的线程。相对的signalAll()方法会唤醒所有在等待中的线程。

1.3 允许多个线程同时访问:信号量(Semaphore)

  • 广义上说,信号量是对锁的扩展。
  • 内部锁synchronized和重入锁,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
  • 方法: 
    • acquire()获取一个许可,无法获得则等待,直到有一个线程释放一个许可或者当前线程被中断。
    • acquireUninterruptibly()和acquire()类似,只是不响应中断。
    • tryAcquire()尝试获得一个许可,不等待
    • release()线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。

1.4 ReadWriteLock读写锁

       读读不互斥、读写互斥、写写互斥

1.5 倒计时:CountDownLatch

1.6 循环栅栏:CyclicBarrier

  • 与CountDownLatch类似,但比其更加强大。可以实现循环阻塞,如等待10个线程后执行,再继续等待10个线程再执行。EX:参考90页
  • CyclicBarrier.await()方法会抛出两个异常,一个中断异常,一个BrokenBarrierException,破损异常,表示CyclicBarrier已经破损了,可能系统已经没有办法等待所有线程到齐了。如果继续等待可能就是徒劳的,因此可以据此结束等待处理等。

1.7 线程阻塞工具类:LockSupport

  • 与Thread.suspend相比,它祢补了由于resume在前发生,导致线程无法继续执行的情况。与Object.wait()相比,它不需要先获取某个对象锁,也不会抛出InterruptedExcepton
  • 方法: 
    • 静态方法park阻塞当前线程,parkNanos、parkUntil实现一个限时阻塞。
    • 使用了类似信号量的机制,它为每一个线程准备了一个许可,如果许可可用,则park会立即返回,并且消费这个许可,如果许可不可用,则阻塞。而unpark则使得一个许可变为可用,但同信号量不同的是,许可不能累加,不可能拥有超过一个许可。
    • park挂起也不会产生想suspend那样还是一个Runnable状态,它会是WAITTING状态。同时park可以传入阻塞对象,这个阻塞对象会出现在线程Dump中,可以便于分析异常。如LockSupport.park(this)
    • park还支持中断影响,但和其他接收中断的函数不一样,其不会抛出InterruptedException异常,只会默默的返回,但可以从Thread.interrupted等方法获得中断标记。

2 线程复用:线程池

扫描二维码关注公众号,回复: 2810945 查看本文章

2.1 什么是线程池

      创建线程变成了从线程池中获取空闲线程,销毁线程变成了向线程池归还线程。

2.2 不要重复发明轮子:JDK对线程池的支持

Executor框架 

  • ThreadPoolExecutor表示一个线程池。Executors扮演着线程池工厂的角色。
  • newFixedThreadPool()方法:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有空闲线程时,则新提交的任务立即执行,如果没有则在一个队列中等待。
  • newSingleThreadExecutor()方法:该方法返回一个只有一个线程的线程池。若处于非空闲状态,新提交的任务则在队列中等待。
  • newCacheThreadPool()方法:返回一个可根据实际情况调整线程数的线程池。线程池的数量不确定,如果有空闲可以复用,则优先使用复用线程。如果不存在空闲线程,则创建一个新的线程处理任务,处理完成后返回线程池中等待复用。
  • newSingleThreadScheduleExecutor()方法:返回一个ScheduledExecutorService对象,线程池大小为1。其在ExecutorService之上扩展了给定时间执行某任务的功能。如在某个固定的延时之后执行,或者周期性执行某个任务。
  • newScheduledThreadPool()方法:返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。 
    1. 固定大小的线程池: newFixedThreadPool
    2. 计划任务 
      • newScheduledThreadPool,返回一个ScheduledExecutorService对象。如果调度的任务产生了异常,则会导致后续的执行都会被中断,因此必须处理好异常的捕获。
      • schedule()方法:在给定的时间,对任务进行一次调度。不会周期性调度
      • scheduleAtFixedRate()方法:任务调度频率一定,以上一个任务开始时间为起始时间,之后period时间后调度下一次任务。如果任务的执行时间超过了定时调度的时间,则下一个任务会在上一个任务结束后立即调用。
      • scheduleWithFixedDelay()方法:任务调度时以上一次执行结束为起始时间的,之后经过delay时间执行任务。

2.3 刨根究底:核心线程池的内部实现

上面的各种类型线程池,都是通过ThreadPoolExecutor实现。 
- ThreadPoolExecutor构造函数参数含义: 
- corePoolSize:指定了线程池中的线程数量 
- maximumPoolSize:指定了线程池中的最大线程数量 
- keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间。即,超过corePoolSize的空闲线程,在多长时间内会被销毁 
- unit:keepAliveTime的单位 
- workQueue:任务队列,被提交但尚未被执行的任务 
- threadFactory:线程工厂,用于创建线程,一般用默认即可 
- handler:拒绝策略。当任务太多来不及处理,如何拒绝任务 
- workQueue说明:是一个BlockingQueue接口对象,存放Runnable对象。有以下几种BolckingQueue: 
1. 直接提交的队列 
- 由SynchronousQueue对象提供。一个特殊的BlockingQueue,没有容量,每一个插入操作都要等待一个相应的删除操作,反之,每一个删除操作都需要等待一个插入操作。如果使用该队列,则任务不会被真实保存,而总是将任务提交给线程执行。如果没有空闲线程,则创建新线程,如果数量达到了最大值,则执行拒绝策略,因此其需要设置很大的maximumPoolSize值,否则很容易执行拒绝策略。 
2. 有界的任务队列 
- 由ArrayBlockingQueue实现。构造函数必须带一个容量参数,表示该队列的最大容量。如果实际线程数小于corePoolSize则创建线程执行,如果超出,则进入等待队列,如果等待队列满,且当前线程数小于maximumPoolSize则继续创建新线程执行任务,如果超出maximumPoolSize则执行拒绝策略。因此,有界队列必须在队列满时,才会将线程数提升大于corePoolSize,即确保了核心线程数维持在corePoolSize之下。 
3. 无界的任务队列 
- 由LinkedBlockingQueue实现。除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新任务来时,如果线程数小于corePoolSize,则创建新线程,如果大于corePoolSize则在队列中等待,如果创建和处理任务的速度差异很大,则等待队列会不断增加,知道内存耗尽。 
4. 优先的任务队列 
- 由PriorityBlockingQueue实现。一个特殊的无界任务队列,可以控制任务执行的先后顺序。 
- newFixedThreadPool采用的是corePoolSize=maxmimuPoolSize的LinkedBlockingQueue无界任务队列,因此会存在资源耗尽的问题。 
- newSingleThreadExecutor是上一种的一种退化,即corePoolSize=maxmimuPoolSize=1 
- newCacheThreadPool返回corePoolSize为0,maxmimuPoolSize无穷大的线程池,在没任务时,线程池内无线程,当任务提交,如果有空闲则立即执行,如果没有空闲则提交到SynchronousQueue队列,因此又会创建新线程执行任务,当任务执行完毕,由于corePoolSize为0,因此空闲线程又会在指定时间内(60秒)被回收。

2.4 超负载了怎么办:拒绝策略

  • 通常由于压力太大引起,线程池中的线程已经用完,等待队列也满了。
  • JDK内置提供的四种拒绝策略: 
    • AbortPolicy策略:
    • 直接抛出异常,阻止系统正常运行
    • CallerRunsPolicy策略:
    • 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。这样不会真正的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降
    • DiscardOledestPolicy
    • 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
    • DiscardPolicy
    • 默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这可能是最好的一种策略。
    • 如无法满足需求,可以实现RejectedExecutionHandler接口,实现自己的拒绝策略

2.5 自定义线程创建:ThreadFactory

  • ThreadFactory是一个接口,只有一个方法,用来创建线程。
  • newThread(Runnable run) 
    • 当线程池需要创建新线程时,就调用该方法。

2.6 我的应用我做主:扩展线程池

  • ThreadPoolExecutor提供了beforeExecute、afterExecute、terminated三个接口对线程池进行控制。
  • shutdown是一个比较安全的方法不会暴力的关闭线程池,其会等待所有任务执行完毕后关闭线程池,但它并不会等待所有线程执行完成后再返回,简单理解则为发送了一个关闭的信号而已。但其执行后线程池就不会再接收其他新的任务了。

2.7 合理的选择:优化线程池线程数量

  • Ncp=CPU的数量
  • Ucpu=目标CPU的使用率, 
    • 0<=Ucpu<=1
    • W/C=等待时间与计算时间的比率
  • 为了保持处理器达到期望的使用率,最优的池的大小等于: 
    • Nthreads=Ncpu * Ucpu * (1+ W/C)

2.8 堆栈去哪里了:在线程池中寻找堆栈

  • 放弃submit方法,该用execute方法,如果需要查看任务在哪里提交,则需要扩展ThreadPoolExecutor。EX:参考115页

2.9 分而治之:Fork/Join框架

  • fork方法产生一个子线程,如果过多的fork则会导致线程太多,反而导致性能下降,ForkJoinPool线程池,对于fork方法并不急于开启线程,而是交给ForkJoinPool线程池处理,以节省系统资源。
  • 由于线程池的优化,提交的任务跟线程数量并不是一对一关系。在绝大多数情况下,一个物理线程实际上需要处理多个逻辑任务。因此实际中可能会存在A线程任务已经完成了,但B线程还没,此时A就会帮助B,当线程尝试去帮助其他线程时,总是从底部开始拿数据。因为这种行为有利于避免数据竞争。
  • ForkJoinTask就是支持fork分解和join等待的任务,ForkJoinTask两个重要子类:RecursiveAction和RecursiveTask,分别表示没有返回值和可以携带返回值的任务。
  • ForkJoin线程池使用一个无锁的栈来管理空闲线程池。
  • 注意:如果任务层次很深,一直得不到返回,则有两种可能: 
    1. 系统内的线程数量越积越多,导致性能严重下降
    2. 函数的调用层次变得很深,最终导致栈溢出

原文链接

参考链接1

参考链接2​​​​​​​

猜你喜欢

转载自blog.csdn.net/qq_34479912/article/details/81700385