JAVA多线程知识框架笔记
2022-10-27 11:27:18 0 举报
AI智能生成
JAVA多线程知识框架笔记
作者其他创作
大纲/内容
进程/线程基础概念
概念:什么是线程/进程,二者区别
用户线程和守护线程区别
用户线程:JVM 会在终止之前等待任何用户线程完成其任务。
守护线程:其唯一作用是为用户线程提供服务。守护线程不会阻止 JVM 退出。
守护线程作用?
将守护线程用于后台支持任务,比如垃圾回收、释放未使用对象的内存、从缓存中删除不需要的条目。
怎么设置:Thread.setDaemon()
检查是否为守护线程:调用isDaemon()
何时用线程/为什么用线程?
cpu/计算密集型程序:最大化利用硬件cpu
IO密集型程序:1.解决超时 2.防止阻塞
启动多少线程合适?
计算密集型程序:任务以内存中的计算为主。避免线程上下午切换的成本。一般CPU是N核,就开N+1个线程。
IO密集型程序:尽量多开启一些线程并发做IO操作,因为在IO过程中,CPU几乎是闲置的。一般可能是2*cpu个数,当然应该根据下面的标准公式计算好一些
标准计算公式:线程数 = (IO时间 + CPU工作时间) / CPU工作时间 * 核数
实际都要根据实践来优化。
上下文切换
什么是上下文切换
任务从保存执行状态到再次被执行的过程
多线程一定快吗?
不一定
如何减少上下文切换?
通过减少线上大量WAITING的线程,来减少上下文切换次数。
什么是线程安全
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
另外 线程安全的体现在类主要成员变量上,因为成员变量是创建在堆上的,是线程共享的,而方法调用不存在线程安全问题,原因是局部变量的对象的引用是放到栈上的。
多线程相关工具
测量上下文切换时长工具:Lmbench
测量上下文切换次数工具:vmstat
查看线程执行状态等信息:jstack
dump线程
java并发底层机制
java代码执行流程
lt;span style=quot;font-size: inherit;quot;gt;Java代码—Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
Volatile
定义
确保所有线程看到这个变量的值是一致的。
保证并发中:原子性,可见行,有序性中的其中两个,即是可见行和有序性
实现原理
可见性
汇编语言-Lock前缀指令
1)Lock前缀指令会引起处理器缓存回写到内存。
lt;bgt;锁定缓存:lt;/bgt;Lock信号在之前的一些处理器,可能会锁IO总线,但是现在的cpu基本都是锁cpu的缓存内存,因为锁IO总线开销大
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效(缓存一致性协议 MESI)
即便缓存行的数据重写回系统内存,但是要保证多个处理器中的缓存的数据是一致且最新,就通过 MESI协议实现
有序性:禁止指令重排
Volatile的优化
共享追加到64字节优化性能
原理
cpu的缓存行有多级的,是64个字节宽,不支持部分填充缓存行,这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。
是否volatile都要追加到64字节
缓存行非64字节宽的处理器。如P6系列和奔腾处理器,它们的L1和L2高速缓存行是32个字节宽。
共享变量不会被频繁地写。因为使用追加字节的方式需要处理器读取更多的字节到高速缓冲区,这本身就会带来一定的性能消耗,如果共享变量不被频繁写的话,锁的几率也非常小,就没必要通过追加字节的方式来避免相互锁定。
synchronized
用法
1. 对于普通同步方法,锁是当前实例对象。
常见 面试题:不同线程是否可以访问同一个实例对象下的不同的同步方法,答案是不可以的,原因就是synchronized是一个java对象的内置锁,一旦对象上了锁,java对象头会保存对应线程ID信息,其他线程无法获取实例对象的。
2.对于静态同步方法,锁是当前类的Class对象。
类锁:为什么可以对类上锁,因为每个java类在jvm都会有一个自己真正的class对象,这个 class对象其实就是一个普通的oop,但这个普通话的oop只有一个,是以class的名称命名的。所以类锁和普通对象的锁是一致的。
3. 对于同步方法块,锁是Synchonized括号里配置的对象
原理
关于Monitor的解释:
1.jvm实现锁原理就是通过Monitor的进入和退出实现的,monitor对象是一个监视器,获取了monitor就获取它的使用权,也就是获取到锁,线程就拥有指令monitorenter和monitorexit之间的代码的使用权。
2.那么在jvm中,每一个实例对象和class对象都拥有一个自己monitor对象,注意类对象和实例对象拥有不同的对象头,他们是不一样的。那么monitor是怎么和对象想关联的呢?其实每个对象的对象头中有一块儿MarkWord区域来保存锁信息,即通过保存线程ID,来将对象和线程关联。也就是说每一个对象自己就是一个monitor,这也是为什么说java对象是内置锁了。
3.每个线程在竞争锁,也就是修改每个对象头中锁标记的一个过程,一般通过cas操作来获取。如果检验到或者CAS操作修改对象头中信息成功,那么该线程就和该对象关联了起来,就可以使用monitorenter和monitorexit之间的 代码了,其他线程是不允许的。
汇编级语言 锁监视器Monitor对象
代码块
monitorenter和monitorexit
修饰方法
检查方法是否设置ACC_SYNCHRONIZED标志,如果设置再获取monitor对象
Java对象头
有一块MarkWord区域保存 对象的hashcode,锁内容等信息
锁升级
SE 1.6中,锁一共有4种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,锁可以升级但不能降级。
偏向锁
获取:锁竞争不只是多线程,当只有一个线程时,如果反复修改对象头中保存这个线程的ID,那么这也是一种竞争,资源消耗,所以如果只有一个线程,保存成功后,就不删除标记,以后获取都通过CAS操作来检验即可。
撤销:如果有锁的竞争,等到执行代码到了安全的局点,根据情况改变锁状态,要么无锁,要么还是偏向锁,要么不适合做偏向锁,开始锁升级
关闭:默认开启,可以通过jvm设置
优点:加锁和解锁不需要额外的消耗,和非同步方法仅存在纳秒级的差距
缺点:如果锁竞争 ,会有额外的开销
轻量级锁
当有多个线程时,就会升级成为轻量级锁,该锁只是适用,多个线程是轮流执行代码块时用,意味着代码块执行的很快,一个线程完了,下一个执行,这个时候线程持有锁时间不长,其他线程只需要自旋就可以。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
获取和释放:都是通过cas操作 ,成功说明没有竞争,否则升级为重量级锁
优点:不会阻塞,提高响应的 速度
缺点:得不到锁的线程会自旋,消耗cpu
重量级锁
优点:不会自旋消耗cpu
缺点:线程阻塞,响应时间缓慢
核心组件
Contention List: 同步队列/竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
Entry List: Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
OnDeck: 任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
Owner: 当前已经获取到所资源的线程被称为 Owner;
Wait Set: 等待队列: 哪些调用 wait 方法被阻塞的线程被放置在这里;
java内存区域
java中原子类
定义
原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。
实现原理
cpu实现
对IO总线加锁:总线上加Lock信号
对缓存行加锁:Lock信号+缓存一致性协议
缓存行加锁失效两种情况
当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cacheline)时,则处理器会调用总线锁定。
有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
java实现
锁操作来实现原子操作
通过锁机制来锁住内存区域,另外在获取锁和释放锁也是使用循环CAS来实现的
循环CAS的操作来实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
CAS实现原子操作的三大问题
1)ABA问题。
使用版本号解决 ,通过检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,全部相等,则更新值。
2)循环时间长开销大。
如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。
第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;
第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU PipelineFlush),从而提高CPU的执行效率。
3)只能保证一个共享变量的原子操作。
如果想保证多个变量的原子性就可以使用锁。
或者把多个变量放到一个对象中,因为jdk1.5后提供的AtomicReference类可以保证对象的原子性
java提供的原子类
Atomic包里的类基本都是使用Unsafe实现的包装类。
使用原子的方式更新基本类型:AtomicBoolean,AtomicInteger, AtomicLong。
通过原子的方式更新数组里的某个元素:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类:AtomicReference,AtomicReferenceFieldUpdater
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater, AtomicStampedReference
新增的一些关于dubbo原子的类
多线程操作
实现多线程的几种方式
继承 Thread 类
实现 Runnable 接口。
实现Callable,可以接收返回值 ,和Future、FutureTask配合可以用来获取异步执行的结果。
使用 线程池的 方式
线程的状态
NEW 初始
线程创建好还没有调用star()方法
RUNNABLE 运行和就绪
Thread.start()
BLOCKED 阻塞
等待进入synchronized方法或者块,即那些Object.notify/AllNotify 或者LockSupport.unpark调用以后
WAITING 等待
Object.wait();Object.join(); LockSupport.park()
TIME_WAITING 超时等待
Thread.sleep(longtime); Object.wait(longtime); Object.join(longtime); LockSupport.parkUtils()
TERMINATED 终止
代码执行完成
注意:
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java. concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
Thread.sleep()
释放资源问题: 不会释放资源醒来后一定继续执行下面的代码 因为没有释放资源
底层调用: 是一个native方法
是否需声明异常: 方法声明要捕获异常
线程状态: 线程调用后 线程进入time_waiting状态
作用范围: 任意地方。
Object.wait()
释放资源问题: 释放锁资源,被唤醒后 不一定继续执行后面的代码 因为它是释放了锁资源,所以要重新竞争资源
底层调用:超时的wait(timeout)是一个native方法 wait调用的就是wait(0)
是否需声明异常: 方法声明有中断异常
线程状态: 线程调用后 线程进入waiting状态
作用范围: 是synchronized的 消息/通知机制中的用法
唤醒 操作: notify不能在之前执行,会报错
LockSupport.park()
释放资源问题: 不会释放锁资源
底层调用: 底层 调用的Unsafe下的native的 park方法,park()阻塞当前线程,park(Thread) 阻塞指定的线程
是否需声明异常: 不需要
线程状态: 在sychronized中,获取到锁的线程调用该方法进入waiting状态,其他线程都是bolcked状态。如果是ReentratLock锁,因为是重入锁,所有的线程都是waiting状态。
作用范围: 任意地方
唤醒操作:LockSupport.unpark() 可以在LockSupport.park()前面执行 唤醒后 一定继续执行后续的代码
Condition.await()
释放资源问题: 会释放资源
底层调用: 底层使用的 是LockSupport.park()
是否需要声明异常: 需要
线程状态: waiting
作用 范围 : 需要在lock块儿中执行,以来lock对象的
唤醒操作: signal不能在前面 执行,唤醒后不一定继续执行代码,因为释放了锁资源,要重新竞争
中断线程
概念
只是一个信号量,是一个标示位,仅此而已。线程在任意位置和时刻自我进行检查Thread.isIinterrupted(),从而自己判断线程何时退出
用法
Thread.interrupted()
注意
如果代码中抛出InterruptedException,那么中断的标识符就会被清除,此时调用isInterrupted()方法将会返回false。
安全的终止程序
通过boolean变量和Thread.interrupted()在 代码块中加入 判断:例如if(boolean !Thread.currentThread().isInterrupted())
暂停,恢复,终止线程
过期的suspend()、resume()和stop()
过期不用的原因
suspend()方法可能会引起死锁,stop()方法可能导致程序出现一些不确定的状态,因为 线程是非正常结束的
线程间通信
线程共享堆栈,因此 可以通过共享变量隐士的通信
volatile可见性,使用共享变量的方式,可以让两个线程通信,比如一个boolean变量true时,线程A运行,线程B通过设置boolean=false来结束线程A。
或者通过调用 方法,显示的通信
等待/通知机制 Object.wait()和Object.notify/Object.notifyAll()来实现
先加锁: 1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。调用wait后,会释放锁资源
状态改变: 2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
wait返回时机: 3)notify()或notifyAll()方法调用后,线程执行完后面的代码 最后释放锁之后,等待线程才有机会从wait()返回。
notify()或notifyAll()区别: 4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
5)从wait()方法返回的前提是获得了调用对象的锁。
等待/通知机制的用法 经典范式
等待方(消费者)
等待方遵循如下原则。1)获取对象的锁。2)如果条件不满足(条件可自定义任意变量),那么调用对象的wait()方法,被通知后仍要检查条件。3)条件满足则执行对应的逻辑。
通知方(生产者)
通知方遵循如下原则。1)获得对象的锁。2)改变条件。3)通知所有等待在对象上的线程。
Thread.join()
含义: 线程B调用了 A.join(),那么只有线程A终止之后线程B.join()返回。所以如果想实现顺序执行,线程B调用join方法前面不要写其他的业务代码。
源码逻辑结构:总的来说还是使用的等待/通知机制。使用的仍然是经典范式。就是即加锁、循环和处理逻辑3个步骤。这里加锁是对 线程加锁,循环判断是判断线程是否isAlive()。所以当线程a结束,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。
ThreadLocal
定义:以ThreadLocal对象为键,任意对象为值,并捆绑到当前的线程,从而实现多线程使用共享变量。
用法:通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
数据结构
Thread类下定义如下变量 ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocalMap是ThreadLocal.里面的一个内部类,里面维护是一个键值对 儿的Entry数组,该数组且是一个弱引用,这样可以 避免threadLocals因为空被回收。其中键是ThreadLocal对象,value是保存的对象。注意没有链表结构的
ThreadLocal的hashcode是通过调用AtomicInteger的getAndAdd方法,参数是个固定值0x61c88647。每个线程可能有多个ThreadLocal,所以它们的hashcode自动加一的
hash冲突
自动寻址法,存在冲突,就查找下一位置,知道发现有空位置
管道输入/输出流
它主要用于线程之间的数据传输,而传输的媒介为内存。
4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常。
线程实现顺序执行
1.使用join
2.使FutureTask,但是任务得是实现Callanle接口的,将任务放到一个有序缓存池中
3.使用线程池中基于数组或者链表的有序队列
4.使用信号量Semaphore
锁
死锁
概念:线程无法释放锁就叫死锁。
发生场景
场景1: 线程t1和线程t2互相等待对方释放锁,现实编写这样的代码比较少
场景2: t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。
场景3: t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。
如何避免死锁
1. 避免一个线程同时获取多个锁。
2.避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
3.尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
4.对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
锁的作用
通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
Lock和synchronized区别
前者JDK实现,后者JVM实现。所以用法不同。
synchronize在优化后,性能和ReentratLock基本差不多
trylock() 可以尝试非阻塞的获取锁
lockInterruptily()可以响应中断,在锁的获取中可以中断当前线程
AQS
定义
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。方便的同步组件有(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
队列同步器的接口
同步器的设计是基于模板方法模式的,同步器有指定的模版方法 可以重写,但是其他的模版方法直接调用即可。不用关心内部的实现逻辑,简化实现自定义的同步器的开发流程。
模版方法分为三种:1.独占式获取与释放同步状态、 2.共享式获取与释放同步状态、 3.查询同步队列中的等待线程情况。
队列同步器的实现
同步队列
一个FIFO双向队列,线程获取同步状态失败,则加到同步队列的尾部,线程同时阻塞。
代码实现方式
自定义一个静态内部类 继承AbstractQueuedSynchronizer,然后使用模版方法重写指定的 方法等内容
独占式实现原理
获取实现原理
acquire(int arg)方法可以获取同步状态。其主要逻辑是: (1) 首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态。 (2) 如果同步状态获取失败,则构造同步节点并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部。 (3) 最后调用acquireQueued(Node node, int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
addWaiter(Node node)方法添加到尾部的实现逻辑
先通过使用compareAndSetTail(Node expect, Node update)方法来确保节点能够被线程安全添加。失败就通过 死循环 和 前面的CAS操作 继续设置 尾结点
acquireQueued(Node node, int arg)方法中只有前驱结点是头结点的才可以获取同步状态的 原因:
一:因为头结点是获取同步状态成功的结点,那么头结点释放同步 结点,需要自己判断下一个是不是自己来 过去同步状态了
二:维护FIFO的原则,自旋自行判断,避免线程因为中断而被唤醒,去获取同步状态
释放实现原理
1.同步器调用tryRelease(int arg)方法释放同步状态,1代表获取成功 2. 然后使用LockSupport唤醒头节点的后继节点。
共享式实现原理
定义:可以有多个线程同时获取同步状态
获取原理
tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。
释放实现原理
一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
独占式超时获取同步状态 略
基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask。
重入锁 ReentratLock
定义
已经获取同步状态的线程,再次获取锁不会阻塞当前的线程,可重复获取锁
获取的实现
判断当前独占的线程是不是自己,是的话给 state➕1
释放的实现
state状态➖1,知道等于0,才给独占 线程设置 为null
公平锁和非公平锁的区别
公平锁会判断 当前队列有没有头结点,有则入队列,不会抢资源。非公平锁则会通过CAS去抢同步状态的获取
非公平锁出现连续获取锁现象的原因
因为是 CAS获取同步状态,刚释放的线程获取到的 同步状态 的机率会非常的大
非公平设定成上面默认的原因
公平锁的线程上下文 切换 是 非公平锁的 很多倍。非公平锁 减少上下文的切换 开销小。吞吐 量更大,所以设定成默认的。
读写锁ReentrantReadWriteLock
定义
维护 一对儿 锁。写操作 时,不能读。避免出现脏数据。如果没有读写锁,只能通过 等待/通知机制机制来实现。写的时候使用synchronized来修饰。读写锁能够提供比排它锁更好的并发性和吞吐量。这里的并发性就是 如果是排他锁,那么永远只能一个线程执行,但是读写 分离后,读锁是共享锁,这点就比排他锁有很好的性能。
使用场景
假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。
读写锁的实现分析
读写状态的设计
一个 int变量,高16位表示读,低16位表示写
读写锁的定位
通过位运算实现。假设当前同步状态值为S,写状态: 等于S0x0000FFFF(将高16位全部抹去),读状态: 等于Sgt;gt;gt;16(无符号补0右移16位)。
读写锁 重入时的计算
读状态等于Sgt;gt;gt;16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1lt;lt;16),也就是S+0x00010000。
写锁的获取与释放
写锁是独占式的 支持锁重入。只有当读锁释放后,才能获取写锁。写锁获取成功后,读锁都阻塞,避免数据脏读。写锁的释放 和 ReentratLock一样,只有state为0时才释放成功
读锁的获取与释放
读锁是一个支持重进入的共享锁,
锁降级
使用的代码
1.先读锁获取和释放一下; 2.然后获取写锁,写锁中获取读锁。 3.然后写锁的释放,读锁的释放。
锁降级的意义/ 或者读锁获取的意义
1.如果一个业务场景是,先写操作后,要对写的内容进行读的一些事务。如果先释放写锁,再加读锁,这中间会有其他的线程获取写锁,就会造成脏数据的读取。所以 降锁可以很好的规避这个问题。
2.如果一些业务写操作很久,那么中间要进行一些读的业务请求,读是共享锁,其他的线程也可以获取。这样就提高了一边写一边读的并发性能。线程的操作也不会被别的线程中断,但如果不用锁降级,同上的道理,写操纵可能被其他的线程获取,中间就又出现ABA的问题。
不能锁升级
原因就是保证数据 的可见性。
LockSupport
释放资源问题: 不会释放锁资源
底层调用: 底层 调用的Unsafe下的native的 park方法,park()阻塞当前线程,park(Thread) 阻塞指定的线程
是否需声明异常: 不需要
线程状态: 在sychronized中,获取到锁的线程调用该方法进入waiting状态,其他线程都是bolcked状态。如果是ReentratLock锁,因为是重入锁,所有的线程都是waiting状态。
作用范围: 任意地方
唤醒操作:LockSupport.unpark() 可以在LockSupport.park()前面执行 唤醒后 一定继续执行后续的代码
Condition接口
Condition用法相关
释放资源问题: 会释放资源
底层调用: 底层使用的 是LockSupport.park()
是否需要声明异常: 需要
线程状态: waiting
作用 范围 : 需要在lock块儿中执行,以来lock对象的
唤醒操作: signal不能在前面 执行,唤醒后不一定继续执行代码,因为释放了锁资源,要重新竞争
获取方式: 获取一个Condition必须通过Lock的newCondition()方法。
实现方式
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。
节点的定义: 同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。
等待: 1. 当调用await()方法时,线程会释放锁,且把同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。 2.如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
通知:调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
并发容器
ConcurrentHashMap
JDK1.7版本
结构
由Segment数组结构组成,每个数组中都放置hashEntry数组。相当于每个segment数组中放置一个hashmap
每个Segment是一个重入锁。
初始化过程
初始化segment数组大小
ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor和concurrencyLevel等几个参数来初始化segment数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现的。
segment的数组长度: 就是根据concurrencyLevel来计算的,该值默认是16,所以segment数组的长度是大于等于concurrencyLevel最近 的2的n次幂的数
其中还要初始化段偏移量和段掩码,这两个主要是在最后定位时候用到的,段偏移量就是为了让高位参与定位。比如当前的长度是16,表示从左移4次,那么段偏移量就是32-4,这样在得到散列后hashcode值,就右移28位,这样高位就能参加到运算了,段掩码就是16-1的全是1的二进制数字
初始化每个segment里面的hashEntry数组大小
hashEntry的数组长度: 根据c=initialCapacity/segment数组长度计算,hashEntry的数组长度就是1或者大于等于c最近的2的N次幂的数。initialCapacity默认16,segment数组长度默认也是16,所以默认每个hashEntryapos;的长度就是1。
如何定位
对segment定位
对hashcode进行再散列,然后对该hash值右移 段偏移量的值,对高位进行与运算
再散列的目的:因为只要低位一样,无论高位是什么数,其散列值总是一样。通过散列算法,从而减少冲突。
对高位进行与运算的目的: 这说明散列运算后的值,高位更分散。
对hashEntry数组进行定位
这里直接通过上面计算的hash值通hashEntry数组长度减一进行与运算,这里没有再进行散列计算 ,所以 对hashEntry数组没有做很好的散列算法
get操作
1.先定位segment数组的下标,即对hashcode进行散列,然后对高位进行与运算
2.通过上面的hash值通hashEntry的length-1进行定位
3.get过程没有加锁操作,除非得到空值进行重读时才会加锁,避免重读时,该元素正好被加入。其次,不加锁是因为,公共变量都被volitale修饰,保证了可见性。
注意这里是为了满足 happens-before的原则,采用加锁来保证的。
put操作
1.判断是否需要扩容
2.定位 添加元素
插入操作是需要加锁的,对某个segment进行加锁
如何扩容
ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。
但是在计算容器中的元素个数的时候,是统计容器内所有的元素,是否超过阀值
在统计count时,为了高效,没有对put,move,clean等操作进行加锁,而是先统计两遍,然后比较是否发生变化,如果统计期间,发生了变化,就加锁重新 统计。
通过比较modcount的值,判断前后是否发生变化。
JDK1.8版本
结构
同hashmap一样,数组加链表,一定条件 下 链表会转换成红黑树
hash方法设计
(h gt;gt;gt; 16):将高比特位转移到低位 * (h ^ (h gt;gt;gt; 16)):将高比特位变成 0 * (h ^ (h gt;gt;gt; 16)) HASH_BITS:HASH_BITS是0x7fffffff,该步是为了消除最高位上的负符号 * hash的负在ConcurrentHashMap中有特殊意义表示在扩容或者是树节点
红黑树和链表互相转
红黑树将链表的查询时间效率从O(n) 提升到了log(n)
初始化过程
正常的初始化一个数组。
sizeCtl意义:该属性是table数组在初始化或者扩容时候的一个标识符 。
当值为负
-1表示正在初始化,-N表示 N-1个线程在扩容
当值为正
初始化成功后,sizeCtl就是0.75*length的临界值
如何定位
散列运算,高位与元算。最后同长度进行与元算定位。
get操作
1.计算hash值,定位到table的索引位置,在定位时使用U.getObjectVolatile()方法来保证元素的可见性,如果tab[i]正被锁住,那么CAS就会失败,失败之后就会不断的重试。这也保证了get在高并发情况下不会出错。
2.U.getObjectVolatile()如何保证可见性的:因为table是被vilatile修饰,对volatile域的写入操作happens-before于每一个后续对同一域的读操作。所以不管其他线程对table链表或树的修改,都对get读取可见。
3.定位到具体的槽后,判断第一个结点是否是要查找的结点(判断方法是先比较hash值,若相等则需要地址相等或者equals为true中的一个成立,则是要查找的结点),否则根据hash值是否为负数,将查找操作分派给相应的find函数。若是ForwardingNode,则用find函数转发到nextTable上查找;若是TreeBin结点,调用TreeBin的find函数,根据自身读写锁情况,去红黑树中查找。最后如果是普通结点,则遍历链表来寻找。
put操作
1.计算hash值
2.开始对table数组无限循环遍历
1.在添加数据之前还需要判断table数据是不是为空,如果是空需要先初始化容量,这算是一个设计点,通过懒加载提高空间利用率,在第一个数据添加到Map的时候才初始化空间;
2.判断新增数据是不是hash桶的第一个元素,如果是通过CAS操作直接添加;
3.再根据hash值判断,在插入数据如果正遇到有其它线程正在扩容,当前线程或加入到库容的任务中,帮助正在扩容的线程移动数据,共同加油 的思想在这里体现;
4.上面情况都不满足,说明是对链表进行元素添加。内置锁synchronized锁住了f,因为f是指定特定的tab[i]的,所以就锁住了整行链表,这个设计跟分段锁有异曲同工之妙,只是其他读取操作需要用cas来保证。
5.下面就是锁住桶的第一个元素后,开始判断进行添加,比如判断是否需要进行红黑树的转换 等。注意:当满足链表长度大于8时,如果数组长度lt;64只会触发扩容。所以 有且数组 长度gt;=64且链表长度=9时才开始转换红黑树 。
3.插入新数据完成后判断,如果map的容量是不是已经触发扩容的加载因子,如果是,则做扩容操作;
如何帮助扩容
细节略
如何扩容
addCount()方法:
1.主要是更新baseCount或者CounterCell,其中baseCount是单线程下元素 个数,当存在线程竞争时就用 CounterCell数组来保存多线程下每个线程更新的元素 个数
2.判断条件,如果满足扩容,就开始扩容。
transfer()方法:
核心思想就是多线程下,分桶扩容。
1.通过计算 CPU 核心数和 Map 数组的长度得到每个线程(CPU)要帮助处理多少个桶,并且这里每个线程处理都是平均的。默认每个线程处理 16 个桶。因此,如果长度是 16 的时候,扩容的时候只会有一个线程扩容。
2.初始化临时变量nextTable。将其在原有基础上扩容两倍。
3.一个for的死循环开始转移。多线程并发转移就是在这个死循环中,根据一个finishing变量来判断,该变量为 true 表示扩容结束,否则继续扩容。
4.for死循环下,进入一个while循环,目的是分配一个桶区间给线程,默认分配16个长度的数组。并且是倒着分配的。
5.while循环出来后,在一个if判断中,分别判断finishing和sizeCtl变量,当finishing是true,说明扩容结束 ,线程退出且sizeCtl-1。如果sizeCtl恢复到正数,说明也是扩容结束。然后检查所有的桶,防止遗漏。
6.如果没有完成任务,且 i 对应的槽位是空,尝试 CAS 插入占位符,让putVal方法的线程感知。
7.如果i对应的槽位不是空,且有了占位符,那么该线程跳过这个槽位,处理下一个槽位。
8.如果以上都是不是,说明这个槽位有一个实际的值。开始加锁同步处理这个节点。
9.到这里,都还没有对桶内数据进行转移,只是计算了下标和处理区间,然后一些完成状态判断。同时,如果对应下标内没有数据或已经被占位了,就跳过了。
10.处理每个桶的行为都是同步的,此处加了synchronized关键字。防止putVal的时候向链表插入数据。
11.如果这个桶是链表,那么就将这个链表根据length取于拆成两份,取于结果是 0 的放在新表的低位,取于结果是 1 放在新表的高位。
12.如果这个桶是红黑数,那么也拆成 2 份,方式和链表的方式一样,然后,判断拆分过的树的节点数量,如果数量小于等于 6,改造成链表。反之,继续使用红黑树结构。
13.到这里,就完成了一个桶从旧表转移到新表的过程。
CAS操作解析
关于U.getObjectVolatile(tab, ((long)i lt;lt; ASHIFT) + ABASE)的 含义?
首先CAS操作就是跟内存中的元素去比较。所以要获得内存地址
ASHIFT是指tab[i]中第i个元素在相对于数组第一个元素的偏移量,而ABASE就算第一数组的内存素的偏移地址,所以呢,((long)i lt;lt; ASHIFT) + ABASE就算i最后的地址
ConcurrentHashmap自己定义了一堆关于自己本身的内存偏移量的变量。
sizeCtl:获取ConcurrentHashMap这个对象字段sizeCtl在内存中的偏移量
ASHIFT:ASHIFT是指tab[i]中第i个元素在相对于数组第一个元素的偏移量
ABASE:可以获取数组第一个元素的偏移地址
ConcurrentLinkedQueue
通过CAS操作,实现的非阻塞的一个队列
结构
ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为
入队列
入队要做两件事情:第一是将入队节点设置成当前队列尾节点的下一个节点;第二是更新tail节点
使用一个死循环来保证获取到最新的tail节点,第一层的死循环获取tail节点,并在for循环前面加上一个退出的标识符,在第二个循环中,只循环两次,目的找真正的尾节点,因为有时候tail不是真正的尾结点,如果找到真正的尾结点的next是空使用CAS操作来添加,如果不是,说明有其它的线程添加了元素,则跳出两层的循环,重新获取tail结点。
tail结点有时候不是真正的尾结点的原因:定位tail时,为了提升入队的效率,tail并不是每次都使用CAS来更新,一般如果tail结点距离大于 1时,才做更新。
出队列
出队要做的事情:1.找到head结点,2.修改head结点的指向
也是通过一个死循环,直到找到一个head结点的value值是不为空的,将其使用CAS来修改head的next指向。
JAVA中的阻塞队列
什么是阻塞队列?
1.阻塞的插入:当一个线程插入数据时,如果队列是满的 ,线程就会一直阻塞在哪里,直到队列不满时。
2.阻塞的移除:当一个线程要移除队列的头结点元素,但是队列是空的,则该线程被阻塞,直到队列有数据 。
应用场景?
主要应用在消费者和生产者中。
JDK提供哪些?
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
其他 略。
Fork/Join框架
什么是Fork/Join框架?(基本的原理)
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
工作窃取算法
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。那么,为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。比如A线程负责处理A队列里的任务。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
总结:大任务分成小任务,一堆一堆 的小任务 分别放到一个不同的队列,一个线程负责一个队列,如果先完成的线程,就从其他 队列中获取任务。
优点:充分利用线程进行并行计算,减少了线程间的竞争。
缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗了更多的系统资源,比如创建多个线程和多个双端队列。
Fork/Join框架的设计
1.分割任务:需要有一个fork类来把大任务分割成子任务
2.将分割的任务放到双端队列中,然后把结果放到一个单独的队列,再启动一个线程,单独进行合并
Fork/Join使用两个类来完成以上两件事情。
1.ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了以下两个子类: ❑RecursiveAction:用于没有返回结果的任务。❑ RecursiveTask:用于有返回结果的任务。
2.ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。
Fork/Join框架的实际应用
略,自己也没做过
JAVA中的并发工具类
等待多线程完成的CountDownLatch
作用:CountDownLatch允许一个或多个线程等待其他线程完成操作。
应用场景:我们需要解析一个Excel里多个sheet的数据,此时可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。在这个需求中,要实现主线程等待所有线程完成sheet的解析操作
上面的解决方案:
1.使用join
每个任务线程都自己调用join,另一部分代码死循环判断有没有join的线程 存活。有则 一直等待,没有则打印结果。
2.使用CountDownLatch
传入一个int的参数N,当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。
同步屏障CyclicBarrier
作用:字面意思循环屏障,意思一个线程执行到一个同步点就阻塞,其他的线程也一样,直到所有线程都阻塞,该屏障才放开,线程继续执行代码。
方法使用:
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnablebarrier-Action),用于在线程到达屏障时,优先执行barrierAction。
CyclicBarrier和CountDownLatch的区别
CyclicBarrier比CountDownLatch的功能更加的丰富
CyclicBarrier的计数器可以重置,比如程序出错,或者按照某种设定可以重新让线程执行,但是CountDownLatch的计数器只能用一次
CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。
控制并发线程数的Semaphore
作用:Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
用法:在你想保护的特定资源的代码的前后,分别使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。
应用场景:比如限制数据库的连接数量,也可以用来保证线程顺序执行
线程间交换数据的Exchanger
作用:Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
用法:两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
应用场景:遗传算法就可以使用该对象来交换数据,或者用于比较的工作,比如 核对什么账目
java线程池
线程池的好处
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
线程池的实现原理
1.判断线程池中现有存活的线程,是否有空闲
2.如果已创建的核心线程都有任务,则 将任务放到队列中
3.如果队列满了,就判断当前 线程池的数量是否 达到最大值,没有的话就创建新的线程了执行
4,如果达到最大的线程池数量,就按照设定的饱和策略来处理。
核心线程数可以销毁吗?
线程池的使用
线程池的创建
我们可以通过ThreadPoolExecutor来创建一个线程池。
创建线程池中涉及的参数
1.corePoolSize(线程池的基本大小):先创建出指定的核心线程,在核心线程数不够时,即便有空闲线程处理任务,此时仍然是创建新的线程,直到核心线程创建完成,就不会创建了。
2.maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。
3.keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。
4.RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。
5.)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
❑ ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
❑ LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
❑ SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
❑ PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
饱和策略有哪几种?
❑ AbortPolicy:直接抛出异常。❑ CallerRunsPolicy:只用调用者所在线程来运行任务。❑ DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。❑ DiscardPolicy:不处理,丢弃掉。
向线程池提交任务
可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。
excute:execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(longtimeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。
关闭原理
它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
区别
shutdown:线程池的状态设置称SHUTDOWN,关闭没有任务的那些空闲线程。该方法用于 需要等待线程都执行完成的任务。
shutdownNow:线程池的状态设置称STOP,所有的线程都关闭,不管该线程是不是在执行
判断线程池是否都关闭:使用isTerminaed方法
如何合理的配置线程池:
首先要根据任务特性来配置
❑ 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
CPU密集型任务:任务以内存中的计算为主。避免线程上下午切换的成本。一般CPU是N核,就开N+1个线程。
IO密集型任务:尽量多开启一些线程并发做IO操作,因为在IO过程中,CPU几乎是闲置的。一般可能是2*cpu个数,当然应该根据下面的标准公式计算好一些
❑ 任务的优先级:高、中和低。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
❑ 任务的执行时间:长、中和短。
根据任务的执行时间,设置核心线程的存活时间
❑ 最好使用有界队列
有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题
线程池的监控
目的:方便在出现问题时,可以根据线程池的使用状况快速定位问题。
监控线程池的一些属性
❑ getActiveCount:获取活动的线程数
❑ taskCount:线程池需要执行的任务数量。
❑ largestPoolSize:线程池里曾经创建过的最大线程数量。
过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。
Executor框架
Java的线程既是工作单元,也是执行机制。从JDK 5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
Executor框架的两级调度模型
应用程序通过Executor框架控制上层的调度;而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。
1. Executor框架的结构
工作任务:被执行任务需要实现的接口:Runnable接口或Callable接口。
执行机制:任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
异步计算结果:包括接口Future和实现Future接口的FutureTask类。
2. Executor框架的成员
执行机制:实现ExcutorService接口的俩个类
ThreadPoolExcutor
1)FixedThreadPool:创建使用固定线程数,FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。
2)SingleThreadExecutor。创建使用单个线程的,SingleThreadExecutor适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。
3)CachedThreadPool。创建一个会根据需要创建新线程。CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。负载大就出问题了,因为队列是无界的。容易内存溢出。
ScheduledThreadPoolExcutors
1)ScheduledThreadPoolExecutor。可以创建指定个数的线程,且可以创建延时任务或者周期性任务。延时任务就是指定时间,等 过了该段时间后,任务开始执行;周期性任务就是在指定的时间内执行任务。该线程池使用需要延时任务或者的周期性任务的业务场景。
2)SingleThreadScheduledExecutor:创建一个周期性的线程,适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。
异步计算结果Future类
Future接口和实现Future接口的FutureTask类用来表示异步计算的结果。当我们把Runnable接口或Callable接口的实现类提交(submit)给ThreadPoolExecutor或ScheduledThreadPoolExecutor时,ThreadPoolExecutor或ScheduledThreadPoolExecutor会 向我们返回一个FutureTask对象。
FutureTask的两种使用方法
1.FutureTask实现了Excutors接口,所以任务交给Excutors,然后任务执行submit后返回一个Future或者FutureTask,然后 调用get方法获取结果。注意任务是实现Callable接口。
2.FutureTask也实现了Runnable接口,所以可以自己使用run方法进行执行。调用get方法获取结果。
使用FutureTask顺序执行任务:一堆任务放到一个缓存池中,从缓存池中获取任务,首先任务是实现Callable接口的,其次FutureTask可以自己调run方法,然后遍历缓存池,调用FutureTask的get方法,因为get方法是阻塞类型的。所以当前 线程 执行不完,无法从缓存池中获取。
子主题
0 条评论
下一页