ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【无标题】

2022-01-11 18:34:27  阅读:114  来源: 互联网

标签:队列 无标题 CPU 获取 任务 线程 节点


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述![在center)在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QzgXKS5Z-1641896374379)(image-20211229020812623.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J2saTMk-1641896374380)(image-20211203125358312.png)]

守护线程是什么?

守护线程是程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收之类的清理工作。但是因为所有用户线程停止,进程会停掉所有守护线程,退出程序,所以不会用它来访问写入资源,只会进行垃圾回收之类清理工作,不然因为垃圾回收线程还在执行整个程序关不掉,所以守护线程主要作用就是其他线程结束了这个线程会自动结束的一种线程。

  • 线程池是否区分核心线程和非核心线程?

  • 如果主动开启allowCoreThreadTimeOut,或者获取当前工作线程大于corePoolSize,那么该线程是可以被超时回收的,一般默认是关闭,核心线程不会被回收。但是这个核心线程也只是指的数量,具体回收的时候如果get task==null就会回收线程

  • 所以保证线程不被销毁的关键代码就是这一句代码

  • 获取线程池中的有效线程数量
      ``int` `wc = workerCountOf(c);
    
     ``// 如果主动开启allowCoreThreadTimeOut,或者获取当前工作线程大于corePoolSize,那么该线程是可以被超时回收的
      ``// allowCoreThreadTimeOut默认为false,即默认不允许核心线程超时回收,为true则代表可以回收核心线程
      ``// 这里也说明了在核心线程以外的线程都为“临时”线程,随时会被线程池回收
      ``boolean` `timed = allowCoreThreadTimeOut || wc > corePoolSize;
    
  • 这句话含义就是核心线程数不允许回收,并且最大线程大于核心线程就阻塞,小于就回收

  • 核心线程要是允许被回收就不管什么最大线程了,直接回收

  • Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
    

    使用了CAS算法避免多个线程同时get null销毁比预期多的线程

  • 如何保证核心线程不被销毁?

  • // 如果主动开启allowCoreThreadTimeOut,或者获取当前工作线程大于corePoolSize,那么该线程是可以被超时回收的// allowCoreThreadTimeOut默认为false,即默认不允许核心线程超时回收。因为它有个timed变量逻辑或条件判断,这个变量为true就会回收线程,为false就阻塞线程,核心线程为false,就只会回收最大线程当线程数量等于核心线程时候就阻塞,如果核心线程为true,这个条件就一直成立,就一直回收线程。

  • 线程池回收线程原理?

    getTask() 返回null

    在核心线程参数为false也就是核心线程不会销毁情况下

    分两种场景。

    1、未调用shutdown() ,RUNNING状态下全部任务执行完成的场景

    线程数量大于corePoolSize,线程此时没有任务就超时阻塞等待任务来临,此时就把超时阻塞的线程唤醒后CAS减少工作线程数,如果CAS成功,返回null,源码是通过getTask()返回null,就会被回收线程。否则进入下一次循环。当工作者线程数量小于等于corePoolSize,就可以一直阻塞了。

    2、调用shutdown()

    shutdown状态不会接受新任务,只会处理阻塞队列里面的任务

    如果阻塞队列任务执行完了,shutdown() 会向所有线程发出中断信号,这时有两种可能。

    2.1)所有线程都在阻塞,全部任务执行完成的场景

    中断唤醒,进入循环,都符合第一个if判断条件,都返回null,所有线程回收。

    2.2)任务还没有完全执行完

    至少会有一条线程被回收。在processWorkerExit(Worker w, boolean completedAbruptly)方法里会调用tryTerminate(),向任意空闲线程发出中断信号。所有被阻塞的线程,最终都会被一个个唤醒,回收。

    第一种情况,线程池的状态已经是STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作队列为空;

    第二种情况,工作线程数已经大于最大线程数或当前工作线程已超时且队列为空

知道线程池中线程复用原理吗?

实现线程复用的逻辑主要在一个不停循环的 while 循环体中。

  • 通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务。直到获取任务为空就跳出循环

  • 直接调用 task 的 run 方法来执行具体的任务(而不是新建线程)。

  • runWorker(Worker w) {
        Runnable task = w.firstTask;
        while (task != null || (task = getTask()) != null) {
         w.lock();
            try {
                task.run();
            } finally {
                task = null;
            }
        }
    }
    

runworker:

runwork方法中会优先取worker绑定的任务,如果创建这个worker的时候没有给worker绑定任务,worker就会从队列里面获取任务来执行,执行完之后worker并不会销毁,而是通过while循环不停的执行getTask方法从阻塞队列中获取任务调用task.run()来执行任务,这样的话就达到了线程复用的目的。 while (task != null || (task = getTask()) != null) 这个循环条件只要getTask 返回获取的值不为空这个循环就不会终止, 这样线程也就会一直在运行。

在这里插入图片描述

首先拿到ctl参数,判断是否小于核心线程数,第二步判断线程池是否running状态,再增加worker

Worker是Runnbale接口的实现类,同时实现了AQS接口

线程池的参数说一下

corePoolSize: 线程池核心线程数最大值

maximumPoolSize: 线程池最大线程数大小

keepAliveTime: 线程池中非核心线程空闲的存活时间大小

unit: 线程空闲存活时间单位

workQueue: 存放任务的阻塞队列

threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。

handler: 线城池的饱和策略事件,主要有四种类型。

https://juejin.cn/post/7036157909390065671#heading-2 线程池如何回收的

在这里插入图片描述

提交优先级
核心线程 > 工作队列 > 非核心线程 具体就是对应的任务,核心线程的任务先执行,然后执行工作队列的
执行优先级
核心线程 > 非核心线程 > 工作队列

四大策略

i. 直接抛出异常(默认) 如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。

ii. 使用调用者所在线程执行任务,调用者出现异常

iii. 丢弃队列中最靠前的任务,并执行当前任务,还得根据实际业务是否允许丢弃老任务来认真衡量。

iv. 直接丢弃当前任务,使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。

· ****ArrayBlockingQueue :****一个由数组结构组成的有界阻塞队列。

· ****LinkedBlockingQueue :****一个由链表结构组成的有界阻塞队列。

· ****PriorityBlockingQueue :****一个支持优先级排序的无界阻塞队列。底层实现了comparable接口的二叉堆

· execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。

· execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。

img

说说几种常见的线程池及使用场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Dn4y6BD-1641896374382)(image-20211203190616263.png)]

  • newFixedThreadPool(固定大小的线程池)

  • 线程池特点:

    • 核心线程数和最大线程数大小一样
    • 没有所谓的非空闲时间,即keepAliveTime为0,一旦有多余线程马上停掉
    • 阻塞队列为无界队列LinkedBlockingQueue
    • 由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;
    • 因为fixed和single都是LinkedBlockingQueue,由链表结构组成的有界阻塞队列。
  • newSingleThreadExecutor(单线程线程池)

  • 它只会创建一条工作线程处理任务;

  • 采用的阻塞队列为LinkedBlockingQueue;

  • newCachedThreadPool(可缓存线程的线程池)

    • SynchronousQueue(同步队列): 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
  • CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。

  • 执行流程:

    1.没有核心线程,直接向 SynchronousQueue 中提交任务

    2.如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个

  • 它是一个可以无限扩大的线程池;

  • corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;

  • 采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。

  • SynchronousQueue 作为无界阻塞队列,阻塞意味着阻塞put和take操作

  • CachedThreadPool 使用没有容量的 SynchronousQueue 作为阻塞队列;意味着,如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。

  • 单线程和多线程是耗尽内存

  • newScheduledThreadPool

  • 就是用数组实现了一个堆对时间排序,从而实现了延时执行任务,时间靠后的后面执行,这个数组会扩容所以是无界的

  • 运行的线程数没有达到corePoolSize的时候,就新建线程去DelayedWorkQueue中取ScheduledFutureTask然后才去执行任务,

  • corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;

  • 阻塞队列是DelayedWorkQueue

  • DelayQueue:中封装了一个优先级队列底层就是堆对时间排序,这个队列会对队列中的ScheduledFutureTask 进行排序,两个任务的执行 time 不同时,time 小的先执行;否则比较添加到队列中的ScheduledFutureTask的顺序号 sequenceNumber ,先提交的先执行。

  • ,DelayedWorkQueue会将任务排序,按新建一个非核心线程顺序执行,执行完线程就回收,然后循环。任务队列采用的DelayedWorkQueue是个无界的队列,延时执行队列任务。

ForkJoinPool

jdk7加入,它非常适合执行可以产生子任务的任务。

ForkJoinPool 线程池内部除了有一个共用的任务队列之外,每个线程还有一个对应的双端队列 deque,这时一旦线程中的任务被 Fork 分裂了,分裂出来的子任务放入线程自己的 deque 里,而不是放入公共的任务队列中。如果此时有三个子任务放入线程 t1 的 deque 队列中,对于线程 t1 而言获取任务的成本就降低了,可以直接在自己的任务队列中获取而不必去公共队列中争抢也不会发生阻塞(除了后面会讲到的 steal 情况外),减少了线程间的竞争和切换,是非常高效的。如果某一个线程队列职责很重还可以stealing帮忙分担

