1.线程池与AQS

1.1为什么要使用线程池:

1.减少每次资源的消耗,提高资源的利用率。限制和管理资源(包括执行),维护基本信息,例如已经完成的任务数量。

2.即:(降低资源消耗(创建、销毁消耗)、提高响应速度(不等创建、立即执行)、提高线程的可管理性(稀缺、无限制、降低稳定性、分配、调优和监控))

1.2 实现Runnable接口和Callable接口的区别

1.Callable 的出现是为了解决Runnable不支持的用例,Runnable接口不会返回结果和抛出异常,但是 Callable 接⼝可以。

2.⼯具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换
        ( Executors.callable(Runnable task );或 Executors.callable(Runnable task,Object resule);

3.Runnable的实现类可以通过构造方法直接传递给一个Thread,并执行;而Callable只能借助Future去执行并获取返回结果
Callable也是借助了同时实现了Runnable接口Future接口的FutureTask来达到目的的。FutureTask作为一个Runnable,被线程执行。而FutureTask的run()方法,实际上调用了其自身持有的Callable对象的call()方法,并将结果保存在内部变量中。

//Runnable
@FunctionalInterface
public interface Runnable(){
    public abstract void run();
}


//Callable
@FunctionalInterface
public interface Callable<V>(){

    v call() throws Exception;
}

1.3 执⾏ execute()⽅法和 submit()⽅法的区别是什么呢?

1.execute()方法提交不需要返回值的任务,不知道成功否;submit()方法则用于需要返回值的任务,他会返回一个Future类型的对象,可以通过它判断是否执行成功,可通过get()方法获取返回值,并阻塞直到线程执行完,如果是get(long timeout,Timeunit unit)就是阻塞一段时间立即返回,可能没执行完。

public Future<?> submit(Runnable task) {
 if (task == null) throw new NullPointerException();
 RunnableFuture<Void> ftask = newTaskFor(task, null);
 execute(ftask);
 return ftask;
 }

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
 return new FutureTask<T>(runnable, value);
 }

public void execute(Runnable command) {
 ...
 }

1.4 如何创建线程池

1.利用Executor创建线程池可能会导致资源耗尽,如OMM问题;因为如果用FixedThreadpool、SingleThreadpool、CacheThreadPool、ScheduledThreadPool允许队列长度为Integer.MAX_VALUE,堆积大量请求或创建大量线程。

2.通过ThreadPoolExecutor构造方法来实现;如ThreadPoolExecutor(int,int,。。。);

3.通过Executor框架的工具类Executors来实现;比如newFixedThreadPool(int);

FixedThreadPool : 该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

SingleThreadExecutor: ⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。

CachedThreadPool: 该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进⾏复⽤。

ScheduledThreadPool是一个能实现定时、周期性任务线程池,用于给定延时之后的运行任务或定期处理任务。

1.5ThreadPoolExecutor 类分析

//构造方法
public ThreadPoolExecutor(int corePoolSize,int maximumSize,long keepAlive,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler){

if (corePoolSize < 0 ||
 maximumPoolSize <= 0 ||
 maximumPoolSize < corePoolSize ||
 keepAliveTime < 0)
 throw new IllegalArgumentException();
 if (workQueue == null || threadFactory == null || handler == null)
 throw new NullPointerException();
 this.corePoolSize = corePoolSize;
 this.maximumPoolSize = maximumPoolSize;
 this.workQueue = workQueue;
 this.keepAliveTime = unit.toNanos(keepAliveTime);
 this.threadFactory = threadFactory;
 this.handler = handler;





}

1.6 ThreadPoolExecutor 构造函数重要参数分析

corePoolSize : 核⼼线程数线程数定义了最⼩可以同时运⾏的线程数量.

maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数量变为最⼤线程数。

workQueue : 当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数,如果达到的话,新任务就会被存放在队列中。

1. keepAliveTime :当线程池中的线程数量⼤于 corePoolSize 的时候,如果这时没有新的任务
提交,核⼼线程外的线程不会⽴即销毁,⽽是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁;
2. unit : keepAliveTime 参数的时间单位。
3. threadFactory :executor 创建新线程的时候会⽤到。
4. handler :饱和策略。

