一。Future接口
Future模式始于JDK5,比较常见的用法就是接受线程池的submit()方法的结果:
Future<String> future = executor.submit(xxx);
xxx可以说就是Callable接口或者Runnable接口的实现类,提交xxx后,xxx会被包装成FutureTask类型,然后推到线程池执行run(),我们通过Future类型的对象拿到这个正在执行的异步任务,FutureTask其实也是Future接口的实现类之一,Future接口提供了以下常用API操作任务:
- boolean isDone():执行结束(完成/取消/异常)返回true
- boolean isCancelled():任务完成前被取消返回true
- V get(): 获取结果,若无结果会阻塞至异步计算完成
- V get(long timeOut, TimeUnit unit):获取结果,可阻塞等待指定的时间,超时返回null
- boolean cancel(boolean mayInterruptRunning):取消任务,未开始或已完成返回false,参数表示是否中断执行中的线程
通过这些API就能基本的去操作一个异步任务,但也有一定的缺陷,比如获取值时调用get()方法可能会阻塞线程,这显然不符合异步的需要,所以JDK8提供了基于Future的CompletableFuture类
二。FutureTask类
FutureTask实现了Future接口,所以也可以操作异步任务。同时还实现了runnable接口,所以被用于包装Callable接口或者Runnable接口的实现,给线程池执行。
三。CompletableFuture类
CompletableFuture类是JDK8对Future接口的升级,比如解决了get的阻塞问题、支持函数式编程、提供了类似ajax的回调机制
常用api:CompletableFuture 使用详解
太多了就不一一写了,就写写看完这些api后的一些比较重要的总结吧:
1.方法以async结尾的都是异步执行,如果不指定线程池,由ForkJoinPool的common池执行,指定了则用自定义的线程池。
2.runasync无返回值,supplyasync有返回值
3.带apply的方法有返回值,带accept的方法无返回值,这点从它们接受的函数式接口类型也可看出。
4.带handle的方法,如果调用者执行出现异常,也会执行handle的逻辑,但是thenapply出现异常不会执行自己。handle接受的是函数型接口,可以转换值,whencomplete接受的是消费型接口,不能转换值。
5.thenrun不关心调用者的执行结果,thenapply需要接受结果再消费
6.thenCompose 可以用于组合多个CompletableFuture,将前一个任务的返回结果作为下一个任务的参数,它们之间存在着先后顺序。thenCombine会在两个任务都执行完成后,把两个任务的结果合并,两个任务是并行执行的,它们之间并没有先后依赖顺序,两个任务中只要有一个执行异常,则将该异常信息作为指定任务的执行结果。
四。JMM内存模型
简介
JMM内存模型控制了Java线程之间的通信,决定了一个线程对主内存中共享变量的修改何时对其他线程可见。
在JMM内存模型中,线程之间的共享变量存储在主内存,每个线程有自己的本地内存。
约定
1.线程加锁前要从主存中读取共享变量的最新值,解锁之前要把修改后的共享变量写回主存。
2.所有线程操作的都是存储在自己本地内存中共享变量的副本,不直接操作主存中的变量,所以线程之间的通信必须经过主内存。
线程通信
如果线程A想要和线程B通信,需要有如下步骤
1.线程A将修改后的共享变量写回主存
2.线程B到主存中读取最新的共享变量到主内存。
八大操作
- lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态
- read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中
- use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作
- assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作
- store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用
- write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
- unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。‘
- 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)
happens-before语义
前一个操作的顺序在第二个操作之前,且结果对第二个操作可见
五。volitaile
轻量级的Synchronized关键字,有如下特性:
1.保证一个线程修改共享变量后,对其他线程可见,但不保证操作的原子性,即修改可见但是读取后的操作可能中断。
2.禁止指令重排序,防止线程拿到未初始化完全的对象,未初始化完全的对象的属性只有默认值,使用的时候会出问题。
禁止重排序
对象在初始化的时候,可能有如下三条指令:
1).对象的属性开辟内存空间,赋默认值,比如int的0
2).为对象的属性执行真正的赋值语句,比如int num=10
3).将赋值完成的对象空间的地址赋给对象引用
如果指令重排序了,3可能会和2交换位置。指令刚执行完3,一个线程通过引用访问了对象空间中只有默认值的属性,就可能导致执行出错。
变量可见性
根据happens-before语义,被volitaile修饰的变量,每个线程对它的读操作,一定能看到其他任意线程对它的最后的写入。
如果一个变量被volitaile修饰,JMM有如下规定:
1.当写一个volitaile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新回主内存,更新主内存中共享变量的值。
2.当读一个volatile变量时,JMM会先把该线程本地内存中的该变量值设置为无效,重新到主内存中读取。
总线嗅探
总线嗅探机制其实就是一个监听器
- CPU1读取数据a=1,CPU1的缓存中都有数据a的副本,该缓存行置为(E)状态
- CPU2也执行读取操作,同样CPU2也有数据a=1的副本,此时总线嗅探到CPU1也有该数据,则CPU1、CPU2两个缓存行都置为(S)状态
- CPU1修改数据a=2,CPU1的缓存以及主内存a=2,同时CPU1的缓存行置为(S)状态,总线发出通知,CPU2的缓存行置为(I)状态
- CPU2再次读取a,虽然CPU2在缓存中命中数据a=1,但是发现状态为(I),因此直接丢弃该数据,去主内存获取最新数据
当我们使用volatile关键字修饰某个变量之后,就相当于告诉CPU:我这个变量需要使用MESI和总线嗅探机制处理。从而也就保证了可见性。
MESI缓存一致性协议
MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
- Modify(修改):当缓存行中的数据被修改时,该缓存行置为M状态
- Exclusive(独占):当只有一个缓存行使用某个数据时,置为E状态
- Shared(共享):当其他CPU中也读取某数据到缓存行时,所有持有该数据的缓存行置为S状态
- Invalid(无效):当某个缓存行数据修改时,其他持有该数据的缓存行置为I状态
它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
而这其中,监听和通知又基于总线嗅探机制来完成。
总结
所以说,volatile保证可见性的机制就是:在写操作完成后,通过总线通知其他线程修改自己本地内存中共享变量副本值的缓存行,其他线程再次读取这个副本发现缓存行被修改了,就重新到主存中读取最新值。这也对应了volatile的happens-before,读取总在最后一次写入之后。