比如说主任务需要执行非常繁重的计算任务,我们就可以把计算拆分成三个部分,这三个部分是互不影响相互独立的,这样就可以利用 CPU 的多核优势,并行计算,然后将结果进行汇总。这里面主要涉及两个步骤,第一步是拆分也就是 Fork,第二步是汇总也就是 Join

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回的线程池对象弊端如下:

  • FixedThreadPool 和 SingleThreadPool:因为使用的LinkedBlolkingQueue请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
  • CacheThreadPool 和 ScheduledThreadPool:允许创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致 CPU百分之百

使用场景:

newFixedThreadPool : 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能给每个线程时间片分配久一点,即适用执行长期的任务。因为它是固定的可以避免CPU上下文切换带来的时间浪费,适合CPU密集的。

newCachedThreadPool: 用于并发执行大量短期的小任务。因为它可以无限创建线程可以可以执行大量任务,同时也会按时清空不用的,所以为了避免线程的堆积过多适合清除小任务。

newSingleThreadExecutor: 适用于串行执行任务的场景,一个任务一个任务地执行。因为它是单线程的。

newScheduledThreadPool :周期性执行任务的场景,需要限制线程数量的场景,因为它底层是二叉堆实现的对时间排序,综合来说,这类线程池适用于执行定时任务和具体固定周期的重复任务。

各种队列底层

当线程池中的核心线程数已满时,任务就要保存到队列中了。

线程池中使用的队列是 BlockingQueue 接口,常用的实现有如下几种:

ArrayBlockingQueue: 底层采用数组实现的有界队列,初始化需要指定队列的容量。ArrayBlockingQueue 是如何保证线程安全的呢?它内部是使用了一个重入锁 ReentrantLock,并搭配 notEmpty、notFull 两个条件变量 Condition 来控制并发访问。从队列读取数据时,如果队列为空,那就进入notempty条件队列去等待,直到队列有数据了才用signal唤醒。如果队列已经满了,也同样会进入notfull条件队列,直到队列有空闲才会用signal唤醒。

LinkedBlockingQueue:内部采用的数据结构是链表,队列的长度可以是有界或者无界的,初始化不需要指定队列长度,默认是 Integer.MAX_VALUE。LinkedBlockingQueue 内部使用了 takeLock、putLock两个重入锁 ReentrantLock,以及 notEmpty、notFull 两个条件变量 Condition 来控制并发访问。采用读锁和写锁的好处是可以避免读写时相互竞争锁的现象,所以相比于 ArrayBlockingQueue,LinkedBlockingQueue 的性能要更好。Executors.newFixedThreadPool() 使用了这个队列

  • PriorityBlockingQueue:采用最小堆实现的优先级队列,队列中的元素按照优先级进行排列,每次出队都是返回优先级最高的元素。PriorityBlockingQueue 内部是使用了一个 ReentrantLock 以及一个条件变量 Condition notEmpty 来控制并发访问,不需要 notFull 是因为 PriorityBlockingQueue 是无界队列,所以每次 put 都不会发生阻塞。PriorityBlockingQueue 底层的最小堆是采用数组实现的,当元素个数大于等于最大容量时会触发扩容,在扩容时会先释放锁,保证其他元素可以正常出队,然后使用 CAS 操作确保只有一个线程可以执行扩容逻辑。

  • SynchronizedQueue,又称无缓冲队列。比较特别的是 SynchronizedQueue 内部不会存储元素。与 ArrayBlockingQueue、LinkedBlockingQueue 不同,SynchronizedQueue 直接使用 CAS 操作控制线程的安全访问。其中 put 和 take 操作都是阻塞的,每一个 put 操作都必须阻塞等待一个 take 操作,反之亦然。所以 SynchronizedQueue 可以理解为生产者和消费者配对的场景,双方必须互相等待,直至配对成功。在 JDK 的线程池 Executors.newCachedThreadPool 中就存在 SynchronousQueue 的运用,对于新提交的任务,如果有空闲线程,将重复利用空闲线程处理任务,否则将新建线程进行处理。

  • DelayQueue,一种支持延迟获取元素的阻塞队列,常用于缓存、定时任务调度等场景。DelayQueue 内部是采用优先级队列 PriorityQueue 存储对象。DelayQueue 中的每个对象都必须实现 Delayed 接口,并重写 compareTo 和 getDelay 方法。向队列中存放元素的时候必须指定延迟时间,只有延迟时间已满的元素才能从队列中取出。

吞吐量 同步队列大于linked大于array

面试题:使用无界队列的线程池会导致内存飙升吗?

答案 :会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM。

Executors中FixedThreadPool使用的是LinkedBlockingQueue队列,近乎于无界,队列大小默认为Integer.MAX_VALUE,几乎可以无限制的放任务到队列中,线程池中数量是固定的,当线程池中线程数量达到corePoolSize,不会再创建新的线程,所有任务都会入队到workQueue中,线程从workQueue中获取任务,,此时,如果线程池中的线程处理任务的时间特别长,导致无法处理新的任务,队列中的任务就会不断的积压,这个过程,会导致机器的内存使用不停的飙升,极端情况下会导致JVM OOM,系统就挂了。

总结:Executors中FixedThreadPool指定使用无界队列LinkedBlockingQueue会导致内存溢出,所以,最好使用ThreadPoolExecutor自定义线程池

CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。 CPU百分之百,他们同样会爆出outofmemory内存溢出,但是在这之前他们的线程数因为要处理任务肯定已经到达极限了,所以会先爆出CPU百分之百

ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。CPU百分之百

面试题7:线程池为什么要使用阻塞队列而不使用非阻塞队列?

当阻塞队列为空的时候,从队列中取元素的操作就会被阻塞。当阻塞队列满的时候,往队列中放入元素的操作就会被阻塞。

而后,一旦空队列有数据了,或者满队列有空余位置时,被阻塞的线程就会被自动唤醒。

这就是阻塞队列的好处,你不需要关心线程何时被阻塞,也不需要关心线程何时被唤醒,一切都由阻塞队列自动帮我们完成。我们只需要关注具体的业务逻辑就可以了。

一般使用阻塞队列在任务数不多但资源占用大,非社交流媒体的使用场景下,该情况多发生于文件流、长文本对象或批量数据加工的处理,如日志收集、图片流压缩或批量订单处理等场景

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。

使得在线程不至于一直占用cpu资源。

线程池里面的锁?

线程池里面有个叫做 workers 的变量,它存放的东西,可以理解为线程池里面的线程。

而这个对象的数据结构是 HashSet。

HashSet 不是一个线程安全的集合类,用一个 ReentrantLock 来保护一个 HashSet,

什么不直接搞一个线程安全的 Set 集合,比如用这个玩意 Collections.synchronizedSet?

当我们调用shutdownnow方法里面进来第一件事就是拿 ReentrantLock 锁,然后尝试去做中断线程的操作,

因为shutdownnow会直接让线程池进入stop状态,不处理新来的任务也不处理队列中的,还要中断正在执行的线程

因为有 ReentrantLock 锁串行化起来的好处是什么呢?避免了多个线程同时发起中断,一个线程已经被中断然后另一个线程还要中断他,引起了中断风暴,所以这里主要是为了避免并发访问。这里注意正在执行的线程不会中断,线程池会通过CAS不断查看当前线程是否已经执行完任务,执行完了才中断这个空闲线程

面试题5:线程池中核心线程数量大小怎么设置?
**「CPU密集型任务」:**比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

**「IO密集型任务」:**比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

另外:线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;

以上只是理论值,实际项目中建议在本地或者测试环境进行多次调优,找到相对理想的值大小。

因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

面试题3:shutdownNow() 和 shutdown() 两个方法有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

线程池几种状态?

  • RUNNING:能接受新任务,并处理阻塞队列中的任务
  • SHUTDOWN:不接受新任务,但是可以处理阻塞队列中的任务
  • STOP:不接受新任务,并且不处理阻塞队列中的任务,并且还打断正在运行任务的线程
  • TIDYING:所有任务都终止,并且工作线程也为0,处于关闭之前的状态
  • TERMINATED:已关闭。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-orfoBbuq-1641896374383)(image-20211203142557942.png)]

一般配置线程数=CPU总核心数 * 2 +1

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

一般说来,大家认为线程池的大小经验值应该这样设置:(其中N为CPU的个数)

  • 如果是CPU密集型应用,则线程池大小设置为N+1
  • 如果是IO密集型应用,则线程池大小设置为2N+1

如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。

但是,IO优化中,这样的估算公式可能更适合:

最佳线程数目 = ((线程等待时间+线程执行时间)/线程执行时间 )* CPU数目

因为很显然,线程等待时间当分子所占比例越高,需要越多线程。让CPU运行时间当分母线程CPU时间所占比例越高,需要越少线程。