(任务进来判断当下线程数是否达到核心线程数,如果没达到,直接取线程运行,如果达到就放入到队列中,当队列满时,将当前线程数设为最大线程数,如果没用的话,达到keepAlive时,销毁)

1.7 ThreadPoolExecutor 饱和策略

如果当前同时运⾏的线程数量达到最⼤线程数量并且队列也已经被放满了时.

(默认)ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任务的处理。
(建议)ThreadPoolExecutor.CallerRunsPolicy调⽤执⾏⾃⼰的线程运⾏任务。不会拒绝请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应⽤程序可以承受此延迟并且你不能任务丢弃任何⼀个任务请求的话,你可以选择这个策略。(可伸缩应用程序和队列)
ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉
ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。

1.8 线程池原理分析

线程池每次会同时执⾏ 5 个任务,这 5 个任务执⾏完之后,剩余的 5 个任务才会被执⾏。

executor.execute();

// 存放线程池的运⾏状态 (runState) 和线程池内有效线程的数量 (workerCount)
 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
 
 private static int workerCountOf(int c) {
 return c & CAPACITY;
 }
 
 private final BlockingQueue<Runnable> workQueue;
 
 public void execute(Runnable command) {
 // 如果任务为null,则抛出异常。
 if (command == null)
 throw new NullPointerException();
 // ctl 中保存的线程池当前的⼀些状态信息
 int c = ctl.get();
 
 // 下⾯会涉及到 3 步 操作
 // 1.⾸先判断当前线程池中之⾏的任务数量是否⼩于 corePoolSize
 // 如果⼩于的话,通过addWorker(command, true)新建⼀个线程,并将任务(command)
添加到该线程中;然后,启动该线程从⽽执⾏任务。
 if (workerCountOf(c) < corePoolSize) {
 if (addWorker(command, true))
 return;
 c = ctl.get();
 }
 // 2.如果当前之⾏的任务数量⼤于等于 corePoolSize 的时候就会⾛到这⾥
通过下图可以更好的对上⾯这 3 步做⼀个展示,下图是我为了省事直接从⽹上找到,原地址不
明。
现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?
没搞懂的话,也没关系,可以看看我的分析:
 // 通过 isRunning ⽅法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以
加⼊任务,该任务才会被加⼊进去
 if (isRunning(c) && workQueue.offer(command)) {
 int recheck = ctl.get();
 // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除
任务,并尝试判断线程是否全部执⾏完毕。同时执⾏拒绝策略。
 if (!isRunning(recheck) && remove(command))
 reject(command);
 // 如果当前线程池为空就新创建⼀个线程并执⾏。
 else if (workerCountOf(recheck) == 0)
 addWorker(null, false);
 }
 //3. 通过addWorker(command, false)新建⼀个线程,并将任务(command)添加到该线
程中;然后,启动该线程从⽽执⾏任务。
 //如果addWorker(command, false)执⾏失败,则通过reject()执⾏相应的拒绝策略的内
容。
 else if (!addWorker(command, false))
 reject(command);
 }

 1.9 Atomic原子类

执行时不可分割,不可中断;

基本类型:

AtomicInteger :整形原⼦类
AtomicLong :⻓整型原⼦类
AtomicBoolean :布尔型原⼦类

数组类型
使⽤原⼦的⽅式更新数组⾥的某个元素
AtomicIntegerArray :整形数组原⼦类
AtomicLongArray :⻓整形数组原⼦类
AtomicReferenceArray :引⽤类型数组原⼦类
引⽤类型
AtomicReference :引⽤类型原⼦类
AtomicStampedReference :原⼦更新带有版本号的引⽤类型。该类将整数值与引⽤关联起
来,可⽤于解决原⼦的更新数据和数据的版本号,可以解决使⽤ CAS 进⾏原⼦更新时可能
出现的 ABA 问题。
AtomicMarkableReference :原⼦更新带有标记位的引⽤类型
对象的属性修改类型
AtomicIntegerFieldUpdater :原⼦更新整形字段的更新器
AtomicLongFieldUpdater :原⼦更新⻓整形字段的更新器
AtomicReferenceFieldUpdater :原⼦更新引⽤类型字段的更新器

