Java并发编程:并发工具(上)

6. 并发工具

6.1 自定义线程池

  • 线程池、阻塞队列(平衡生产者和消费者之间的桥梁),线程池中的线程相当于消费者,而生产者线程会产生源源不断的任务。
  • 自定义任务队列过程:
    • 属性
      • 创建任务队列,使用双向链表
      • 给头部和尾部任务上锁
      • 设置生产者条件变量,任务满就等待
      • 设置消费者条件变量,任务空就等待
      • 设置容量上限
    • 方法
      • 阻塞获取,队列为空就等待,否则取出第一个元素,需要唤醒添加方法
      • 阻塞添加,队列为满就阻塞,否则就把新的元素放到队列尾部,需要唤醒获取方法
      • 获取大小
      • poll:带超时的阻塞获取,防止虚假唤醒,将需要等待的剩余时间返回
  • 自定义线程池:
    • 属性
      • 任务队列
      • 线程集合,workers
      • 核心线程数
      • 超时时间
    • 方法:
      • 构造方法:参数有核心数、超时时间、时间单位、任务队列
      • execute(Runnable task):执行任务,当任务数没有超过coreSize时,直接交给worker对象执行,如果超过coreSize时,加入任务队列暂存
    • 需要用到Worker对象,真正执行,Worker类中的run方法:当task不为空执行任务,当task执行完毕,再接着从任务队列中获取任务再执行(选择不同的实现方式,take死等待或者poll超时(移除worker))
    • 主线程:
      • 创建线程池
      • 放任务
    • 如果放入任务数超过任务队列数,有很多处理方法,可以抽象成接口,让调用者自己决定哪种处理方式,这就是策略模式
      • 死等:queue.put(task)
      • 可以设置超时时间。queue.offer(task,timeout,timeunit):带超时时间的阻塞添加。
      • 让调用者放弃任务执行, 空操作
      • 让调用者抛出异常
      • 让调用者自己执行任务 task.run()

6.2 ThreadPoolExecutor

  • 线程池状态:ThreadPoolExecutor 使用 int 的高 3 位(最高位代表符号)来表示线程池状态,低 29 位表示线程数量
    在这里插入图片描述

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作 进行赋值

  • 构造方法
public ThreadPoolExecutor(int corePoolSize,
 				int maximumPoolSize,
 				long keepAliveTime,
 				TimeUnit unit,
 				BlockingQueue<Runnable> workQueue,
 				ThreadFactory threadFactory,
 				RejectedExecutionHandler handler)

corePoolSize 核心线程数目 (最多保留的线程数)

maximumPoolSize 最大线程数目

keepAliveTime 生存时间 - 针对救急线程

unit 时间单位 - 针对救急线程

workQueue 阻塞队列

threadFactory 线程工厂 - 可以为线程创建时起个好名字

handler 拒绝策略

  • 工作方式:

    • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务
    • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排 队,直到有空闲的线程。
    • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急(救急线程,有生存时间)
    • 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现:
      • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
      • CallerRunsPolicy 让调用者运行任务 DiscardPolicy 放弃本次任务
      • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
      • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
      • Netty 的实现,是创建一个新线程来执行任务
      • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
      • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 工厂方法:三个线程池

    • newFixedThreadPool

      public static ExecutorService newFixedThreadPool(int nThreads) {
       return new ThreadPoolExecutor(nThreads, nThreads,
       									0L, TimeUnit.MILLISECONDS,
       										new LinkedBlockingQueue<Runnable>());
       }
      
      

      核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间

      阻塞队列是无界的,可以放任意数量的任务,适用于任务量已知,相对耗时的任务

    • newCachedThreadPool,带缓冲的线程池

    public static ExecutorService newCachedThreadPool() {
     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
     60L, TimeUnit.SECONDS,
     new SynchronousQueue<Runnable>());
     }
    

    全部都是救急线程,60s后可以回收,可以无限创建

    队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的

    整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线 程。 适合任务数比较密集,但每个任务执行时间较短的情况

    • newSingleThreadExecutor 单线程线程池

      public static ExecutorService newSingleThreadExecutor() {
       return new FinalizableDelegatedExecutorService
              (new ThreadPoolExecutor(1, 1,
       				0L, TimeUnit.MILLISECONDS,
       					new LinkedBlockingQueue<Runnable>()));
      }
      

      使用场景: 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程 也不会被释放。

      区别:

      • 如果前面线程失败了,还会新建一个线程来顶替,保证线程池的正常工作
      • 与固定大小线程池线程数设置为1不同,单线程线程池对返回值做了包装,不能调用ThreadPoolExecutor中特有的方法。
  • 提交任务

    // 执行任务
    void execute(Runnable command);
    
     // 提交任务 task,用返回值 Future 获得任务执行结果
    <T> Future<T> submit(Callable<T> task);
    
     // 提交 tasks 中所有任务
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
     throws InterruptedException;
     // 提交 tasks 中所有任务,带超时时间
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
     long timeout, TimeUnit unit)
     throws InterruptedException;
    
     // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
     throws InterruptedException, ExecutionException;
    // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
     long timeout, TimeUnit unit)
     throws InterruptedException, ExecutionException, TimeoutException;
    
    

6.3 关闭线程池

shutdown

/*
线程池状态变为 SHUTDOWN- 不会接收新任务- 但已提交任务会执行完- 此方法不会阻塞调用线程的执行
*/
 public void shutdown() {
 final ReentrantLock mainLock = this.mainLock;
 mainLock.lock();
 try {
 checkShutdownAccess();
 // 修改线程池状态
advanceRunState(SHUTDOWN);
 // 仅会打断空闲线程
interruptIdleWorkers();
 onShutdown(); // 扩展点 ScheduledThreadPoolExecutor
    } 
finally {
 mainLock.unlock();
}
 // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)
 tryTerminate();
 }