下面举个例子:

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程执行时间之比 + 1)* CPU数目

QPS = 并发线程数 * ( 1000 / 平均耗时ms )

动态调参数

修改线程数

1.还有一种方式就是学netty一个BOSS group线程池一个work group线程池,分别处理不同类型任务,可以多定义几个线程池来处理来针对不同场景用不同的线程池

2.修改线程池参数成本降下来实现动态修改,括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效,原本修改完了还需要重新发布检查运行状况之类的,现在改成直接用可视化节面动态修改

3.添加任务监控和负载警告,当任务积压和线程数创建到某一个阈值的时候就进行警告开发负责人

4.提供操作日志可以查看修改线程池参数的日志记录

5.权限校验:只有开发负责人有权限修改

核心线程可以直接setCorePoolSize为方法覆盖原有的corepoolsize

如何动态指定队列长度?

按照这个思路自定义一个队列,让其可以对 Capacity 参数进行修改即可。

操作起来也非常方便,把 LinkedBlockingQueue 粘贴一份出来,修改个名字,然后把 Capacity 参数的 final 修饰符去掉,并提供其对应的 get/set 方法。

核心线程数会被回收吗?需要什么设置?

allowCoreThreadTimeOut 该值默认为 false不会回收,要回收设置为true

任务数不多但资源占用大,非社交流媒体的使用场景下,该情况多发生于文件流、长文本对象或批量数据加工的处理,如日志收集、图片流压缩或批量订单处理等场景,而此类场景下的单个资源处理,往往会发生较大的资源消耗,因此为使系统达到较强处理能力,同时又可以控制任务资源对内存过大的使用,通常可以在创建线程池时适当加大扩展线程数量,当遇到任务数突增的情况,可以有更多的并发线程来应对 ,同时设置相对较小的任务队列长度避免cpu切换,如此,,此外需要合理设置扩展线程空闲回收的等待时长以节省不必要的开销

任务数不多但资源占用大,通常可以在创建线程池时适当加大扩展线程数量,当遇到任务数突增的情况,可以有更多的并发线程来应对 ,同时设置相对较小的任务队列长度避免cpu切换,设置扩展线程空闲回收的等待时长以节省不必要的

(任务数多但资源占用不大,电商平台的消息推送或短信通知,该场景需要被处理的消息对象内容简单占用资源非常少,通常为百字节量级,但在高并发访问下,可能瞬间产生大量的任务数,而此类任务的处理通常效率非常高,因此处理的重点在于控制并发线程数,不要因为大量的线程启用及线程的上下文频繁切换而导致内存使用率过高,CPU的内核态使用率过高等不良情况发生,通常可以在创建线程池时设置较长的任务队列,并以CPU内核数2-4倍(经验值)的值设置核心线程与扩展线程数,合理固定的线程数使得CPU的使用率更加平滑,如: 使用类型可以参考new cache可缓存线程池无限个线程,任务处理完了线程很快因缓存设置时间失效,同时使用无界队列,进来一个任务没有线程处理就创建一个线程去处理

任务数多但资源占用不大,通常可以在创建线程池时设置较长的任务队列,避免因为大量的线程启用及线程的上下文频繁切换而导致内存使用率过高,并以CPU内核数设置核心线程与扩展线程数

Execute和Submit?

submit() 有三种重载,参数可以是 Callable 也可以是 Runnable。

两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

execute() 的参数是一个 Runnable,也没有返回值。因此提交后无法判断该任务是否被线程池执行成功。

任务申请方式:1.直接被新创建得线程获取 2.被已经创建的线程从任务队列里面获取

线程池的异常处理

img

线程池生命周期

生命周期管理

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起,如下代码所示:

ctl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

img

img

线程池在业务中的实践

3.1 业务背景

在当今的互联网业界,为了最大程度利用CPU的多核性能,并行运算的能力是不可或缺的。通过线程池管理线程获取并发性是一个非常基础的操作,让我们来看两个典型的使用线程池获取并发性的场景。

场景1:快速响应用户请求

描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

场景2:快速处理批量任务

这时候需要考虑固定线程数来处理,至于固定多少看看上下文切换带来的损失和线程处理的吞吐量求一个平衡值,。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

动态化线程池提供如下功能:

查看当前任务哪种类型,IO密集还是CPU密集,然后看看队列状态和CPU状态,使用无界队列导致任务积压过多,或者创建的线程是不是过多导致CPU飙升

  • 动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效。
  • 任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。
  • 负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。
  • 操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。
  • 除了参数动态化之外,为了更好地使用线程池,我们需要对线程池的运行状况有感知,比如当前线程池的负载是怎么样的?分配的资源够不够用?任务的执行情况是怎么样的?是长任务还是短任务?

AtomicInteger

unsafe类具有访问

  • valueOffset: 存储value在AtomicInteger中的偏移量。
  • value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。
  • AtomicReference来保证原子性,AtomicReference 主要是依赖于 sun.misc.Unsafe 提供的一些 native 方法保证操作的原子性,unsafe类可以获取成员属性在操作系统内存地址的偏移量,因此getset操作都是原子性的对内存地址直接进行操作,并且用volatile修饰保证了getset操作的可见性,获取了内存地址以后可以基于CAS进行交换。AtomicInteger 是对整数的封装。
  • 整个比较并交换的过程是原子性的机器指令

在并发的场景下,如果我们需要实现计数器。

本质也是cas,但是CAS缺点就是并发量太大时候容易CAS失败导致还没有加锁串行化执行效率高

atomicInteger.get(); //获取当前值
atomicInteger.set(999); //设置当前值

LongAdder 带来的改进和原理

因为 LongAdder 引入了分段累加的概念,内部一共有两个参数参与计数:第一个叫作 base,它是一个变量,第二个是 Cell[] ,是一个数组。竞争激烈的时候,LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系。本质是空间换时间,因为它有多个计数器同时在工作,所以占用的内存也要相对更大一些。LongAdder 只提供了 add、increment 等简单的方法,适合的是统计求和计数的场景,场景比较单一,而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要 CAS 的场景。

Java 中的原子操作有哪些

在了解了原子操作的特性之后,让我们来看一下 Java 中有哪些操作是具备原子性的。Java 中的以下几种操作是具备原子性的,属于原子操作:

  • 除了 long 和 double 之外的基本类型(int、byte、boolean、short、char、float)的读/写操作,都天然的具备原子性;
  • 所有引用 reference 的读/写操作;
  • 加了 volatile 后,所有变量的读/写操作(包含 long 和 double)。这也就意味着 long 和 double 加了 volatile 关键字之后,对它们的读写操作同样具备原子性;
  • 在 java.concurrent.Atomic 包中的一部分类的一部分方法是具备原子性的,比如 AtomicInteger 的 incrementAndGet 方法。

Long 和 double 的值需要占用 64 位的内存空间,而对于 64 位值的写入,可以分为两个 32 位的操作来进行。

这样一来,本来是一个整体的赋值操作,就可能被拆分为低 32 位和高 32 位的两个操作。如果在这两个操作之间发生了其他线程对这个值的读操作,就可能会读到一个错误、不完整的值.,由于规范规定了对于 volatile long 和 volatile double 而言,JVM 必须保证其读写操作的原子性,所以加了 volatile 之后,对于程序员而言,就可以确保程序正确。

什么是JMM

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4VLu0vnY-1641896374385)(image-20211209211251154.png)]

JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以**java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。**

Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。

温馨提醒一下,这里有些人会把Java内存模型误解为Java内存结构,然后答到堆,栈,GC垃圾回收,最后和面试官想问的问题相差甚远。实际上一般问到Java内存模型都是想问多线程,Java并发相关的问题

JMM只能保证基本的原子性,如果要保证一个代码块的原子性,提供了monitorenter 和 moniterexit 两个字节码指令,也就是 synchronized 关键字。因此在 synchronized 块之间的操作都是原子性的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tuc8XLIP-1641896374386)(image-20211209211357959.png)]

Volatile

内存屏障主要靠底层加了一个汇编指令,相当于一个内存屏障,屏障后的数据不能比屏障之前的先执行

场景:单例模式双重检查锁禁止重排序,boolean值得变化通知做一个状态量改变得标记,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了

对基本类型的变量进行直接赋值时而不是多个那种++操作,如果加了 volatile 就可以保证它的线程安全。因为这时volatile可以利用自己对并发环境下共享变量可见性这种特性读到最新值,所以直接赋值是线程安全的。底层通过cpu总线嗅探机制实现

使用volatile修饰的共享变量,总线会开启MESI缓存一致性协议以及CPU总线嗅探机制来解决JMM缓存一致性问题

并发基础理论:缓存可见性、MESI协议、内存屏障、JMM - 知乎 (zhihu.com)

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WejYHiRv-1641896374386)(image-20211209211509547.png)]
  • volatile 仅能使用在变量级别;synchronized 则可以使用在 变量. 方法. 和类级别的