1.10AQS 的全称为( AbstractQueuedSynchronizer )

AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器,⽐如我们提到的 ReentrantLock , Semaphore ,其他的诸如 ReentrantReadWriteLock , SynchronousQueue , FutureTask 等等皆是基于 AQS 的。

原理:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒,时锁分配的机制,这个机制 AQS 是⽤ CLH 队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。

CLH(Craig,Landin,and Hagersten)队列是⼀个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成⼀个 CLH 锁队列的⼀个结点(Node)来实现锁的分配。

 AQS工作机制:AQS 使⽤⼀个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队⼯作。AQS 使⽤ CAS 对该同步状态进⾏原⼦操作实现对其值的修改。状态信息通过 protected 类型的 getState,setState,compareAndSetState (compareAndSwapInt)进⾏操作。

1.11. AQS 对资源的共享⽅式 


两种:
        Exclusive(独占):只有⼀个线程能执⾏,如 ReentrantLock 。⼜可分为公平锁⾮公平锁
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
⾮公平锁:当线程要获取锁时,⽆视队列顺序直接去抢锁,谁抢到就是谁的
        Share(共享):多个线程可同时执⾏,如CountDownLatch 、 Semaphore 、 CyclicBarrier 、 ReadWriteLock 
ReentrantReadWriteLock
可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某⼀资源进⾏读。

不同的⾃定义同步器争⽤共享资源的⽅式也不同。⾃定义同步器在实现时只需要实现共享资源 state 的获取与释放⽅式即可,⾄于具体线程等待队列的维护(如获取资源失败⼊队/唤醒出队等),AQS 已经在顶层实现好了。

1.12AQS 底层使⽤了模板⽅法模式 


同步器的设计是基于模板⽅法模式的,⾃定义同步器采用模板⽅法模式
1. 使⽤者继承 AbstractQueuedSynchronizer 并重写指定的⽅法。(这些重写⽅法很简单,⽆⾮是对于共享资源 state 的获取和释放)
2. 将 AQS 组合在⾃定义同步组件的实现中,并调⽤其模板⽅法,⽽这些模板⽅法会调⽤使⽤者重写的⽅法。
模板方法与接口实现有很大的区别:
重写方法(其中一种就行):

isHeldExclusively()//该线程是否正在独占资源。只有⽤到condition才需要去实现它。
tryAcquire(int)//独占⽅式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占⽅式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享⽅式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可⽤资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享⽅式。尝试释放资源,成功则返回true,失败则返回false。

每个⽅法都抛出 UnsupportedOperationException内部线程安全的,不是阻塞。AQS 类中的其他⽅法都是 final ,所以⽆法被其他类使⽤,只有这⼏个⽅法可以被其他类使⽤。


以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调⽤ tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0’(即释放锁)为⽌,其它线程才有机会获取该锁。当然,释放锁之前,A 线程⾃⼰是可以重复获取此锁的(state 会累加),这就是可重⼊的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。

再以 CountDownLatch 以例,任务分为 N 个⼦线程去执⾏,state 也初始化为 N(注意 N 要与线程个数⼀致)。这 N 个⼦线程是并⾏执⾏的,每个⼦线程执⾏完后 countDown() ⼀次,state 会 CAS(Compare and Swap)减 1。等到所有⼦线程都执⾏完后(即 state=0),会 unpark()主调⽤线程,然后主调⽤线程就会从 await() 函数返回,继续后余动作。


⼀般来说,⾃定义同步器要么是独占⽅法,要么是共享⽅式,他们也只需实现 tryAcquiretryRelease、 tryAcquireShared-tryReleaseShared 中的⼀种即可。但 AQS 也⽀持⾃定义同步器同时实现独占和共享两种⽅式,如 ReentrantReadWriteLock 。

(26条消息) 一个简单排他锁的原理与实现_丶幻一的博客-CSDN博客_排他锁的实现