shutdownNow

 /*
线程池状态变为 STOP- 不会接收新任务- 会将队列中的任务返回- 并用 interrupt 的方式中断正在执行的任务
*/
 public List<Runnable> shutdownNow() {
     List<Runnable> tasks;
 final ReentrantLock mainLock = this.mainLock;
 mainLock.lock();
 try {
 checkShutdownAccess();
 // 修改线程池状态
advanceRunState(STOP);
 // 打断所有线程
interruptWorkers();
 // 获取队列中剩余任务
tasks = drainQueue();
    } 
finally {
 mainLock.unlock();
    }
 // 尝试终结
tryTerminate();
 return tasks;
 }

其他方法

// 不在 RUNNING 状态的线程池,此方法就返回 true
 boolean isShutdown();
 // 线程池状态是否是 TERMINATED
 boolean isTerminated();
 // 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

6.4 AQS原理

全称:AbstractQueuedSynchronizer,阻塞式锁和相关的同步器工具框架

  • states属性:设置资源的状态,独占还是共享

    • getState
    • setState
    • compareAndSetState-乐观锁(cas)机制设置state状态
  • 子类需要实现的方法:

    • tryAcquire:获取锁的姿势

      //获取锁失败
      if(!tryAcquire(arg)){
          //入队,可以阻塞当前线程 使用park、unpark
      }
      
    • tryRelease:释放锁的姿势

      //如果释放锁成功
      if(tryRelease(arg)){
          //让阻塞线程恢复运行
      }
      
    • tryAcquireShared

    • tryReleaseShared

    • isHeldExclusively:是否持有独占锁

6.5 Reentrantlock 原理

默认为非公平锁

流程:

  • 加锁成功:

    在这里插入图片描述

  • 加锁失败,Thread1会执行:
    在这里插入图片描述

    1. CAS尝试将state由0变为1,失败

    2. 进入tryAcquire,继续失败

    3. 接下来进入addWaiter逻辑,构造Node队列,第一个Node称为Dummy,用来占位,不关联线程

      在这里插入图片描述

    4. 进入acquireQueued逻辑

    5. 不断尝试获得锁,失败后进入park阻塞

    6. 如果紧邻head,再次尝试获得锁,失败

    7. 进入shouldParkAfterFailedAcquire,将前驱节点,waitStatus改为-1,并返回false(还有机会,返回true则park)

      在这里插入图片描述

    8. 回到acquireQueued,再次尝试获取锁,失败

    9. 再次进入shouldParkAfterFailedAcquire,发现前驱结点waitStatus为-1,返回true

    10. 进入parkAndCheckInterrupt,阻塞

  • 释放锁:

    • thread0释放锁,进入tryRelease,设置ExclusiveOwnerThread为null,state=0

    • 如果当前队列不为null,并且head的waitStatus=-1,进入unparkSuccessor(唤醒后继结点)流程

    • 找到队列中离head最近的一个节点,unpark

    • 回到这个节点的acquireQueued流程
      在这里插入图片描述

    • 加锁成功的话

      • ExclusiveOwnerThread为Thread1,state=1
      • head指向刚刚的Node,该Node清空Thread
      • 之前的head被垃圾回收
    • 如果这个时候来了新线程,Thread就会获取失败,重新进入park阻塞

可重入原理

  • 判断当前线程是否是Owner线程
  • state++
  • 释放时,state–,减为0才能free

不可打断模式

此模式下,即使被打断,依然会驻留到AQS队列中,等获得锁后方能继续运行(只是知道有人打断过)

可打断模式

被打断时,抛出异常

非公平锁

只检查state是否为0,不会检查等待队列

公平锁:还要检查AQS队列中是否有前驱结点,没有才去竞争

条件变量

条件不满足进入waitset等待

Thread0持有锁,调用await,进入ConditionObject的addConditionWaiter流程

  • 创建新的Node状态为-2,关联Thread-0,加入等待队列尾部
  • FullRelease,清除State计数为0
  • unpark下一个节点

Signal方法,从ConditionObject断开,加入竞争锁的队列

  • 先检查是否是锁的持有者
  • 将下一个节点设置为null
  • 转移到等待队列
  • 如果转移失败,继续寻找下一个
  • 成功的话,将waitStatus设置为0

6.6 读写锁-ReentrantReadWriteLock

读读并发,读写互斥

注意:

  • 读锁不支持条件变量
  • 不能在有读锁的情况下获取写锁,会导致永久等待,即重入时升级
  • 可以在有写锁的情况下获取读锁,即重入时降级

6.7 semaphore

信号量,用来限制能访问共享资源的线程上限

public static void main(String[] args) {
 // 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3);
 // 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
 new Thread(() -> {
 // 3. 获取许可
		try {
				 semaphore.acquire();
                } 
				catch (InterruptedException e) {
					 e.printStackTrace();
                }
		 try {
 			log.debug("running...");
 			sleep(1);
 				log.debug("end...");
                }  finally {
 // 4. 释放许可
					semaphore.release();
                }
            }).start();
      }
    }

应用

适合单机线程数量,仅是限制线程数,不限制资源数

可以优化数据库连接池

原理

  • acquire:通过permits来给state赋值
    • 竞争失败的线程进入AQS队列阻塞
  • release:释放permits
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值