3 volatile只保证变量可见性,可以实现轻量级同步,不保证原子性,sychronized可以保证原子性

4 volatile 可以防止指定重排序,sychronized不行

5 因为不用加锁释放锁所以不会造成线程堵塞,所以只是轻量级的同步。但也是低成本的同步,Synchronized会阻塞线程

  • LoadLoad 屏障:对于这样的语句Load1,LoadLoad,Load2。在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1, StoreStore, Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

Store Barrier(写屏障)

强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,并把store缓冲区的数据都刷到CPU缓存。

结合上面的场景,这个指令其实就是告诉CPU,执行这个指令的时候需要把store buffer的数据都同步到内存中去。

就是全部写完而且对其他CPU可见

Load Barrier(读屏障)

强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行,并且一直等到load缓冲区被该CPU读完才能执行之后的load指令。

这个指令的意思是,在读取共享变量的指令前,先处理所有在失效队列中的消息,这样就保证了在读取数据之前所有失效的消息都得到了执行,从而保证自己是读取到的树是最新的。

第一句在屏障前,第二句在屏障后

第二句开始读之前,第一句必须s全部读完

Full Barrier(全能屏障)

包含了Store Barrier 和Load Barrier的功能。

MESI 优化

Store Buffere

而是把广播invalid指令发出去以后,然后直接把要修改的数据放到 Store Bufferes里,然后就去干其它事情了,当等到其他CPU都响应了ACK之后,然后再回头从Store Bufferes读取出来执行最后的数据修改操作。

Store Forward

要求CPU读取数据时得先看Store Buferes里面有没有,如果有则直接读取Store Buferes里的值,如果没有才能读取自己缓存里面的数据,这也就是所谓的“Store Forward”。

Invalidate Queue

收到其他CPU广播的invalid 消息后,不一定要马上处理,而是把放这个“失效队列里面”,然后就马上返回 invalid ack 。然后当自己有时间的时候再去处理失效队列里的消息,最后通过这种异步的方式,加快了CPU整个修改数据的过程。

总线嗅探机制

在现代计算机中,CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,在存取过程中,CPU 将一直空闲,这是一种极大的浪费,所以,为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。

由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而**嗅探是实现缓存一致性的常见机制**。

嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。

注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。

volatile的嗅探机制则会不断地占用总线带宽,因为一个CPU里面的变量修改了他要通过广播通知其他的CPU,导致总线流量激增,就会产生总线风暴。 总之,就是因为volatile 和CAS 的操作导致BUS总线缓存一致性流量激增所造成的影响。

1、总线锁

在早期处理器提供一个 LOCK# 信号,CPU1在操作共享变量的时候会预先对总线加锁,此时CPU2就不能通过总线来读取内存中的数据了,但这无疑会大大降低CPU的执行效率。

有volatile变量修饰共享变量在编译器编译后,后多出一个“lock” 来(lock前缀指令相当于一个内存屏障,会强制将对缓存的修改操作写入主内存)该字符在多核处理器下回引发两个事件:

1.将当前处理器缓存行的数据写回系统内存;

2.这个写内存的操作会使得其他处理器里缓存的该内存地址的数据无效。

总线仲裁

导致缓存不一致的另外一个问题在于,CPU操作共享数据的顺序性,想让并发的操作变得有序,那么常用的方式就是让操作的资源具备独占性,这也就是我们常用的的方式加锁,当一个CPU对操作的资源加了锁,那么其它CPU就只能等待,只有等前一个释放了锁(资源占用权),后面的才能获得执行权,从而保证整体操作的顺序性。

而实现这个机制的功能就叫“总线仲裁”,在多个CPU同时申请对总线的使用权时,为避免产生总线冲突,需由总线仲裁来合理地控制和管理系统中需要占用总线的申请者,在多个申请者同时提出总线请求时,以一定的优先算法仲裁哪个应获得对总线的使用权。

2、缓存一致性协议

如何通过自己的数据状态就能知道其他CPU的缓存情况从而做出对应的策略,而这套机制就是缓存一致性协议。

由于总线锁的效率太低所以就出现了缓存一致性协议,Intel 的MESI协议就是其中一个佼佼者。MESI协议保证了每个缓存变量中使用的共享变量的副本都是一致的。

上面的例子中,我们看到,使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。

要解决这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。

1.当写一个volatile变量时,JMM(java共享内存模型)会把该线程对应的本地内存中的共享变量值刷新到主内存;

2.当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来从主内存中读取共享变量。

synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中。synchronized直接从主内存读取最新值,然后加锁,在没有释放锁之前其他线程都无法操作这个值。

有序性

在Java中,可以使用synchronized或者volatile保证多线程之间操作的有序性。实现原理有些区别:

volatile关键字是使用内存屏障达到禁止指令重排序,以保证有序性。

synchronized的原理是,加锁也可以,原理是当一个线程进入 synchronize代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。加锁的线程直接去内存拿最新的值,因此不不会出现没有被通知到不知道值被的更改的情况

as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。

volatile关键字禁止指令重排序有两层意思:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

AQS的实现原理

state的含义:

比如说在信号量里面,state 表示的是剩余许可证的数量。如果我们最开始把 state 设置为 10,这就代表许可证初始一共有 10 个,然后当某一个线程取走一个许可证之后,这个 state 就会变为 9,所以信号量的 state 相当于是一个内部计数器。

再比如,在 CountDownLatch 工具类里面,state 表示的是需要“倒数”的数量。一开始我们假设把它设置为 5,当每次调用 CountDown 方法时,state 就会减 1,一直减到 0 的时候就代表这个门闩被放开。

下面我们再来看一下 state 在 ReentrantLock 中是什么含义,在 ReentrantLock 中它表示的是锁的占有情况。最开始是 0,表示没有任何线程占有锁;如果 state 变成 1,则就代表这个锁已经被某一个线程所持有了,如果是234就是可重入

并且state被volatile修饰实现了可见性,所以纯赋值操作对它来说没有线程安全问题,AQS里面都是直接setstate对state进行直接赋值。

AQS的获取操作:

比如 ReentrantLock 中的 lock 方法就是其中一个“获取方法”,执行时,如果发现 state 不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到锁,于是就让该线程进入阻塞状态。

再比如,Semaphore 中的 acquire 方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state 的值。如果 state 值是正数,那么代表还有剩余的许可证,数量足够的话,就可以成功获取;但如果 state 是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态,所以这里同样也是和 state 的值相关的。

再举个例子,CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state 减为 0,此时就代表现在这个门闩放开了,所以之前阻塞的线程就会被唤醒。

释放方法

释放方法是站在获取方法的对立面的,通常和刚才的获取方法配合使用。我们刚才讲的获取方法可能会让线程阻塞,比如说获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。

比如在 Semaphore 信号量里面,释放就是 release 方法(包含重载方法),release() 方法的作用是去释放一个许可证,会让 state 加 1;而在 CountDownLatch 里面,释放就是 countDown 方法,作用是倒数一个数,让 state 减 1。所以也可以看出,在不同的实现类里面,他们对于 state 的操作是截然不同的,需要由每一个协作类根据自己的逻辑去具体实现。

AQS追求性能的提现:

获取锁时候两次cas,先cas再tryacquired,链表插入时候直接插入,插入不了再进行完整插入

img

这里还要注意唤醒是从后往前遍历

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n3lHaUBr-1641896374387)(image-20211227034456053.png)]

signal 和cancel 唤醒后面的和取消自己

condition 条件队列节点状态

p’ropagate 共享模式特有的传播状态

AQS状态码含义,Siganl状态就表示当前节点如果被释放同步状态或者被取消就必须通知后面的节点,让后面的节点会被唤醒,但是这样有个问题,第一个节点没法被通知,所以初始情况就需要创建一个虚拟头节点让他来通知所以和头节点相连的节点会自动被唤醒去争抢锁,如果你给当前节点状态设置signal含义就是告诉前一个节点释放的时候唤醒它。抢到锁的节点又会成为新的头节点,旧的头节点指向null被JVM回收。后面的节点都根据前面节点状态是不是siganl将自己挂起,只有连接到头节点才会被唤醒

挂起线程就是把自己状态设置给signal

注意头节点并不存储数据,只是和他相连的线程会被唤醒去抢锁

AQS获取同步资源失败了做什么操作?

主要做了三件事:

  • 将当前线程封装成Node。
  • 判断队列中尾部节点是否为空,若不为空,则将当前线程的Node节点通过CAS插入到尾部,独占模式就是把节点状态设置为当前节点释放同步状态就唤醒下一个节点,共享模式就是释放后进行传播。
  • 如果CAS插入失败则通过完整入队方法插入到队列中。
  • 完整入队只有队列为空时候会触发,此时会把当前线程设置为头尾节点(这里主要是节省性能的提现)
  • 等待自己前一个节点把将自己唤醒,如果是独占模式就是head将自己唤醒,共享模式就是前一个节点具体看许可证数量

如果头节点相连的节点没有拿到锁会干什么?