1.13AQS 组件总结 


Semaphore (信号量):允许多个线程同时访问某个资源, synchronized 和 ReentrantLock 都是⼀次只允许⼀个线程访问某个资源。
CountDownLatch (倒计时器): CountDownLatch 是⼀个同步⼯具类,⽤来协调多个线程之间的同步。控制线程等待,让某⼀个线程等待直到倒计时结束,再开始执⾏。
CyclicBarrier (循环栅栏): CyclicBarrier 和 CountDownLatch ⾮常类似,它也可以实现线程间的技术等待,更加复杂和强⼤。主要应⽤场景和 CountDownLatch 类似。 是可循环使⽤( Cyclic )的屏障( Barrier )。它要做的事情是,让⼀组线程到达⼀个屏障(也可以叫同步点)时被阻塞,直到最后⼀个线程到达屏障时,屏障才会开⻔,所有被屏障拦截的线程才会继续⼲活。 CyclicBarrier 默认的构造⽅法是 CyclicBarrier(int parties) ,其参数表示屏障拦截的线程数量,每个线程调⽤ await() ⽅法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

1.14 ⽤过 CountDownLatch 么?什么场景下⽤的?

CountDownLatch 的作⽤就是允许 count 个线程阻塞在⼀个地⽅,直⾄所有线程的任务都执⾏完毕。

之前在项⽬中,有⼀个使⽤多线程读取多个⽂件处理的场景,我⽤到了 CountDownLatch

我们要读取处理 6 个⽂件,这 6 个任务都是没有执⾏顺序依赖的任务,但是我们需要返回给⽤户的时候将这⼏个⽂件的处理的结果进⾏统计整理。为此我们定义了⼀个线程池和 count 为 6 的 CountDownLatch 对象 。使⽤线程池处理读取任务,每⼀个线程处理完之后就将 count-1,调⽤ CountDownLatch 对象的 await() ⽅法,直到所有⽂件读取完之后,才会接着执⾏后⾯的逻辑。

public class CountDownLatchExample1 {
 // 处理⽂件的数量
 private static final int threadCount = 6;
 
 public static void main(String[] args) throws InterruptedException {
 // 创建⼀个具有固定线程数量的线程池对象(推荐使⽤构造⽅法创建)
 ExecutorService threadPool = Executors.newFixedThreadPool(10);
 final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
 for (int i = 0; i < threadCount; i++) {
 final int threadnum = i;
 threadPool.execute(() -> {
 try {
 //处理⽂件的业务操作
 ......
 } catch (InterruptedException e) {
 e.printStackTrace();
 } finally {
 //表示⼀个⽂件已经被完成
 countDownLatch.countDown();
 }
 
 });
 }
 countDownLatch.await();
 threadPool.shutdown();
 System.out.println("finish");
 }

可以使⽤ CompletableFuture 类来改进!Java8 的 CompletableFuture 提供了很多对多线程友好的⽅法,使⽤它可以很⽅便地为我们编写多线程程序,什么异步、串⾏、并⾏或者等待所有线程执⾏完任务什么的都⾮常⽅便。

CompletableFuture<Void> task1 =CompletableFuture.supplyAsync(()->{
 //⾃定义业务操作
 });
......
CompletableFuture<Void> task6 =
 CompletableFuture.supplyAsync(()->{
 //⾃定义业务操作
 });
......
 CompletableFuture<Void> 
headerFuture=CompletableFuture.allOf(task1,.....,task6);
 
 try {
 headerFuture.join();
 } catch (Exception ex) {
 ......
 }
System.out.println("all done. ");

上⾯的代码还可以接续优化,当任务过多的时候,把每⼀个 task 都列出来不太现实,可以考虑通过循环来添加任务。

//⽂件夹位置
List<String> filePaths = Arrays.asList(...)
// 异步处理所有⽂件
List<CompletableFuture<String>> fileFutures = filePaths.stream()
 .map(filePath -> doSomeThing(filePath))
 .collect(Collectors.toList());
// 将他们合并起来
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
 fileFutures.toArray(new CompletableFuture[fileFutures.size()])

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值