非公平锁情况下可能拿不到锁,此时就判断头节点状态决定是否阻塞,如果头节点是signal就阻塞等待,否则

tryRelease方法为什么不用CAS进行减少锁计数

这个问题其实是最简单的一个问题,就是前面也提到的,能够执行到release方法这里来的线程都是已经获取到锁的线程,并且独占锁也只能是一个线程,因此不需要进行CAS进行比较后才赋值。

AQS中的interrupt方法有什么用?

正常wait sleep状态线程被挂起使用中断会抛出异常,但是它这里使用的LockSupport.park是unsafa类park方法的实现,系统内核的中断原语所以不会抛出异常,就仅仅改变中断标志位。然后因为当线程处于等待队列被挂起时候无法响应外部的中断请求,只有等这个线程拿到锁之后才能进行中断响应

  • 重入锁是什么?

  • 公平锁和非公平锁是什么?有什么区别?

  • ReentrantLock::lock公平锁模式现实

    • ReentrantLock如何实现公平锁?
    • ReentrantLock如何实现可重入?
  • ReentrantLock公平锁模式与非公平锁获取锁的区别?

  • ReentrantLock::unlock()释放锁,如何唤醒等待队列中的线程?

  • 当头结点下一个节点不为空的时候,会直接唤醒该节点,如果该节点为空,则会队尾开始向前遍历,找到队列中和头节点的后置节点,并且它的状态不能是cancel状态,然后唤醒。

  • ReentrantLock除了可重入还有哪些特性?

    • 支持线程中断,只是在线程上增加一个中断标志interrupted,并不会对运行中的线程有什么影响,具体需要根据这个中断标志干些什么,用户自己去决定。比如,实现了等待锁的时候,5秒没有获取到锁,中断等待,线程继续做其它事情。
    • 超时机制,在ReetrantLock::tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。
  • ReentrantLock使用场景

  • 独占模式拿到锁的线程有哪些释放方式?

  • 第一种直接释放锁然后从后往前遍历找到和头节点相连那个节点,让他调用tryacquired方法去获取锁。

  • 第二种调用await进入等待队列中等待,因为可能执行到一半需要达到某个条件才能继续执行,所以先释放那个条件达成,然后再让后面的线程唤醒加入同步队列

  • Q:某个线程获取锁失败的后续流程是什么呢?

    A:存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。

    Q:既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

    A:是CLH变体的FIFO双端队列。

    Q:处于排队等候机制中的线程,什么时候可以有机会获取锁呢?

    前置节点是头结点,且当前线程获取锁成功

    Q:如果处于排队等候机制中的线程一直无法获取锁,需要一直等待么?还是有别的策略来解决这一问题?

    A:线程所在节点的状态会变成取消状态,取消状态的节点会从队列中释放,出队列的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,

    Q:Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢?

    A:AQS的Acquire会调用tryAcquire方法,tryAcquire由各个自定义同步器实现,通过tryAcquire完成加锁过程。

    比如公平锁具体实现就是获得锁的时候要看看节点是不是队列中连着头节点那个,还有可重入锁判断state!=0不会马上放弃还会继续看看state的独占线程是不是当前线程,如果是的话就+1实现可重入。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZBtkSDkw-1641896374388)(image-20211210194657706.png)]

进入队列的入队方法有两个,第一个方法就是先尝试快速入队一下,第二个方法是完整入队方法,完整入队方法只比快速入队方法多了一个判断队列是否为空的操作,如果为空就创建一个虚的头尾节点。因为一般队列都不是空的,所以作者不想让每次入队都要判断队列是否为空浪费性能,所以先创建了一个快速入队的方法,

AQS主要是维护了一个volatile修饰的共享变量state和一个先入先出的队列,这个队列是一个虚拟的双向链表。

这里队列它维护了头尾节点都是虚节点,所以每次取节点时候都是头节点的后面一个节点去tryacquire方法获取锁

acquireQueued()这个方法会先判断当前传入的Node对应的前置节点是否为head,如果是则尝试加锁。加锁成功过则将当前节点设置为head节点,然后空置之前的head节点,方便后续被垃圾回收掉。

当有线程拿到锁的时候会调用cas把state改成1,acquire里面有个tryacquire的判断,tryacquire用来判断是否获取到锁,获取到就进行+1操作,然后其他线程就在队列里面等待,,获取到以后就出队通过cas改state进行+1操作这个state可以被多次+1Reentantlock用这种机制来实现可重入锁。因为这个过程是多个线程进来操作的,当其他线程看见state不为0,所以获取锁失败就会进入等待队列。进入等待队列的线程就等待持有锁的线程执行完毕,就会执行realease方法,去等待队列唤醒线程出来争抢锁。如果本来就在等待队列那就继续挂起,等下次realease再来竞争

等待队列等待的线程看看前置节点是不是头节点如果是头节点而且获取锁已经成功了那就cas修改state的值然后对资源进行调用。

但是如果这个节点的前置节点不是头节点或者没有获得锁就会判断是否挂起,判断是否被挂起取决于前置节点状态,如果前置节点也在等待锁那就挂起,这里挂起调用的locksupport.park操作系统原语来吧线程挂起,这样就保证head之后只有一个节点在通过CAS获取锁,其他线程都被挂起,避免了无用资源消耗CPU。接下来获取锁成功那个节点用操作完成以后就要调用release释放锁,如果释放锁成功就唤醒等待队列其他节点locksupport.unpark方法来唤醒,这里唤醒是从尾节点开始搜索找到一个除了头节点以外最靠前的被挂起的节点,被挂起的线程一旦唤醒就继续执行aquire方法,然后重复这个流程。

如果没有获取到锁,当线程执行完了以后调用realease方法进行释放,然后队列中从后往前遍历找到离头节点最近的被挂起的节点,将他唤醒然后调用tryacquire去获取锁,获取到了就通过cas改变状态

AQS共享模式

获取共享锁资源的时候,如果资源除了用于唤醒下一个节点后,还有剩余,就会用于唤醒后面的节点,直到资源被用完。这里充分体现共享模式的“共享”。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ga44SCif-1641896374389)(image-20211210143506930.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yvEIGLFS-1641896374389)(image-20211210143611044.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9wofJZ3U-1641896374390)(image-20211210143658889.png)]

队列是一个先进先出的双向链表,队列里面每个节点存取了前后指针,线程对象,等待状态,并且头节点是虚节点只是占位作用,真正拿锁改线程状态的是第二个节点,拿到锁以后第二节点变成头节点出队,所以源码中判断是不是头节点就是根据当前节点的前置节点去判断是不是头节点,如果当前节点前置节点是头节点那么他就是头节点。注意每个节点拿到锁执行完自己的操作以后就变成下一个结点的虚节点

AQS是抽象队列同步器,是一种锁机制

AQS实现类和资源共享方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UEQgAdV6-1641896374390)(image-20211210143721611.png)]

Reentantlock的实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7z7VMuUi-1641896374390)(image-20211210144503670.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zp2398ya-1641896374391)(image-20211210144634789.png)]

等待子线程结束的这些多种方法都是基于AQS实现的,主要继承AQS这个类然后重写它指定的方法,比如state可以当作信号量,也可以当作countdown线程-1,也可以当作cyclibarrier的屏障state达到几以后就满足条件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gn0g5WUE-1641896374391)(image-20211210145136993.png)]

countdownlatch的实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PJK8K5QJ-1641896374392)(image-20211210145214113.png)]

AQS的state怎么+1?

  • 当某个线程执行至tryAcquire方法时,有一个if条件判断,尝试获取锁的线程和持有锁的线程进行比较,如果一致,则对state执行加一的操作,表示锁重入

AQS实现锁的两种思路,尝试获取没有获取就立刻返回tryaquire,还有一定是一定要获取锁,没有获取到也要去队列中等待aquire

Tryaquire,通常是被当父类继承然后编写业务逻辑,调用成功时获取锁使用完了再释放,treRelease也是一样的,并且你不去继承他然后编写业务逻辑会抛出异常,release里面会调用tryrelease,aquire会调用Tryaquire。ReentrantLock里面有一个内部类Sync,Sync继承AQS,比如Reentantlock通过这个方法实现了公平锁非公平锁,实现公平锁机制主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。公平锁非公平锁代码实现一样只是多了一个条件,这个条件就是判断当前线程的前置节点是不是头节点,从而判断是他不是在队列中。

挂起是用的locksupport park,唤醒是unpark

调用aquire方法必须要先让tryauire获取到锁,没有得到锁就进入队列等待,进入等待队列等待的线程看看前置节点是不是头节点如果是头节点而且获取锁已经成功了那就返回被调用。但是如果这两个条件其中一个不满足就判断当前线程是否需要挂起,判断是否被挂起取决于前置节点,如果前置节点也在等待锁那就挂起,这里挂起调用的locksupport.park操作系统原语来吧线程挂起,这样就保证head之后只有一个节点在通过CAS获取锁,对他线程都被挂起,避免了无用资源消耗CPU。接下来获取锁成功那个节点用操作完成以后就要调用release释放锁,如果释放锁成功就唤醒等待队列其他节点locksupport.unpark方法来唤醒,这里唤醒是从尾节点开始搜索找到一个除了头节点以外最靠前的被挂起的节点,被挂起的线程一旦唤醒就继续执行aquire方法,然后重复这个流程。

为什么从尾节点开始搜索?

在竞争锁的过程中,会执行一个 enq() 方法,这个方法创建队列的头尾节点,因为队列会维护两个虚的头尾节点,所以这时候需要给之前那头或者尾虚节点赋值然后创建新的头或者尾节点。

node.prev = t;尾节点前驱和上一个尾节点相连先于CAS执行,也就是说,你在将当前节点置为尾部之前就已经把前驱节点赋值了,也就是说先让前驱赋值,再让他成为尾节点,自然不会出现prev=null的情况,但是你的后继是在cas设置节点之后执行的,这时候才让倒数第二个节点的后继指向倒数第一个节点,多线程下可能会出现后继被中断,此时从前往后遍历就少了一个节点

但是设置完了尾部节点出现并发然后还没开始设置后继next就开始从前后往后遍历就少了一个节点,然后其他线程要根据自己前一个线程的状态看看自己是不是要挂起

线程唤醒的时候,通常是从当前线程的下个节点线程开始寻找,但是下个节点有可能已经取消了或者为null了,所以从后向前找,直到找到一个非 取消状态的节点线程。

  node.prev = t;
            //set尾部节点
            if (compareAndSetTail(t, node)) {//当前节点置为尾部
                t.next = node; //前驱节点的next指针指向当前节点
                return t;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CPSZcVnm-1641896374392)(image-20211210154554242.png)]

我们继续来看acquire()方法,在执行完tryAcquire()方法后,如果加锁失败那么就会执行addWaiter()方法和acquireQueued(),这两个方法的作用是将竞争锁失败的线程放入到等待队列中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9WaSOtoW-1641896374393)(image-20211210155710242.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QaXwlxkL-1641896374393)(image-20211210205050520.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MxPWOdqe-1641896374394)(image-20211210161107623.png)]

这张图表示队列中的节点怎么等待的?

如果当前节点前一个节点被挂起,那他也挂起,如果前一个节点取消状态那就删除前一个,其他情况就把前一个节点直接挂起,这样保证队列只有一个线程会cas

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TV0xJWdv-1641896374395)(image-20211211004636262.png)]

Reentantlock底层

通过源码可以看到,lock()方法,首先是通过CAS的方式抢占锁,如果抢占成功则将state的值设置为1。然后将对象独占线程设置为当前线程。

如果抢占锁失败,就会调用acquire()方法,这个acquire()方法的实现就是在AQS类中了,说明具体抢占锁失败后的逻辑,AQS已经规定好了模板。

可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。

释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

独享锁 VS 共享锁

在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

在最开始提及AQS的时候我们也提到了state字段(int类型,32位),该字段用来描述有多少线程获持有锁。

在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。如下图所示:

图片

读锁使用的是AQS的共享模式,AQS的acquireShared方法如下,先判断写锁数量,再判断当前线程是不是写锁线程,因为写锁可重入,

写锁使用的是AQS的独占模式,因为写锁是排他锁所以获取锁释放锁逻辑和Reentrantlock差不多

并且也实现了公平非公平特性

当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。

读写锁获取锁主要也是通过重写tryacquire方法实现,在tryacquire里面写锁判断读锁是否存在,读锁判断写锁不存在可以对高16位+1,写锁判断读锁不存在对低16位+1,并且写锁不可重入,读锁可重入。其中写锁还支持锁降级可以降级成读锁,读写锁都实现可中断和condition。

锁升级:获取到读锁,没有释放读锁然后获取到写锁。

锁降级:获取到写锁,没有释放写锁然后获取到读锁。因为获取了读锁以后

读锁不支持条件队列,因为他是可以同一时刻被多个线程获取,不存在等待的情况

JAVA中断

运行状态只会改变运行中的某个值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BYXulHUJ-1641896374396)(image-20211210155935134.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0kwPwMKd-1641896374396)(image-20211210155901716.png)]

什么时候会抛出Interruptexception?

当线程在等待休眠或者被占用,也就是被调用了wait sleep join 这些方法的时候,然后在运行之前被interrupt中断就会抛出这个异常,

一般是处于 block waiting timewaiting状态的时候 ,注意locksupport不会抛出异常

如果活跃状态下中断线程会怎么样?

中断和线程是否活跃没关系,之所以不活跃状态下抛出异常是因为不从不活跃变成活跃就没法处理中断,如果活跃触发中断并不影响线程运行,你可以选择无视掉或者用这个触发的中断处理一些需求,活跃状态下可以用isInterrupted或者interrpted方法查询自己是否被中断

这两个方法有什么区别?

isinterrpted查询完自己被中断以后不会清除中断标志位,interrpted会清除这个标志位

*ReentraeenntLock使用场景

sync只用实现资源state的获取-释放方式tryAcquire-tryRelelase,至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了,我们不用关心。

  • 场景1:利用ReentraeenntLock不可重入特性,如果已加锁,则不再重复加锁,多用于进行非重要任务防止重复执行,如,清除无用临时文件,检查某些资源的可用性,数据备份操作等

  • 场景4:利用ReentraeenntLock可中断特性,可中断锁,取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞

    利用公平锁特性保证了线程先进先出FIFO的使用锁,不会产生"饥饿"问题,

主线程等待子线程结束的多种方法

CountDownLatch

目标是所有人执行完了我再执行,

await()底层调用tryAcquireShared,初始化state为线程值,countdown通过CAS让state-1一边减一边让线程去AQS双端队列,然后countdown就是等state等于0调用tryrelease方法,让AQS里面的虚拟双向队列唤醒线程,拿到锁就给state+1,整个过程就是countdown让state一直减,然后线程失去锁就一直去队列,等减到0的时候就唤醒队列里面的线程节点去争抢锁,但是这里争抢锁调用的await方法,await底层是重写的tryacquiredshared,它要求state等于LockSupport

底层用的unsafe类的park unpark0了才有资格去争抢锁,通过这种方法把队列里面的线程阻塞住

countdown底层调用AQS共享模式的releaseshared

  • CountDownLatch会将任务分成N个子线程去执行,state的初始值也是N(state与子线程数量一致)。N个子线程是并行执行的,每个子线程执行完成后countDown()一次,state会通过CAS方式减1。直到所有子线程执行完成后(state=0),会通过unpark()方法唤醒主线程,然后主线程就会从await()方法返回,继续后续操作。

同步辅助类,通过它可以阻塞当前线程。也就是说,能够实现一个线程或者多个线程一直等待,直到其他线程执行的操作完成。使用一个给定的计数器进行初始化,该计数器的操作是原子操作,即同时只能有一个线程操作该计数器。

CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。

是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

private static final class Sync extends AbstractQueuedSynchronizer {
   private static final long serialVersionUID = 4982264981922014374L;
   Sync(int count) {
       setState(count);
   }
}

我们看到创建CountDownLatch的过程,其实就是将count值赋值给state的过程。

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  


class Foo {
    CountDownLatch latch12 = new CountDownLatch(1);
    CountDownLatch latch23 = new CountDownLatch(1);

    public Foo() {

    }

    public void first(Runnable printFirst) throws InterruptedException {
        printFirst.run();
        latch12.countDown();//唤醒线程2
    }

    public void second(Runnable printSecond) throws InterruptedException {
        latch12.await();//latch12的值为0会执行下面的语句,否则会在此次阻塞
        printSecond.run();
        latch23.countDown();//准备唤醒线程3
    }

    public void third(Runnable printThird) throws InterruptedException {
        latch23.await();//latch23的值为0会执行下面的语句,否则会在此次阻塞
        printThird.run();
    }
}

使用场景

在某些业务场景中,程序执行需要等待某个条件完成后才能继续执行后续的操作。典型的应用为并行计算:当某个处理的运算量很大时,可以将该运算任务拆分成多个子任务,等待所有的子任务都完成之后,父任务再拿到所有子任务的运算结果进行汇总。

代码示例

调用ExecutorService类的shutdown()方法,并不会第一时间内把所有线程全部都销毁掉,而是让当前已有的线程全部执行完,之后,再把线程池销毁掉。

Semaphore

总结:如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放。他和countdownlatch主要区别是它是计数器里面有值下一个线程才能拿到计数器执行然后把计数器-1,countdownlatch则是要等计数器完全清0代表所有线程执行完毕然后轮到自己执行了才执行

Semaphore的acquired方法底层tryAcquireShared(),此时state可以理解为信号量,访问一个资源就必须要一个信号量,acquired就是访问一个资源的方法所以让state信号量-1,release方法底层就是**tryReleaseShared()**会对信号量进行+1,AQS里面执行过程就是唤醒线程。独占模式下是拿锁成功就+1执行完了就变为0,共享模式就是-1变为0以后唤醒再来拿锁,独占模式release方法就是让单个线程唤醒,共享模式多个线程。这semaphore不是唤醒再来拿锁,是等那个持有锁的执行完了释放然后state又重新+1,如果没抢到的就去队列等,这里又涉及到公平semaphore和非公平,公平的话加了个条件必须优先处理队列和头节点相连的,非公平就可能导致队列里面的线程饿死

Semaphore s12 = new Semaphore(0);Semaphore s23 = new Semaphore(0);public Foo() {}public void first(Runnable printFirst) throws InterruptedException {    printFirst.run();    s12.release();//释放后s12的值会变成1}public void second(Runnable printSecond) throws InterruptedException {    s12.acquire();//没有会阻塞  当为1的时候,说明线程2可以拿到s12了    printSecond.run();    s23.release();//释放后s23的值会变成1}public void third(Runnable printThird) throws InterruptedException {    s23.acquire();//0的时候拿不到,1的时候可以拿到    printThird.run();}

CyclicBarrier

public CyclicBarrier(int parties)parties 是参与线程的个数public CyclicBarrier(int parties, Runnable barrierAction)第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务public int await() throws InterruptedException, BrokenBarrierExceptionpublic int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException线程调用 await() 表示自己已经到达栅栏BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时

概述

  • 线程调用 await() 表示自己已经到达栅栏,一个线程组的线程需要等待所有线程完成任务后再继续执行下一次任务
  • BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时

是一个同步辅助类,允许一组线程相互等待,直到到达某个公共的屏障点,通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。

与CountDownLatch有相似的地方,都是使用计数器实现,当某个线程调用了CyclicBarrier的await()方法后,该线程就进入了等待状态,而且计数器执行加1操作,当计数器的值达到了设置的初始值,调用await()方法进入等待状态的线程会被唤醒,继续执行各自后续的操作。CyclicBarrier在释放等待线程后可以重用,所以,CyclicBarrier又被称为循环屏障。

使用场景

可以用于多线程计算数据,最后合并计算结果的场景。

CyclicBarrier与CountDownLatch的区别

  • CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法进行重置,并且可以循环使用
  • CountDownLatch主要实现1个或n个线程需要等待其他线程完成某项操作之后,才能继续往下执行,描述的是1个或n个线程等待其他线程的关系。而CyclicBarrier主要实现了多个线程之间相互等待,直到所有的线程都满足了条件之后,才能继续执行后续的操作,描述的是各个线程内部相互等待的关系。
  • CyclicBarrier能够处理更复杂的场景,如果计算发生错误,可以重置计数器让线程重新执行一次。
  • CyclicBarrier中提供了很多有用的方法,比如:可以通过getNumberWaiting()方法获取阻塞的线程数量,通过isBroken()方法判断阻塞的线程是否被中断。

Condition

协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。

sychronized只有一个等待队列,对象调用wait方法释放锁就会进入等待队列,被notify唤醒进入同步,对比condition也是await进入等待队列,然后signal唤醒进入同步

拿到锁的对象有两种选择,一种是直接释放去通知同步队列和头节点相连的几点,一种是调用signal或者notify通知等待队列加入到同步队列

img

img

  • 1)多个形成执行lock()方法时,线程会竞争获取同步锁state,获取成功的线程占有锁state、获取失败的线程会封装成node加入到AQS的同步队列中,等待锁state的释放。

  • (2)等获取了state锁的线程(同步队列中head节点)执行await()方法时,condition会将当前线程封装成一个新的node添加到condition等待队列的尾部,同0时释放锁,直到被唤醒。

  • (3)等获取了state锁的线程(同步队列中head节点)single()方法时,condition会将等待队列首节点移动到同步队列的尾部,直到获取同步锁state才被唤醒。

  • 1)能够调用Condition.await()方法的节点是获取了同步state锁的node,即同步队列中的head节点;调用Condition的await()方法(或者以await开头的方法)会使当前线程进入等待队列并释放锁、唤醒同步队列中的后继节点,最后线程状态变为等待状态。

  • 唤醒signal
  • 自旋唤醒等待队列的firstWaiter(首节点),在唤醒firstWaiter节点之前,会将等待队列首节点移到同步队列中。

  • 为什么需要使用Condition(什么时候需要使用await/signalAll/signal方法)?

    因为有时候获得锁的线程发现其某个条件不满足导致不能继续后面的业务逻辑,此时该线程只能先释放锁,等待条件满足。那可不可以不释放锁的等待呢?比如将await方法替换为sleep方法(这也是面试经常问的await和sleep的区别)?sleep不会释放锁,如果你想要的那个条件正好需要这把锁就会导致这个条件永远没法满足,所以必须要释放锁让那个条件先满足
    
 图中可以看到,每个Condition会有自己单独的等待队列,调用await方法,会放到对应的等待队列中。当调用某个Condition的signalAll/signal方法,则只会唤醒对应的等待队列中的线程。唤醒的粒度变小了,且更具针对性。如果只使用一个Condition的话,有些线程即使被唤醒并取得锁,其依然有可能并不满足条件而浪费了机会,产生时间损耗,相当于notEmpty的Condition唤醒了notFull的队列线程。

Threadlocal

为什么value要为强引用?因为有可能这时候这个value正在被使用,防止在只用的时候被垃圾回收,使用完了发现key为null以后调用threadlocal任何方法都会把value回收掉

Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系

img

一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。

通过线程得引用得到threadlocalmap,再让引用当key得到threadllocapmap里面key对应得value

threadlocal在高并发环境下给线程提供了一个可以隔绝其他线程访问得局部变量空间,每个线程都拿threadlocalmap里面得entry得那个value值来当作自己独立得变量副本空间,其他线程都无法访问到

Map结构:

ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同。

比如其中一个不同点就是,我们知道 HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链,但是 ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。这是 ThreadLocalMap 和 HashMap 在处理冲突时不一样的点。

内存泄露问题:

虽然 ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用无法被回收。

在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。

所以,在使用完了 ThreadLocal 之后,我们应该手动去调用它的 remove 方法,目的是防止内存泄漏的发生。

img

ThreadLocal 适合用在哪些实际生产的场景中?

场景1,ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

场景2,ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念,这样会让代码更加简洁。

ThreadLocal 是用来解决共享资源的多线程访问的问题吗?

ThreadLocal 并不是用来解决共享资源问题的。虽然 ThreadLocal 确实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。它可以在 initialValue 中 new 出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。

ThreadLocal 和 synchronized 是什么关系

  • ThreadLocal 是通过让每个线程独享自己的副本,避免了资源的竞争。
  • synchronized 主要用于临界资源的分配,在同一时刻限制最多只有一个线程能访问该资源。

相比于 ThreadLocal 而言,synchronized 的效率会更低一些,但是花费的内存也更少,但是threadlocal还可以作为一个线程安全得全局变量来使用

Sleep和wait的区别?

wait的等待唤醒机制通常用于线程通信,sleep用于暂停

sleep任何场景都可以用,wait只能在同步代码块中,因为wait要释放锁,sleep不用释放锁

wait是通过notify或者notifyall这种方法唤醒也可以设置时间,然后去锁池争抢锁,sleep是设置的时间自动唤醒

wait是object类,sleep是线程类Thread的静态方法

Synchronized

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vd0Fv3qH-1641896374397)(image-20211209212039219.png)]

*说一下 synchronized 底层实现原理?*

底层字节码里面有一个moniotrenter和两个moniotorexit,其中moniotorenter用来获取锁,第一个moniotorexit用来释放锁,第二个monitorexit防止同步代码块中线程出现异常没有释放锁,就会导致线程死锁。因此第二个monitorexit保证线程异常时候也会释放锁。

而且每个对象还会在底层维护一个计数器,在使用可重入锁时候记录重入了几次,当线程获取一次该锁时,计数器加1,再获取再加1,释放就减1,减到0就表示没有被任何线程使用。

什么是monitor(监视器或者管程),

Synchronized的同步机制如下

只能容纳一个线程的房间,一个线程进入其他线程等待

Entryset聚集想进入monitor的线程,处于waiting状态进入monitor后进入active状态,如果遇到判断条件让他暂时退出monitor就会进入waitset中等待,状态变成waiting,然后entryset线程就可以进入monitor,假设这个线程完成了任务就可以唤醒之前waitset中的等待线程

Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。这里还可以重复进入the owner。早期JDK版本都是用的mute lock重量级锁,后来进行升级不会直接调用mute lock,引入了偏向锁轻量级锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ppe9CinZ-1641896374398)(image-20211209212106546.png)]

synchronized的用法有哪些?

必须有“对象”来充当“锁”的角色。

1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁 。静态上下文中是不能引用this,this代表的是当前对象。因为静态的东西不属于任何对象,他是所有对象共享的,如果你用this,他根本不知道你指代的是哪个对象。
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象,因为一个静态类不管new多少次生成的对象都是同一个对象,所以如果线程A访问非静态方法,线程B访问静态方法并不会引起锁冲突,因为一个拿的类锁,一个拿的对象锁
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

Synchronized的作用有哪些?

  1. 原子性:确保线程互斥的访问同步代码;
  2. 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
  3. 执行过程的有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”。

什么是死锁(deadlock)?

互斥使用:当资源被一个线程使用时,别的线程不能使用 。互斥没办法破坏因为锁本来就是要实现互斥访问

不可抢占:资源请求者不能强制从资源占有者手里夺取资源。 破坏:一次性申请所有资源,不从别人手里抢占

占有且等待:线程在请求其他资源的同时保持对原有资源的占有。破坏:释放自己占有资源,再一次申请所有资源

循环等待:线程1等待线程2占有资源,线程2等待线程1形成环路。

破坏:按照锁排序顺序来申请,如果线程需要A锁和B锁才能继续操作,可以规定只有拿到A锁的线程才能去获取B锁,指定锁的申请顺序

*Synchronized和lock* ****的区别 ****

· *ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、等。*

ReentrankLock更加灵活,可以实现重入锁,不重入锁,公平锁,非公平锁,提供超时机制在指定之内获得锁,得不到就返回,还支持响应中断

但是ReentrankLock需要手动finally释放,synchronized会自动获得释放锁\

还有性能因为Synchronizedjdk1.6引入了锁升级,自适应自旋性能也提升了很多,之前比rl慢

synchronized底层是mute lock

手动释放,范围代码块 类变量代码块

因为绑定了条件队列可以实现选择性通知,不像synchronized那样只有一个等待队列通过wait来通知,粒度比较大

synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,一般来说我们优先选择他,以前jdk版本比较慢但是引入了锁优化机制以后就快很多,并且不用担心忘了释放锁的风险,concurrenthashmapjdk1.8就把reentrantlock换成了sychronized

img

CAS

CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统应用范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。

valueOffset表示在内存中偏移地址 ,CAS是一条CPU并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

unsafe类就是java可以直接访问底层操作系统的一个后门,它是通过本地native方法来访问,cas就是一个native方法,并不是靠java语言实现的

注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务

为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类

  • 循环时间长CPU开销大
  • 只能保证一个共享变量的原子操作
  • 会引发ABA问题

ABA 问题

版本号控制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-06L4gbAI-1641896374398)(image-20220104104328381.png)]

范围不能灵活控制

CAS 的第三个缺点就是不能灵活控制线程安全的范围。

通常我们去执行 CAS 的时候,是针对某一个,而不是多个共享变量的,这个变量可能是 Integer 类型,也有可能是 Long 类型、对象类型等等,但是我们不能针对多个共享变量同时进行 CAS 操作,因为这多个变量之间是独立的,简单

AtomicReference来保证原子性,AtomicReference 主要是依赖于 sun.misc.Unsafe 提供的一些 native 方法保证操作的原子性,unsafe类可以获取成员属性在操作系统内存地址的偏移量,因此getset操作都是原子性的对内存地址直接进行操作,并且用volatile修饰保证了getset操作的可见性,获取了内存地址以后可以基于CAS进行交换。AtomicInteger 是对整数的封装

自旋时间过长

CAS 的第二个缺点就是自旋时间过长。的把原子操作组合到一起,并不具备原子性。因此如果我们想对多个对象同时进行 CAS 操作并想保证线程安全的话,是比较困难的。

由于单次 CAS 不一定能执行成功,所以 CAS 往往是配合着循环来实现的,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。

可是如果我们的应用场景本身就是高并发的场景,就有可能导致 CAS 一直都操作不成功,这样的话,循环时间就会越来越长。而且在此期间,CPU 资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用 CAS,在高并发的场景下,通常 CAS 的效率是不高的。

原理

*CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。*

unsafe

img

java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,主要提供了以下功能:

通过Unsafe类可以分配内存,可以释放内存

可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的

挂起与恢复

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。这种挂起是无法被中断的

CAS操作

是通过compareAndSwapXXX方法实现的

多线程通信的几种方式

从操作系统角度来讲就是

事件event:通过通知操作的方式来保持多线程同步,还可以方便实现多线程优先级的比较操作,Wait/Notify 等待唤醒

互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制,本质是一个计数器

信号量PV(Semphares):它允许同一时刻多个线程来访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量【用来实现生产者消费者模型】

信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程,信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。 注意,信号量的值仅能由PV操作来改变

P表示可操作资源数-1,V表示+1

synchronized

AQS系列

CyclicBarrier

当某个线程调用了CyclicBarrier的await()方法后,该线程就进入了等待状态,而且计数器执行加1操作,当计数器的值达到了设置的初始值,调用await()方法进入等待状态的线程会被唤醒,继续执行各自后续的操作。

从0到n

Semaphore

场景:假如你有多个线程都要访问一个服务,但是这个服务并发量有限制,访问的太多就会崩溃,这时候就可以用信号量来控制访问得数量

  • 在初始化的时候可以设置公平性,如果设置为 true 则会让它更公平,但如果设置为 false 则会让总的吞吐量更高,但是有得线程会饥饿等待

  • 信号量能被 FixedThreadPool 替代吗?

  • 因为我们的信号量具有跨线程、跨线程池的特性,所以即便这些请求来自于不同的线程池,我们也可以限制它们的访问。如果用 FixedThreadPool 去限制,那就做不到跨线程池限制了,这样的话会让功能大大削弱。

CountDownLatch

state从n countdown到0才开始执行

ReentrantLock+Condition

LockSupport

底层用的unsafe类的park unpark

SynchronousQueue

SynchronousQueue 是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)

在线程池中用Exeucor创建的可缓存的线程池就是用的这个类型队列

CopyOnWriteArrayList 有什么特点?

CopyOnWriteArrayList 的思想比读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,更厉害的是,写入也不会阻塞读取操作,也就是说你可以在写入的同时进行读取,只有写入和写入之间需要进行同步,也就是不允许多个写入同时发生(因为不可能创建多个副本吧),但是在写入发生时允许读取同时发生。这样一来,读操作的性能就会大幅度提升。

从 CopyOnWriteArrayList 的名字就能看出它是满足 CopyOnWrite 的 ArrayList,CopyOnWrite 的意思是说,当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器,完成修改之后,再将原容器的引用指向新的容器。这样就完成了整个修改过程。

这样做的好处是,CopyOnWriteArrayList 利用了“不变性”原理,因为容器每次修改都是创建新副本,所以对于旧容器来说,其实是不可变的,也是线程安全的,无需进一步的同步操作。我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,也不会有修改。

CopyOnWriteArrayList 的所有修改操作(add,set等)都是通过创建底层数组的新副本来实现的,所以 CopyOnWrite 容器也是一种读写分离的思想体现,读和写使用不同的容器。

  • 内存占用问题

因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,这一点会占用额外的内存空间。

  • 在元素较多或者复杂的情况下,复制的开销很大

复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,会降低整体性能。

  • 数据一致性问题

由于 CopyOnWrite 容器的修改是先修改副本,所以这次修改对于其他线程来说,并不是实时能看到的,只有在修改完之后才能体现出来。如果你希望写入的的数据马上能被其他线程看到,CopyOnWrite 容器是不适用的。

add操作:总结流程:在添加的时候首先上锁,并复制一个新数组,增加操作在新数组上完成,然后将 array 指向到新数组,最后解锁。

.CopyOnWriteArrayList的写操作性能较差因为复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,会降低整体性能。,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多它不需要内存拷贝,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6hYJWO3K-1641896374399)(image-20220102140121064.png)]

构造方法静态块是new这个线程类的线程调用的,run才是线程自己调用的

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法

因为run方法里面的代码是被线程自身调用的,start方法是被另一个线程调用来生成这个线程。当启动start以后就从新建状态变成就绪状态

Java 是怎么加锁的,有哪些锁?分别在哪些场景使用?(各种分类,乐观锁、悲观锁…)

多线程访问 double 会怎么样

jvm一般情况下最多能操作32位,

Long 和 double 的值需要占用 64 位的内存空间,而对于 64 位值的写入,可以分为两个 32 位的操作来进行。

这样一来,本来是一个整体的赋值操作,就可能被拆分为低 32 位和高 32 位的两个操作。如果在这两个操作之间发生了其他线程对这个值的读操作,就可能会读到一个错误、不完整的值.,由于规范规定了对于 volatile long 和 volatile double 而言,JVM 必须保证其读写操作的原子性,所以加了 volatile 之后,对于程序员而言,就可以确保程序正确。

Java 线程的同步方式有哪些

synchronized:方法变量代码块,wait notify

threadlocal

volatile

AQS系列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FLTHFZF1-1641896374399)(image-20211210143721611.png)]

阻塞队列

sleep 和 wait 区别(两个方法加 synchronized,一个线程进去后 sleep,另一个线程可以进入到另一个方法吗)

标签:队列,无标题,CPU,获取,任务,线程,节点
来源: https://blog.csdn.net/weixin_44076644/article/details/122438638

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有