Java高并发系列——检视阅读
参考
java高并发系列
liaoxuefeng Java教程 CompletableFuture
AQS原理没讲,需要找资料补充。
JUC中常见的集合原来没讲,比如ConcurrentHashMap最常用的,后面的都很泛,没有深入,虎头蛇尾。
阻塞队列讲得不够深入。
并发概念词
同步(Synchronous)和异步(Asynchronous)
同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。
举例:
拿泡泡面来说,我们把整个泡泡面的过程分3个步骤:
- 烧水
- 泡面加调味加蛋
- 倒开水泡面
如果我们泡泡面的时候是按照这三个步骤,等开水开了再加调味加蛋,最后倒开水泡面,这时候就是同步步骤;而如果我们烧开水的同时把泡面加调味加蛋准备好,就可以省去烧水的同步等待时间,这就是异步。
并发(Concurrency)和并行(Parallelism)
并发和并行是两个非常容易被混淆的概念。他们都可以表示两个或者多个任务一起执行,但是侧重点有所不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的(等待阻塞等),而并行是真正意义上的“同时执行” 。
举例:
大家排队在一个咖啡机上接咖啡,交替执行,是并发;两台咖啡机上面接咖啡,是并行。
并发说的是在一个时间段内,多件事情在这个时间段内交替执行。
并行说的是多件事情在同一个时刻同时发生。
如果系统内只有一个CPU,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的,毕竟一个CPU一次只能执行一条指令,在这种情况下多进程或者多线程就是并发的,而不是并行的(操作系统会不停地切换多任务) 。
临界区
临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用,但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。
阻塞(Blocking)和非阻塞(Non-Blocking)
阻塞和非阻塞通常用来形容很多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。 非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行。
死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)
死锁、饥饿和活锁都属于多线程的活跃性问题 。
死锁:两个线程都持有独占的资源(锁),同时又互相尝试获取对方独占的资源(锁),这时候双方都没有释放自己的独占资源,导致永远也获取不到阻塞等待下去。
饥饿是指某一个或者多个线程因为种种原因无法获得所要的资源,导致一直无法执行。一种比如它的优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。另一种如某一个线程一直占着关键资源不放(例子:单线程池里submit一个线程任务,而该线程又往该单线程池里submit一个新的任务并等待结果返回,因为线程池是单线程池,所以便一种套娃着),导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。与死锁相比,饥饿还是有可能在未来一段时间内解决的(比如,高优先级的线程已经完成任务,不再疯狂执行)。
活锁:当两个线程都秉承着“谦让”的原则(导致死循环),主动将资源释放给他人使用,那么就会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行。这种情况就是活锁。
扩展
通过jstack查看到死锁信息
1、使用jps找到执行代码的进程ID,启动类名为DeadLockTest(main函数所在类)的进程ID为11084
jps
2、通过jstack命令找到java进程中死锁的线程锁信息,执行jstack -l 11084
jstack -l 11084
最后输出:
===================================================
"thread2":
at com.self.current.DeadLockTest$SynAddRunalbe.run(DeadLockTest.java:331)
- waiting to lock <0x000000076b77e048> (a com.self.current.DeadLockTest$Obj1)
- locked <0x000000076b780358> (a com.self.current.DeadLockTest$Obj2)
at java.lang.Thread.run(Thread.java:748)
"thread1":
at com.self.current.DeadLockTest$SynAddRunalbe.run(DeadLockTest.java:282)
- waiting to lock <0x000000076b780358> (a com.self.current.DeadLockTest$Obj2)
- locked <0x000000076b77e048> (a com.self.current.DeadLockTest$Obj1)
at java.lang.Thread.run(Thread.java:748)
并发级别
由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,我们可以把并发的级别分为阻塞、无饥饿、无障碍、无锁、无等待5种。
阻塞——悲观锁
一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字或者重入锁时,我们得到的就是阻塞的线程。
synchronize关键字和重入锁都试图在执行后续代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需资源为止。
例子:synchronize或ReentrantLock。
无饥饿(Starvation-Free)——公平与非公平锁
表示非公平锁与公平锁两种情况 。如果线程之间是有优先级的,那么线程调度的时候总是会倾向于先满足高优先级的线程。
对于非公平锁来说,系统允许高优先级的线程插队。这样有可能导致低优先级线程产生饥饿。但如果锁是公平的,按照先来后到的规则,那么饥饿就不会产生 。
例子:ReentrantLock 默认采用非公平锁,除非在构造方法中传入参数 true 。
//默认
public ReentrantLock() {
sync = new NonfairSync();
}
//传入true or false
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
无障碍(Obstruction-Free)——乐观锁CAS
无障碍是一种最弱的非阻塞调度。两个线程如果无障碍地执行,那么不会因为临界区的问题导致一方被挂起。
对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所做的修改进行回滚,确保数据安全。但如果没有数据竞争发生,那么线程就可以顺利完成自己的工作,走出临界区。
无障碍的多线程程序并不一定能顺畅运行。因为当临界区中存在严重的冲突时,所有的线程可能都会不断地回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行。所以,一种可行的无障碍实现可以依赖一个"一致性标记"来实现。
数据库中乐观锁(通过版本号或者时间戳实现)。表中需要一个字段version(版本号),每次更新数据version+1,更新的时候将版本号作为条件进行更新,根据更新影响的行数判断更新是否成功,伪代码如下:
1.查询数据,此时版本号为w_v
2.打开事务
3.做一些业务操作
//此行会返回影响的行数c
4.update t set version = version+1 where id = 记录id and version = w_v;
5.if(c>0){ //提交事务 }else{ //回滚事务 }
多个线程更新同一条数据的时候,数据库会对当前数据加锁,同一时刻只有一个线程可以执行更新语句。
无锁(Lock-Free)
无锁的并发都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。 (注意有限步)
在无锁的调用中,一个典型的特点是可能会包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果没有冲突,修改成功,那么程序退出,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程是可以胜出的,不至于全军覆没。至于临界区中竞争失败的线程,他们必须不断重试,直到自己获胜。如果运气很不好,总是尝试不成功,则会出现类似饥饿的先写,线程会停止。(并发量太大时会出现饥饿,这时候有必要改成阻塞锁)
下面就是一段无锁的示意代码,如果修改不成功,那么循环永远不会停止。
while(!atomicVar.compareAndSet(localVar, localVar+1)){ localVal = atomicVar.get();}
无等待——读写锁
无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步扩展。无等待要求所有线程都必须在有限步内完成,这样不会引起饥饿问题。如果限制这个步骤的上限,对循环次数的限制不同。分为为
- 有界无等待
- 线程数无关的无等待。
一种典型的无等待结果就是RCU(Read Copy Update)。它的基本思想是,对数据的读可以不加控制。因此,所有的读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但在写数据的时候,先获取原始数据的副本,接着只修改副本数据(这就是为什么读可以不加控制),修改完成后,在合适的时机回写数据。
并行的两个重要定律
为什么要使用并行程序 ?
第一,为了获得更好的性能;
第二,由于业务模型的需要,确实需要多个执行实体。
关于并行程序对性能的提高定律有二,Amdahl(阿姆达尔)定律和Gustafson(古斯塔夫森 )定律。
加速比定义:加速比 = 优化前系统耗时 / 优化后系统耗时
根据Amdahl定律,使用多核CPU对系统进行优化,优化的效果取决于CPU的数量,以及系统中串行化程序的比例。CPU数量越多,串行化比例越低,则优化效果越好。仅提高CPU数量而不降低程序的串行化比例,也无法提高系统的性能。
根据Gustafson定律,我们可以更容易地发现,如果串行化比例很小,并行化比例很大,那么加速比就是处理器的个数。只要不断地累加处理器,就能获得更快的速度。
总结
Gustafson定律和Amdahl定律的角度不同
Amdahl强调:当串行换比例一定时,加速比是有上限的,不管你堆叠多少个CPU参与计算,都不能突破这个上限。
Gustafson定律强调:如果可被并行化的代码所占比例足够大,那么加速比就能随着CPU的数量线性增长。
总的来说,提升性能的方法:想办法提升系统并行的比例(减少串行比例),同时增加CPU数量。
附图:
Amdahl公式的推倒过程
其中n表示处理器个数,T表示时间,T1表示优化前耗时(也就是只有1个处理器时的耗时),Tn表示使用n个处理器优化后的耗时。F是程序中只能串行执行的比例。
Gustafson公式的推倒过程
并发编程中JMM相关的一些概念
JMM(JAVA Memory Model:Java内存模型),由于并发程序要比串行程序复杂很多,其中一个重要原因是并发程序中数据访问一致性和安全性将会受到严重挑战。如何保证一个线程可以看到正确的数据呢?
Q:如何保证一个线程可以看到正确的数据呢?
A:通过Java内存模型管理,JMM关键技术点都是围绕着多线程的原子性、可见性、有序性来建立的 。
原子性
原子性是指操作是不可分的,要么全部一起执行,要么不执行。java中实现原子操作的方法大致有2种:锁机制、无锁CAS机制。
可见性
可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可见的。
看一下java线程内存模型及规则:
- 我们定义的所有变量都储存在 主内存中。
- 每个线程都有自己 独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作都必须在自己的工作内存中进行,不能直接从主内存中读写(不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行。(同级不能相互访问)
例子:线程需要修改一个共享变量X,需要先把X从主内存复制一份到线程的工作内存,在自己的工作内存中修改完毕之后,再从工作内存中回写到主内存。如果线程对变量的操作没有刷写回主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个线程的变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。
共享变量可见性的实现原理:
线程A对共享变量的修改要被线程B及时看到的话,需要进过以下2个步骤:
1.线程A在自己的工作内存中修改变量之后,需要将变量的值刷新到主内存中 。
2.线程B要把主内存中变量的值更新到工作内存中。
关于线程可见性的控制,可以使用volatile、synchronized、锁来实现。
有序性
有序性指的是程序按照代码的先后顺序执行。这是因为为了性能优化,编译器和处理器会进行指令重排序,有时候会改变程序语句的先后顺序。
例子:
在单例模式的实现上有一种双重检验锁定的方式,因为指令重排导致获取并发时获取到的单例可能是未正确初始化的单例。代码如下:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
我们先看 instance=newSingleton();
未被编译器优化的操作:
- 指令1:分配一款内存M
- 指令2:在内存M上初始化Singleton对象
- 指令3:将M的地址赋值给instance变量
编译器优化后的操作指令:
- 指令1:分配一块内存M
- 指令2:将M的地址赋值给instance变量
- 指令3:在内存M上初始化Singleton对象
现在有2个线程,刚好执行的代码被编译器优化过,过程如下:
最终线程B获取的instance是没有初始化的,此时去使用instance可能会产生一些意想不到的错误。
可以使用volatile修饰变量或者换成采用静态内部内的方式实现单例。
深入理解进程和线程
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程具有的特征:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进行一起并发执行
- 独立性:进程是系统进行资源分配和调度的一个独立单位
- 结构性:进程由程序,数据和进程控制块三部分组成
线程
线程是轻量级的进程,是程序执行的最小单元,使用多线程而不是多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
我们用一张图来看一下线程的状态图:
Java中线程的状态分为6种 ,在java.lang.Thread中的State枚举中有定义,如:
public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;}
Java线程的6种状态及切换
1. 初始(NEW):表示刚刚创建的线程,但还没有调用start()方法。
2. 运行(RUNNABLE):运行状态.Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):阻塞状态,表示线程阻塞于锁。当线程在执行的过程中遇到了synchronized同步块,但这个同步块被其他线程已获取还未释放时,当前线程将进入阻塞状态,会暂停执行,直到获取到锁。当线程获取到锁之后,又会进入到运行状态(RUNNABLE)(维护在同步队列中)
4. 等待(WAITING):等待状态。进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。和TIMED_WAITING都表示等待状态,区别是WAITING会进入一个无时间限制的等,而TIMED_WAITING会进入一个有限的时间等待,那么等待的线程究竟在等什么呢?一般来说,WAITING的线程正式在等待一些特殊的事件,比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到期望的事件,线程就会再次进入RUNNABLE运行状态。(维护在等待队列中)
5. 超时等待(TIMED_WAITING):超时等待状态。该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):结束状态,表示该线程已经执行完毕。
几个方法的比较
Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。
线程的状态图
进程与线程的一个简单解释
计算机的核心是CPU,整个操作系统就像一座工厂,时刻在运行 。进程就好比工厂的车间 ,它代表CPU所能处理的单个任务 。线程就好比车间里的工人。一个进程可以包括多个线程。 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。 每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。 这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。
操作系统的设计,因此可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
疑问:
Q:thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
调用其他线程的thread.join()方法,当前线程不会释放已经持有的对象锁,那如果进入了BLOCKED状态时会释放对象锁么?
线程的基本操作
新建线程
start方法是启动一个线程,run方法只会在当前线程中串行的执行run方法中的代码。
我们可以通过继承Thread类,然后重写run方法,来自定义一个线程。但考虑java是单继承的,从扩展性上来说,我们实现一个接口来自定义一个线程更好一些,java中刚好提供了Runnable接口来自定义一个线程。实现Runnable接口是比较常见的做法,也是推荐的做法。
终止线程——stop()方法已废弃
stop方法为何会被废弃而不推荐使用?stop方法过于暴力,强制把正在执行的方法停止了。
大家是否遇到过这样的场景:听着歌写着代码突然断电了。线程正在运行过程中,被强制结束了,可能会导致一些意想不到的后果。可以给大家发送一个通知,告诉大家保存一下手头的工作,将电脑关闭。
线程中断——interrupt()正确的中断线程方法
线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出了!至于目标线程接收到通知之后如何处理,则完全由目标线程自己决定,这点很重要,如果中断后,线程立即无条件退出,我们又会到stop方法的老问题。
Thread提供了3个与线程中断有关的方法,这3个方法容易混淆,大家注意下:
public void interrupt() //中断线程
public boolean isInterrupted() //判断线程是否被中断
public static boolean interrupted() //判断线程是否被中断,并清除当前中断状态
interrupt()方法是一个实例方法,它通知目标线程中断,也就是设置中断标志位为true,中断标志位表示当前线程已经被中断了。
isInterrupted()方法也是一个实例方法,它判断当前线程是否被中断(通过检查中断标志位)。
interrupted()是一个静态方法,返回boolean类型,也是用来判断当前线程是否被中断,但是同时会清除当前线程的中断标志位的状态。
Q:通过变量控制和线程自带的interrupt方法来中断线程有什么区别呢?
A:如果一个线程调用了sleep方法,一直处于休眠状态,通过变量控制,是不能中断线程么,因为此时线程处于睡眠状态无法执行变量控制语句,此时只能使用线程提供的interrupt方法来中断线程了。
实例:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
//sleep方法由于中断而抛出异常之后,线程的中断标志会被清除(置为false),所以在异常中需要执行this.interrupt()方法,将中断标志位置为true
this.interrupt();
System.out.println("exception:"+ e.getMessage());
}
System.out.println(Thread.currentThread().getName() + " in the end");
break;
}
}
};
t1.setName("interrupt thread");
t1.start();
TimeUnit.SECONDS.sleep(1);
//调用interrupt()方法之后,线程的sleep方法将会抛出 InterruptedException: sleep interrupted异常。
t1.interrupt();
}
错误写法:
注意:sleep方法由于中断而抛出异常之后,线程的中断标志会被清除(置为false),所以在异常中需要执行this.interrupt()方法,将中断标志位置为true
等待(wait)和通知(notify)
为了支持多线程之间的协作,JDK提供了两个非常重要的方法:等待wait()方法和通知notify()方法。这2个方法并不是在Thread类中的,而是在Object类中定义的。这意味着所有的对象都可以调用者两个方法。
(即只有这个对象是被当成锁来作为多线程之间的协作对象,那么在同步代码块中,线程之间就是通过等待wait()方法和通知notify()方法协作。)
public final void wait() throws InterruptedException;
public final native void notify();
如果一个线程调用了object.wait()方法,那么它就会进出object对象的等待队列。这个队列中,可能会有多个线程,因为系统可能运行多个线程同时等待某一个对象。当object.notify()方法被调用时,它就会从这个队列中随机选择一个线程,并将其唤醒。这里希望大家注意一下,这个选择是不公平的,并不是先等待线程就会优先被选择,这个选择完全是随机的。 nofiyAll()方法,它和notify()方法的功能类似,不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。
这里强调一点,Object.wait()方法并不能随便调用。它必须包含在对应的synchronize语句块中,无论是wait()方法或者notify()方法都需要首先获取目标独享的一个监视器。
等待wait()方法和通知notify()方法工作过程:
wait()方法和nofiy()方法的工作流程细节:
图中其中T1和T2表示两个线程。T1在正确执行wait()方法前,必须获得object对象的监视器。而wait()方法在执行后,会释放这个监视器。这样做的目的是使其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行。
线程T2在notify()方法调用前,也必须获得object对象的监视器。所幸,此时T1已经释放了这个监视器,因此,T2可以顺利获得object对象的监视器。接着,T2执行了notify()方法尝试唤醒一个等待线程,这里假设唤醒了T1。T1在被唤醒后,要做的第一件事并不是执行后续代码,而是要尝试重新获得object对象的监视器,而这个监视器也正是T1在wait()方法执行前所持有的那个。如果暂时无法获得,则T1还必须等待这个监视器。当监视器顺利获得后,T1才可以在真正意义上继续执行。
注意:Object.wait()方法和Thread.sleeep()方法都可以让现场等待若干时间。除wait()方法可以被唤醒外,另外一个主要的区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放锁。
示例:
public class WaitNotifyTest {
public static Object lock = new Object();
public static void main(String[] args) {
new T1("Thread-1").start();
new T2("Thread-2").start();
}
static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
synchronized (lock) {
System.out.println(this.getName() + " start");
try {
System.out.println(this.getName() + " wait");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName() + " end");
}
}
}
static class T2 extends Thread {
public T2(String name) {
super(name);
}
@Override
public void run() {
synchronized (lock) {
System.out.println(this.getName() + " start");
System.out.println(this.getName() + " notify");
lock.notify();
System.out.println(this.getName() + " end");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName() + " end,2 second later");
}
}
}
}
输出:
Thread-1 start
Thread-1 wait
Thread-2 start
Thread-2 notify
Thread-2 end
Thread-2 end,2 second later
Thread-1 end
注意下打印结果,T2调用notify方法之后,T1并不能立即继续执行,而是要等待T2释放objec投递锁之后,T1重新成功获取锁后,才能继续执行。因此最后2行日志相差了2秒(因为T2调用notify方法后休眠了2秒)。
可以这么理解,obj对象上有2个队列,q1:等待队列,q2:准备获取锁的队列;
挂起(suspend)和继续执行(resume)线程——方法已废弃
Thread类中还有2个方法,即线程挂起(suspend)和继续执行(resume),这2个操作是一对相反的操作,被挂起的线程,必须要等到resume()方法操作后,才能继续执行。系统中已经标注着2个方法过时了,不推荐使用。
系统不推荐使用suspend()方法去挂起线程是因为suspend()方法导致线程暂停的同时,并不会释放任何锁资源。此时,其他任何线程想要访问被它占用的锁时,都会被牵连,导致无法正常运行(如图2.7所示)。直到在对应的线程上进行了resume()方法操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是,如果resume()方法操作意外地在suspend()方法前就被执行了,那么被挂起的线程可能很难有机会被继续执行了。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它线程的状态上看,居然还是Runnable状态,这也会影响我们对系统当前状态的判断。
等待线程结束(join)和谦让(yeild)
很多时候,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖的线程执行完毕,才能继续执行。jdk提供了join()操作来实现等待线程结束。
//表示无限等待,当前线程会一直等待,直到目标线程执行完毕
public final void join() throws InterruptedException;
//millis参数用于指定等待时间,如果超过了给定的时间目标线程还在执行,当前线程也会停止等待,而继续往下执行。
public final synchronized void join(long millis) throws InterruptedException;
例子:线程T1需要等待T2、T3完成之后才能继续执行,那么在T1线程中需要分别调用T2和T3的join()方法。
示例:
public class JoinTest {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("T1");
t1.start();
long start = System.currentTimeMillis();
t1.join();
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " end .user time ="+(end-start)+" ,get num=" + num);
}
static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + " start");
for (int i = 0; i < 9; i++) {
num++;
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName() + " end");
}
}
}
输出:
T1 start
T1 end
main end .user time =3003 ,get num=9
Thread.yield()方法。是屈服,放弃,谦让的意思。
这是一个静态方法,一旦执行,它会让当前线程让出CPU,但需要注意的是,让出CPU并不是说不让当前线程执行了,当前线程在出让CPU后,还会进行CPU资源的争夺,但是能否再抢到CPU的执行权就不一定了。因此,对Thread.yield()方法的调用好像就是在说:我已经完成了一些主要的工作,我可以休息一下了,可以让CPU给其他线程一些工作机会了。
如果觉得一个线程不太重要,或者优先级比较低,而又担心此线程会过多的占用CPU资源,那么可以在适当的时候调用一下Thread.yield()方法,给与其他线程更多的机会。
public static native void yield();
总结
- 创建线程的4种方式:继承Thread类;实现Runnable接口;实现Callable接口;使用线程池创建。
- 启动线程:调用线程的start()方法
- 终止线程:调用线程的stop()方法,方法已过时,建议不要使用
- 线程中断相关的方法:调用线程实例interrupt()方法将中断标志置为true;使用线程实例方法isInterrupted()获取中断标志;调用Thread的静态方法interrupted()获取线程是否被中断,此方法调用之后会清除中断标志(将中断标志置为false了)
- wait、notify、notifyAll方法
- 线程挂起使用线程实例方法suspend(),恢复线程使用线程实例方法resume(),这2个方法都过时了,不建议使用
- 等待线程结束:调用线程实例方法join()
- 让出cpu资源:调用线程静态方法yeild()
疑问:
Q:方法interrupted()是一个静态方法,返回boolean类型,也是用来判断当前线程是否被中断,但是同时会清除当前线程的中断标志位的状态。 清除当前线程的中断标志位的状态是表示该线程可以不中断了么?清除当前线程的中断标志位的状态是什么意思,有什么作用?怎么使用?
Q:三个线程交替打印ABC 10次使用wait(),notifyAll()如何实现?
volatile与Java内存模型
volatile解决了共享变量在多线程中可见性的问题,可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。
使用volatile保证内存可见性示例:
public class VolatileTest {
//public static boolean flag = true;
public static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("T1");
t1.start();
//TimeUnit.SECONDS.sleep(3);
Thread.sleep(2000);
//将flag置为false
flag = false;
}
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + " start");
while (VolatileTest.flag) {
//奇怪现象,为什么执行输出语句在运行一会儿后会让flag=false读取到,而 ; 空循环却会导致程序无法终止呢?
//个人觉得应该是虚拟机从解释执行转换为编译执行,这时候会重新读到flag。
//System.out.println(this.getName() +"endless loop");
;
}
System.out.println(this.getName() + " end");
}
}
}
不加volatile运行上面代码,会发现程序无法终止。
Q:t1线程中为什么看不到被主线程修改之后的flag?
要解释这个,我们需要先了解一下java内存模型(JMM),Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
Java内存模型的抽象示意图:
线程A需要和线程B通信,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
线程t1中为何看不到被主线程修改为false的flag的值的原因,有两种可能:
- 主线程修改了flag之后,未将其刷新到主内存,所以t1看不到
- 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中获取flag最新的值
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
线程组
我们可以把线程归属到某个线程组中,线程组可以包含多个线程以及线程组,线程和线程组组成了父子关系,是个树形结构。使用线程组可以方便管理线程 。(线程池是不是更实在一点?)
创建线程关联线程组
创建线程的时候,可以给线程指定一个线程组。
创建线程组的时候,可以给其指定一个父线程组,也可以不指定,如果不指定父线程组,则父线程组为当前线程的线程组,系统自动获取当前线程的线程组作为默认父线程组。java api有2个常用的构造方法用来创建线程组:
public ThreadGroup(String name) public ThreadGroup(ThreadGroup parent, String name)
第一个构造方法未指定父线程组,看一下内部的实现:
public ThreadGroup(String name) { this(Thread.currentThread().getThreadGroup(), name); }
批量停止线程
调用线程组interrupt(),会将线程组树下的所有子孙线程中断标志置为true,可以用来批量中断线程。
建议创建线程或者线程组的时候,给他们取一个有意义的名字,在系统出问题的时候方面查询定位。
示例:
public class ThreadGroupTest {
public static class R1 implements Runnable {
@Override
public void run() {
System.out.println("threadName:" + Thread.currentThread().getName());
while (!Thread.currentThread().isInterrupted()){
;
}
System.out.println(Thread.currentThread().getName()+"线程停止了");
}
}
public static void main(String[] args) throws InterruptedException {
//threadGroup1未指定父线程组,系统获取了主线程的线程组作为threadGroup1的父线程组,输出结果中是:main
ThreadGroup threadGroup = new ThreadGroup("thread-group-1");
Thread t1 = new Thread(threadGroup, new R1(), "t1");
Thread t2 = new Thread(threadGroup, new R1(), "t2");
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("活动线程数:" + threadGroup.activeCount());
System.out.println("活动线程组:" + threadGroup.activeGroupCount());
System.out.println("线程组名称:" + threadGroup.getName());
ThreadGroup threadGroup2 = new ThreadGroup(threadGroup, "thread-group-2");
Thread t3 = new Thread(threadGroup2, new R1(), "t3");
Thread t4 = new Thread(threadGroup2, new R1(), "t4");
t3.start();
t4.start();
threadGroup.list();
//java.lang.ThreadGroup[name=main,maxpri=10] 主线程的线程组为main
System.out.println(Thread.currentThread().getThreadGroup());
//java.lang.ThreadGroup[name=system,maxpri=10] 根线程组为system
System.out.println(Thread.currentThread().getThreadGroup().getParent());
//null
System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent());
threadGroup.interrupt();
TimeUnit.SECONDS.sleep(2);
threadGroup.list();
}
}
输出:
threadName:t1
threadName:t2
活动线程数:2
活动线程组:0
线程组名称:thread-group-1
java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
Thread[t1,5,thread-group-1]
Thread[t2,5,thread-group-1]
java.lang.ThreadGroup[name=thread-group-2,maxpri=10]
Thread[t3,5,thread-group-2]
Thread[t4,5,thread-group-2]
java.lang.ThreadGroup[name=main,maxpri=10]
java.lang.ThreadGroup[name=system,maxpri=10]
null
t2线程停止了
t1线程停止了
threadName:t4
threadName:t3
t4线程停止了
t3线程停止了
java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
java.lang.ThreadGroup[name=thread-group-2,maxpri=10]
用户线程和守护线程
守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程都是守护线程。与之对应的是用户线程,用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出。
java线程分为用户线程和守护线程,线程的daemon属性为true表示是守护线程,false表示是用户线程。
线程daemon的默认值
我们看一下创建线程源码,位于Thread类的init()方法中:
Thread parent = currentThread();
this.daemon = parent.isDaemon();
dameon的默认值为为父线程的daemon,也就是说,父线程如果为用户线程,子线程默认也是用户现场,父线程如果是守护线程,子线程默认也是守护线程。
总结
- java中的线程分为用户线程和守护线程
- 程序中的所有的用户线程结束之后,不管守护线程处于什么状态,java虚拟机都会自动退出
- 调用线程的实例方法setDaemon()来设置线程是否是守护线程
- setDaemon()方法必须在线程的start()方法之前调用,在后面调用会报异常,并且不起效
- 线程的daemon默认值和其父线程一样
示例:
public class DaemonThreadTest {
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + " start ,isDaemon= "+isDaemon());
while (true) {
;
}
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("T1");
// 设置守护线程,需要在start()方法之前进行
// t1.start()必须在setDaemon(true)之后,否则执行会报异常:Exception in thread "main" java.lang.IllegalThreadStateException
//t1.start();
//将t1线程设置为守护线程
t1.setDaemon(true);
t1.start();
//当程序中所有的用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。
TimeUnit.SECONDS.sleep(1);
}
}
疑问:
Q:JIT线程?
A: JIT一般指准时制。准时制生产方式(Just In Time简称JIT ).JIT线程在Java中表示即时编译线程,解释执行。
在Java编程语言和环境中,即时编译器(JIT compiler,just-in-time compiler)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。
线程安全和synchronized关键字
什么是线程安全?
当多个线程去访问同一个类(对象或方法)的时候,该类都能表现出一致的行为,没有意想不到的不同结果,那我们就可以所这个类是线程安全的。
造成线程安全问题的主要诱因有两点:
- 一是存在共享数据(也称临界资源)
- 二是存在多条线程共同操作共享数据
为了解决这个问题,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,这种方式有个高尚的名称叫互斥锁,在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能)
锁的互斥性表现在线程尝试获取的是否是同一个锁,相同类型不同实例的对象锁不互斥,而class类对象的锁与实例锁之间也不互斥。
synchronized主要有3种使用方式
- 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
- 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
- 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁
synchronized作用于实例对象
synchronize作用于实例方法需要注意:
- 实例方法上加synchronized,线程安全的前提是,多个线程操作的是同一个实例,如果多个线程作用于不同的实例,那么线程安全是无法保证的
- 同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法
synchronized作用于静态方法
当synchronized作用于静态方法时,锁的对象就是当前类的Class对象。
synchronized同步代码块
方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分时使用。加锁时可以使用自定义的对象作为锁,也可以使用this对象(代表当前实例)或者当前类的class对象作为锁 。
疑问:
Q:synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能),synchronized是怎么保证可见性的呢?
Q:同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法.验证同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法是对的。
A:示例有下:
public class MethodObject {
public synchronized void methodA() throws InterruptedException {
System.out.println("methodA start");
TimeUnit.SECONDS.sleep(10);
System.out.println("methodA finish");
}
public synchronized void methodB() throws InterruptedException {
System.out.println("methodB start");
TimeUnit.SECONDS.sleep(5);
System.out.println("methodB finish");
}
}
public class SynchronousTest {
public static void main(String[] args) throws InterruptedException {
MethodObject mo = new MethodObject();
T1 t1 = new T1("T1", mo);
T2 t2 = new T2("T2", mo);
t1.start();
TimeUnit.MILLISECONDS.sleep(300);
t2.start();
}
public static class T1 extends Thread {
private MethodObject mo;
public T1(String name, MethodObject mo) {
super(name);
this.mo = mo;
}
@Override
public void run() {
try {
mo.methodA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static class T2 extends Thread {
private MethodObject mo;
public T2(String name, MethodObject mo) {
super(name);
this.mo = mo;
}
@Override
public void run() {
try {
mo.methodB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
synchronized实现原理
深入理解Java并发之synchronized实现原理
线程中断的2种方式
1、通过一个volatile修饰的变量控制线程中断
利用volatile控制的变量在多线程中的可见性,Java内存模型实现。
示例:
public class VolatileTest {
//public static boolean flag = true;
public static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("T1");
t1.start();
//TimeUnit.SECONDS.sleep(3);
Thread.sleep(3000);
//将flag置为false
flag = false;
}
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + " start");
while (VolatileTest.flag) {
;
}
System.out.println(this.getName() + " end");
}
}
}
2、通过线程自带的中断标志interrupt() 控制
当调用线程的interrupt()实例方法之后,线程的中断标志会被置为true,可以通过线程的实例方法isInterrupted()获取线程的中断标志。
当运行的线程处于阻塞状态时:
- 调用线程的interrupt()实例方法,线程的中断标志会被置为true
- 当线程处于阻塞状态时,调用线程的interrupt()实例方法,线程内部会触发InterruptedException异常,并且会清除线程内部的中断标志(即将中断标志置为false)
阻塞状态处理方法:这时候应该在catch中再调用this.interrupt();一次,将中断标志置为true。然后在run()方法中通过this.isInterrupted()来获取线程的中断标志,退出循环break。
总结
- 当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,可以使用 Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态)。阻塞状态线程要通过线程自带的中断标志interrupt() 控制中断。
- 内部有循环体,可以通过一个变量来作为一个信号控制线程是否中断,注意变量需要volatile修饰。
- 文中的2种方式可以结合起来灵活使用控制线程的中断.
示例:
public class InterruptTest1 {
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + " start");
while (true) {
try {
//下面模拟阻塞代码
TimeUnit.SECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
this.interrupt();
}
if (this.isInterrupted()) {
break;
}
}
System.out.println(this.getName() + " end");
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("thread1");
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
}
}
输出:
thread1 start
thread1 end
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.self.current.InterruptTest1$T1.run(InterruptTest1.java:27)
ReentrantLock重入锁
synchronized的局限性
synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:
- 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制。(synchronized不能响应中断?)
- 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。(synchronized不能响应中断?)
ReentrantLock
ReentrantLock是Lock的默认实现,在聊ReentranLock之前,我们需要先弄清楚一些概念:
- 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁
- 可中断锁:可中断锁是指线程在获取锁的过程中,是否可以响应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的
- 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁。
ReentrantLock基本使用
ReentrantLock的使用过程:
- 创建锁:ReentrantLock lock = new ReentrantLock();
- 获取锁:lock.lock()
- 释放锁:lock.unlock();
对比上面的代码,与关键字synchronized相比,ReentrantLock锁有明显的操作过程,开发人员必须手动的指定何时加锁,何时释放锁,正是因为这样手动控制,ReentrantLock对逻辑控制的灵活度要远远胜于关键字synchronized,上面代码需要注意lock.unlock()一定要放在finally中,否则若程序出现了异常,锁没有释放,那么其他线程就再也没有机会获取这个锁了。
ReentrantLock是可重入锁
假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。
- lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁了;可以将add中的unlock删除一个事实,上面代码运行将无法结束
- unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放
示例:
public class ReentrantLockTest {
private static int num = 0;
private static Lock lock = new ReentrantLock();
public static void add() {
lock.lock();
lock.lock();
try {
num++;
} finally {
//lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁
lock.unlock();
lock.unlock();
}
}
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
ReentrantLockTest.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("get num =" + num);
}
}
//输出: get num =3000
ReentrantLock实现公平锁
在大多数情况下,锁的申请都是非公平的。这就好比买票不排队,上厕所不排队。最终导致的结果是,有些人可能一直买不到票。而公平锁,它会按照到达的先后顺序获得资源。公平锁的一大特点是不会产生饥饿现象,只要你排队,最终还是可以等到资源的;synchronized关键字默认是有jvm内部实现控制的,是非公平锁。而ReentrantLock运行开发者自己设置锁的公平性,可以实现公平和非公平锁。
看一下jdk中ReentrantLock的源码,2个构造方法:
public ReentrantLock() { sync = new NonfairSync();}
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
默认构造方法创建的是非公平锁。
第2个构造方法,有个fair参数,当fair为true的时候创建的是公平锁,公平锁看起来很不错,不过要实现公平锁,系统内部肯定需要维护一个有序队列,因此公平锁的实现成本比较高,性能相对于非公平锁来说相对低一些。因此,在默认情况下,锁是非公平的,如果没有特别要求,则不建议使用公平锁。
示例:
public class ReentrantLockFairTest {
private static int num = 0;
//private static Lock lock = new ReentrantLock(false);
private static Lock lock = new ReentrantLock(true);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" got lock");
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
}
}
输出:
公平锁:
t1 got lock
t1 got lock
t2 got lock
t2 got lock
t3 got lock
t3 got lock
非公平锁:
t1 got lock
t3 got lock
t3 got lock
t2 got lock
t2 got lock
t1 got lock
ReentrantLock获取锁的过程是可中断的——使用lockInterruptibly()和tryLock(long time, TimeUnit unit)有参方法时。
对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:
- 要么获取到锁然后继续后面的操作
- 要么一直等待,直到其他线程释放锁为止
而ReentrantLock提供了另外一种可能,就是在等的获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。拿李云龙平安县围点打援来说,当平安县城被拿下后,鬼子救援的部队再尝试救援已经没有意义了,这时候要请求中断操作。
关于获取锁的过程中被中断,注意几点:
- ReentrankLock中必须使用实例方法 lockInterruptibly()获取锁时,在线程调用interrupt()方法之后,才会引发 InterruptedException异常
- 线程调用interrupt()之后,线程的中断标志会被置为true
- 触发InterruptedException异常之后,线程的中断标志有会被清空,即置为false
- 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false->true->false
实例:
public class InterruptTest2 {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
public static class T1 extends Thread {
int lock;
public T1(String name, Integer lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
//线程发送中断信号触发InterruptedException异常之后,中断标志将被清空。
System.out.println(this.getName() + "中断标志:" + this.isInterrupted());
e.printStackTrace();
} finally {
//ReentrantLock自有的方法,多态实现的Lock不能用
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("thread1", 1);
T1 t2 = new T1("thread2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1000);
//不加interrupt()通过jstack查看线程堆栈信息,发现2个线程死锁了
//"thread2":
// waiting for ownable synchronizer 0x000000076b782028, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
// which is held by "thread1"
//"thread1":
// waiting for ownable synchronizer 0x000000076b782058, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
// which is held by "thread2"
t1.interrupt();
}
}
ReentrantLock锁申请等待限时
ReentrantLock刚好提供了这样功能,给我们提供了获取锁限时等待的方法 tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。
tryLock无参方法——tryLock()是立即响应的,中间不会有阻塞。
看一下源码中tryLock方法:
public boolean tryLock()
tryLock有参方法
该方法在指定的时间内不管是否可以获取锁,都会返回结果,返回true,表示获取锁成功,返回false表示获取失败。 此方法在执行的过程中,如果调用了线程的中断interrupt()方法,会触发InterruptedException异常。
可以明确设置获取锁的超时时间:
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
关于tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,说明一下:
- 都会返回boolean值,结果表示获取锁是否成功。
- tryLock()方法,不管是否获取成功,都会立即返回;而有参的tryLock方法会尝试在指定的时间内去获取锁,中间会阻塞的现象,在指定的时间之后会不管是否能够获取锁都会返回结果。
- tryLock()方法不会响应线程的中断方法;而有参的tryLock方法会响应线程的中断方法,而出发 InterruptedException异常,这个从2个方法的声明上可以可以看出来。
ReentrantLock其他常用的方法
- isHeldByCurrentThread:实例方法,判断当前线程是否持有ReentrantLock的锁,上面代码中有使用过。
获取锁的4种方法对比
获取锁的方法 是否立即响应(不会阻塞) 是否响应中断
lock() × ×
lockInterruptibly() × √
tryLock() √ ×
tryLock(long timeout, TimeUnit unit) × √
实例:
public class ReentrantLockTest1 {
private static ReentrantLock lock1 = new ReentrantLock();
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
try {
System.out.println(this.getName()+"尝试获取锁");
if (lock1.tryLock(2,TimeUnit.SECONDS)){
System.out.println(this.getName()+"获取锁成功");
TimeUnit.SECONDS.sleep(3);
}else {
System.out.println(this.getName()+"获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
t1.start();
t2.start();
}
}
输出:
lock1.tryLock()
t1尝试获取锁
t1获取锁成功
t2尝试获取锁
t2获取锁失败
lock1.tryLock(2,TimeUnit.SECONDS)
t1尝试获取锁
t2尝试获取锁
t1获取锁成功
t2获取锁失败
总结
- ReentrantLock可以实现公平锁和非公平锁
- ReentrantLock默认实现的是非公平锁
- ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
- 释放锁的操作必须放在finally中执行
- lockInterruptibly()实例方法可以响应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发 InterruptedException异常
- 关于 InterruptedException异常说一下,看到方法声明上带有 throwsInterruptedException,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发 InterruptedException异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用 interrupt()方法而触发 InterruptedException异常,线程的标志由默认的false变为ture,然后又变为false
- 实例方法tryLock()获会尝试获取锁,会立即返回,返回值表示是否获取成功
- 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断
疑问
Q:可中断锁:可中断锁时只线程在获取锁的过程中,是否可以相应线程中断操作。为什么synchronized是不可中断的,ReentrantLock是可中断的?
JUC中的Condition对象
Condition使用简介——实现等待/通知机制
注意:在使用使用Condition.await()方法时,需要先获取Condition对象关联的ReentrantLock的锁;就像使用Object.wait()时必须在synchronized同步代码块内。
从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:
- Condition能够支持不响应中断,而通过使用Object方式不支持
- Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个
- Condition能够支持超时时间的设置,而Object不支持
Condition由ReentrantLock对象创建,并且可以同时创建多个,Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁,之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程,使用方式和wait()、notify()类似。
需要注意的时,当一个线程被signal()方法唤醒线程时,它第一个动作是去获取同步锁,注意这一点,而这把锁目前在调用signal()方法唤醒他的线程上,必须等其释放锁后才能得到争抢锁的机会。
实例:
public class ConditionTest {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) {
T1 t1 = new T1("TT1");
T2 t2 = new T2("TT2");
t1.start();
t2.start();
}
static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
lock.lock();
System.out.println(this.getName() + " start");
try {
System.out.println(this.getName() + " wait");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(this.getName() + " end");
}
}
static class T2 extends Thread {
public T2(String name) {
super(name);
}
@Override
public void run() {
lock.lock();
System.out.println(this.getName() + " start");
System.out.println(this.getName() + " signal");
condition.signal();
System.out.println(this.getName() + " end");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName() + " end,2 second later");
}
}
}
输出:
TT1 start
TT1 wait
TT2 start
TT2 signal
TT2 end
TT2 end,2 second later
Condition常用方法
和Object中wait类似的方法
- void await() throws InterruptedException:当前线程进入等待状态,如果在等待状态中被中断会抛出被中断异常;
- long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;
- boolean await(long time, TimeUnit unit) throws InterruptedException:同第二种,支持自定义时间单位,false:表示方法超时之后自动返回的,true:表示等待还未超时时,await方法就返回了(超时之前,被其他线程唤醒了)
- boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间
- void awaitUninterruptibly();:当前线程进入等待状态,不会响应线程中断操作,只能通过唤醒的方式让线程继续
和Object的notify/notifyAll类似的方法
- void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
- void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程
Condition.await()过程中被打断
调用condition.await()之后,线程进入阻塞中,调用t1.interrupt(),给t1线程发送中断信号,await()方法内部会检测到线程中断信号,然后触发 InterruptedException异常,线程中断标志被清除。从输出结果中可以看出,线程t1中断标志的变换过程:false->true->false
await(long time, TimeUnit unit)超时之后自动返回
t1线程等待2秒之后,自动返回继续执行,最后await方法返回false,await返回false表示超时之后自动返回
await(long time, TimeUnit unit)超时之前被唤醒
t1线程中调用 condition.await(5,TimeUnit.SECONDS);方法会释放锁,等待5秒,主线程休眠1秒,然后获取锁,之后调用signal()方法唤醒t1,输出结果中发现await后过了1秒(1、3行输出结果的时间差),await方法就返回了,并且返回值是true。true表示await方法超时之前被其他线程唤醒了。
long awaitNanos(long nanosTimeout)超时返回
t1调用await方法等待5秒超时返回,返回结果为负数,表示超时之后返回的。
//awaitNanos参数为纳秒,可以调用TimeUnit中的一些方法将时间转换为纳秒。
long nanos = TimeUnit.SECONDS.toNanos(2);
waitNanos(long nanosTimeout)超时之前被唤醒
t1中调用await休眠5秒,主线程休眠1秒之后,调用signal()唤醒线程t1,await方法返回正数,表示返回时距离超时时间还有多久,将近4秒,返回正数表示,线程在超时之前被唤醒了。
其他几个有参的await方法和无参的await方法一样,线程调用interrupt()方法时,这些方法都会触发InterruptedException异常,并且线程的中断标志会被清除。
同一个锁支持创建多个Condition
使用两个Condition来实现一个阻塞队列的例子:
public class MyBlockingQueue {
//阻塞队列最大容量
private int size;
//队列底层实现
private LinkedList list = new LinkedList<>();
private static Lock lock = new ReentrantLock();
//队列满时的等待条件
private static Condition fullFlag = lock.newCondition();
//队列空时的等待条件
private static Condition emptyFlag = lock.newCondition();
public MyBlockingQueue(int size) {
this.size = size;
}
public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
//队列已满,在fullFlag条件上等待
while (list.size() == size) {
fullFlag.await();
}
//入队:加入链表末尾
list.add(e);
System.out.println("生产了" + e);
//通知在emptyFlag条件上等待的线程
emptyFlag.signal();
} finally {
lock.unlock();
}
}
public E dequeue() throws InterruptedException {
lock.lock();
try {
while (list.size() == 0) {
emptyFlag.await();
}
E e = list.removeFirst();
System.out.println("消费了" + e);
//通知在fullFlag条件上等待的线程
fullFlag.signal();
return e;
} finally {
lock.unlock();
}
}
/**
* 创建了一个阻塞队列,大小为3,队列满的时候,会被阻塞,等待其他线程去消费,队列中的元素被消费之后,会唤醒生产者,生产数据进入队列。上面代码将队列大小置为1,可以实现同步阻塞队列,生产1个元素之后,生产者会被阻塞,待消费者消费队列中的元素之后,生产者才能继续工作。
* @param args
*/
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue<>(1);
for (int i = 0; i < 10; i++) {
int finalI = i;
Thread producer = new Thread(() -> {
try {
queue.enqueue(finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
}
for (int i = 0; i < 10; i++) {
Thread consumer = new Thread(() -> {
try {
queue.dequeue();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumer.start();
}
}
}
输出:
生产了0
消费了0
生产了1
消费了1
。。。。
生产了9
消费了9
Object的监视器方法与Condition接口的对比
注意同步队列和等待队列的区别,同步队列表示在竞争一把锁的队列中,是处于阻塞或运行状态的队列。
而等待队列是指被置为等待、超时等待状态的线程,这些是没有竞争锁的权限的,处于等待被唤醒的状态中。
对比项 Object 监视器方法 Condition
前置条件 获取对象的锁 调用Lock.lock获取锁,调用Lock.newCondition()获取Condition对象
调用方式 直接调用,如:object.wait() 直接调用,如:condition.await()
等待队列个数 一个 多个,使用多个condition实现
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁进入等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并进入等待状态到将来某个时间 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持
总结
- 使用condition的步骤:创建condition对象,获取锁,然后调用condition的方法
- 一个ReentrantLock支持创建多个condition对象
- void await() throws InterruptedException;方法会释放锁,让当前线程等待,支持唤醒,支持线程中断
- void awaitUninterruptibly();方法会释放锁,让当前线程等待,支持唤醒,不支持线程中断
- long awaitNanos(longnanosTimeout)throws InterruptedException;参数为纳秒,此方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为负数;超时之前被唤醒返回的,结果为正数(表示返回时距离超时时间相差的纳秒数)
- boolean await (longtime,TimeUnitunit)throws InterruptedException;方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为false;超时之前被唤醒返回的,结果为true
- boolean awaitUntil(Datedeadline)throws InterruptedException;参数表示超时的截止时间点,方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为false;超时之前被唤醒返回的,结果为true
- void signal();会唤醒一个等待中的线程,然后被唤醒的线程会被加入同步队列,去尝试获取锁
- void signalAll();会唤醒所有等待中的线程,将所有等待中的线程加入同步队列,然后去尝试获取锁
疑问:
Q:Condition能够支持超时时间的设置,而Object不支持。Object不是有wait(long timeout)超时时间设置么?这句话是不是错了?
Q:在Condition.await()方法被调用时,当前线程会释放这个锁,并且当前线程会进行等待(处于阻塞状态)?是阻塞状态不是等待状态?是因为这事api语言级别的等待/通知机制么?
JUC中的LockSupport工具类——无需加锁及考虑等待通知顺序
使用Object类中的方法实现线程等待和唤醒
关于Object类中的用户线程等待和唤醒的方法,总结一下:
- wait()/notify()/notifyAll()方法都必须放在同步代码(必须在synchronized内部执行)中执行,需要先获取锁(否则抛出了 IllegalMonitorStateException异常 )
- 线程唤醒的方法(notify、notifyAll)需要在等待的方法(wait)之后执行,等待中的线程才可能会被唤醒,否则无法唤醒
使用Condition实现线程的等待和唤醒
关于Condition中方法使用总结:
- 使用Condtion中的线程等待和唤醒方法之前,需要先获取锁。否者会报 IllegalMonitorStateException异常
- signal()方法先于await()方法之前调用,线程无法被唤醒
Object和Condition的局限性
关于Object和Condtion中线程等待和唤醒的局限性,有以下几点:
- 两种方式中的让线程等待和唤醒的方法能够执行的先决条件是:线程需要先获取锁
- 唤醒方法需要在等待方法之后调用,线程才能够被唤醒
关于这2点,LockSupport都不需要,就能实现线程的等待和唤醒。
LockSupport类介绍
LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。主要是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作的。(注意park方法等待不释放锁)
每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。
- 当调用unpark(thread)方法,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)。
- 当调用park()方法,如果当前线程的permit是1,那么将permit设置为0,并立即返回。如果当前线程的permit是0,那么当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0,并返回。
注意:因为permit默认是0,所以一开始调用park()方法,线程必定会被阻塞。调用unpark(thread)方法后,会自动唤醒thread线程,即park方法立即返回。
LockSupport中常用的方法
阻塞线程
- void park():阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回
- void park(Object blocker):功能同方法1,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查
- void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性
- void parkNanos(Object blocker, long nanos):功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查
- void parkUntil(long deadline):阻塞当前线程,直到deadline,deadline是一个绝对时间,表示某个时间的毫秒格式
- void parkUntil(Object blocker, long deadline):功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
唤醒线程
- void unpark(Thread thread):唤醒处于阻塞状态的指定线程
1、LockSupport调用park、unpark方法执行唤醒等待无需加锁。
2、LockSupport中,唤醒的方法不管是在等待之前还是在等待之后调用,线程都能够被唤醒。 唤醒方法在等待方法之前执行,线程也能够被唤醒,这点是另外两种方法无法做到的。而Object和Condition中的唤醒必须在等待之后调用,线程才能被唤醒。
3、park方法可以相应线程中断。
LockSupport.park方法让线程等待之后,唤醒方式有2种:
- 调用LockSupport.unpark方法
- 调用等待线程的 interrupt()方法,给等待的线程发送中断信号,可以唤醒线程
线程t1和t2的不同点是,t2中调用park方法传入了一个BlockerDemo对象,从上面的线程堆栈信息中,发现t2线程的堆栈信息中多了一行 -parking to waitfor<0x00000007180bfeb0>(a com.itsoku.chat10.Demo10$BlockerDemo),刚好是传入的BlockerDemo对象,park传入的这个参数可以让我们在线程堆栈信息中方便排查问题,其他暂无他用。
线程等待和唤醒的3种方式做个对比
- 方式1:Object中的wait、notify、notifyAll方法
- 方式2:juc中Condition接口提供的await、signal、signalAll方法
- 方式3:juc中的LockSupport提供的park、unpark方法
3种方式对比:
Object Condtion LockSupport
前置条件 需要在synchronized中运行 需要先获取Lock的锁 无
无限等待 支持 支持 支持
超时等待 支持 支持 支持
等待到将来某个时间返回 不支持 支持 支持
等待状态中释放锁 会释放 会释放 不会释放
唤醒方法先于等待方法执行,能否唤醒线程 否 否 可以
是否能响应线程中断 是 是 是
线程中断是否会清除中断标志 是 是 否
是否支持等待状态中不响应中断 不支持 支持 不支持
实例:
public class LockSupportTest {
/**
* 输出:
* create a thread start
* 主线程执行完毕!
* thread 被唤醒
*/
public static void main1(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("create a thread start");
LockSupport.park();
System.out.println("thread 被唤醒");
});
t.start();
TimeUnit.SECONDS.sleep(3);
//唤醒处于阻塞状态的指定线程
LockSupport.unpark(t);
//响应线程中断
//t.interrupt();
System.out.println("主线程执行完毕!");
}
/**
* 唤醒方法在等待方法之前执行,线程也能够被唤醒
*输出:
* create a thread start
* 主线程执行完毕!
* thread 被唤醒
*/
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("create a thread start");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
System.out.println("thread 被唤醒");
});
t.start();
TimeUnit.SECONDS.sleep(1);
//唤醒处于阻塞状态的指定线程
LockSupport.unpark(t);
//响应线程中断
//t.interrupt();
System.out.println("主线程执行完毕!");
}
}
疑问:
Q:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。主要是通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作的。(注意park方法等待不释放锁)不释放锁的等待唤醒是在什么场景下使用?为什么要这样?
JUC中的Semaphore(信号量)——多把锁控制,用于限流
synchronized和重入锁ReentrantLock,这2种锁一次都只能允许一个线程访问一个资源,而信号量可以控制有多少个线程可以访问特定的资源。
Semaphore常用场景:限流
举个例子:
比如有个停车场(临界值,共享资源),有5个空位,门口有个门卫,手中5把钥匙分别对应5个车位上面的锁,来一辆车,门卫会给司机一把钥匙,然后进去找到对应的车位停下来,出去的时候司机将钥匙归还给门卫。停车场生意比较好,同时来了100两车,门卫手中只有5把钥匙,同时只能放5辆车进入,其他车只能等待,等有人将钥匙归还给门卫之后,才能让其他车辆进入。
上面的例子中门卫就相当于Semaphore,车钥匙就相当于许可证,车就相当于线程。
Semaphore主要方法
Semaphore(int permits):构造方法,参数表示许可证数量,用来创建信号量
Semaphore(int permits,boolean fair):构造方法,当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量
void acquire() throws InterruptedException:从此信号量获取1个许可前线程将一直阻塞,相当于一辆车占了一个车位,此方法会响应线程中断,表示调用线程的interrupt方法,会使该方法抛出InterruptedException异常
void acquire(int permits) throws InterruptedException :和acquire()方法类似,参数表示需要获取许可的数量;比如一个大卡车要入停车场,由于车比较大,需要申请3个车位才可以停放
void acquireUninterruptibly(int permits) :和acquire(int permits) 方法类似,只是不会响应线程中断
boolean tryAcquire():尝试获取1个许可,不管是否能够获取成功,都立即返回,true表示获取成功,false表示获取失败
boolean tryAcquire(int permits):和tryAcquire(),表示尝试获取permits个许可
boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException:尝试在指定的时间内获取1个许可,获取成功返回true,指定的时间过后还是无法获取许可,返回false
boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException:和tryAcquire(long timeout, TimeUnit unit)类似,多了一个permits参数,表示尝试获取permits个许可
void release():释放一个许可,将其返回给信号量,相当于车从停车场出去时将钥匙归还给门卫
void release(int n):释放n个许可
int availablePermits():当前可用的许可数
获取许可之后不释放
取许可后,没有释放许可的代码,最终导致,可用许可数量为0,其他线程无法获取许可,会在 semaphore.acquire();处等待,导致程序无法结束。
没有获取到许可却执行释放(没有获取到许可却在finally中直接执行release方法)
如果获取锁的过程中发生异常,导致获取锁失败,最后finally里面也释放了许可,最终会导致许可数量凭空增长了。
释放许可正确的姿势
程序中增加了一个变量 acquireSuccess用来标记获取许可是否成功,在finally中根据这个变量是否为true,来确定是否释放许可。
在规定的时间内希望获取许可
Semaphore内部2个方法可以提供超时获取许可的功能:
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedExceptionpublic boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException
在指定的时间内去尝试获取许可,如果能够获取到,返回true,获取不到返回false。
其他一些使用说明
- Semaphore默认创建的是非公平的信号量,什么意思呢?这个涉及到公平与非公平。让新来的去排队就表示公平,直接去插队争抢第一个,就表示不公平。对于停车场,排队肯定更好一些。不过对于信号量来说不公平的效率更高一些,所以默认是不公平的。
- 方法中带有 throwsInterruptedException声明的,表示这个方法会响应线程中断信号,什么意思?表示调用线程的 interrupt()方法后,会让这些方法触发 InterruptedException异常,即使这些方法处于阻塞状态,也会立即返回,并抛出 InterruptedException异常,线程中断信号也会被清除。
示例:
public class SemaphoreTest {
private static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread t = new T1("T" + i);
t.start();
if (i > 2) {
TimeUnit.SECONDS.sleep(1);
t.interrupt();
}
}
}
public static void main2(String[] args) {
for (int i = 0; i < 5; i++) {
Thread t = new T2("T" + i);
t.start();
}
}
/**
* 在指定的时间内去尝试获取许可,如果能够获取到,返回true,获取不到返回false。
*/
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
Boolean hasTicket = false;
Thread thread = Thread.currentThread();
try {
//semaphore.acquire();
hasTicket = semaphore.tryAcquire(1, TimeUnit.SECONDS);
if (hasTicket) {
System.out.println(thread + "获取到停车位!");
} else {
System.out.println(thread + "获取不到停车位!走了");
}
//hasTicket = true;
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (hasTicket) {
semaphore.release();
//没有获取到许可却在finally中直接执行release方法
//Thread[T6,5,main]离开停车位!当前空余停车位数量10
System.out.println(thread + "离开停车位!当前空余停车位数量" + semaphore.availablePermits());
}
}
}
}
/**
* 正确的释放锁的方式
*/
public static class T2 extends Thread {
public T2(String name) {
super(name);
}
@Override
public void run() {
Boolean hasTicket = false;
Thread thread = Thread.currentThread();
try {
semaphore.acquire();
hasTicket = true;
System.out.println(thread + "获取到停车位!");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (hasTicket) {
semaphore.release();
System.out.println(thread + "离开停车位!当前空余停车位数量" + semaphore.availablePermits());
}
}
}
}
}
输出:
test1
Thread[T0,5,main]获取到停车位!
Thread[T1,5,main]获取到停车位!
Thread[T2,5,main]获取不到停车位!走了
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedNanos(AbstractQueuedSynchronizer.java:1039)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireSharedNanos(AbstractQueuedSynchronizer.java:1328)
at java.util.concurrent.Semaphore.tryAcquire(Semaphore.java:409)
at com.self.current.SemaphoreTest$T1.run(SemaphoreTest.java:52)
java.lang.InterruptedException: sleep interrupted
Thread[T1,5,main]离开停车位!当前空余停车位数量1
at java.lang.Thread.sleep(Native Method)
Thread[T4,5,main]获取到停车位!
at java.lang.Thread.sleep(Thread.java:340)
Thread[T0,5,main]离开停车位!当前空余停车位数量1
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
Thread[T4,5,main]离开停车位!当前空余停车位数量2
at com.self.current.SemaphoreTest$T1.run(SemaphoreTest.java:59)
test2
Thread[T0,5,main]获取到停车位!
Thread[T1,5,main]获取到停车位!
Thread[T1,5,main]离开停车位!当前空余停车位数量2
Thread[T2,5,main]获取到停车位!
Thread[T0,5,main]离开停车位!当前空余停车位数量2
Thread[T3,5,main]获取到停车位!
Thread[T2,5,main]离开停车位!当前空余停车位数量1
Thread[T4,5,main]获取到停车位!
Thread[T3,5,main]离开停车位!当前空余停车位数量1
Thread[T4,5,main]离开停车位!当前空余停车位数量2
示例2:
通过判断byteBuffer != null得出是否获取到了许可。
semaphore = new Semaphore(maxBufferCount);
byteBuffer = allocator.allocate();// semaphore.acquire();
finally {
if (byteBuffer != null) {
byteBuffer.clear();
allocator.release(byteBuffer);
}
}
疑问:
Q:Semaphore内部排队等待资源的队列是怎么实现的,公平信号量与非公平的队列类型都是哪种的?
JUC中等待多线程完成的工具类CountDownLatch(闭锁 )——等待多线程完成后执行操作或者实现最大的并发线程数同时执行
CountDownLatch介绍
CountDownLatch称之为闭锁,它可以使一个或一批线程在闭锁上等待,等到其他线程执行完相应操作后,闭锁打开,这些等待的线程才可以继续执行。确切的说,闭锁在内部维护了一个倒计数器。通过该计数器的值来决定闭锁的状态,从而决定是否允许等待的线程继续执行。
一批线程等待闭锁一般用于同步并发,如跑步比赛时作为发令枪作用;
一个线程等待闭锁一般用于等待并发线程或资源的获取满足,用于执行收尾工作,如多任务执行完后合并结果等。
常用方法:
public CountDownLatch(int count):构造方法,count表示计数器的值,不能小于0,否者会报异常。
public void await() throws InterruptedException:调用await()会让当前线程等待,直到计数器为0的时候,方法才会返回,此方法会响应线程中断操作。
public boolean await(long timeout, TimeUnit unit) throws InterruptedException:限时等待,在超时之前,计数器变为了0,方法返回true,否者直到超时,返回false,此方法会响应线程中断操作。
public void countDown():让计数器减1
CountDownLatch(作为一个参数,锁传递到方法中)使用步骤:
- 创建CountDownLatch对象
- 调用其实例方法 await(),让当前线程等待
- 调用 countDown()方法,让计数器减1
- 当计数器变为0的时候, await()方法会返回
假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要统计解析总耗时。分析一下:解析每个sheet耗时可能不一样,总耗时就是最长耗时的那个操作。
方法一:使用join实现。
此方法会让当前线程等待被调用的线程完成之后才能继续。可以看一下join的源码,内部其实是在synchronized方法中调用了线程的wait方法,最后被调用的线程执行完毕之后,由jvm自动调用其notifyAll()方法,唤醒所有等待中的线程。这个notifyAll()方法是由jvm内部自动调用的,jdk源码中是看不到的,需要看jvm源码,有兴趣的同学可以去查一下。所以JDK不推荐在线程上调用wait、notify、notifyAll方法。
方法二:使用CountDownLatch实现。
示例:
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(2);
T1 t1 = new T1("sheet1", 3, latch);
T1 t2 = new T1("sheet2", 5, latch);
t1.start();
t2.start();
//调用await()会让当前线程等待,直到计数器为0的时候,方法才会返回,此方法会响应线程中断操作。
//latch.await();
//限时等待,在超时之前,计数器变为了0,方法返回true,否者直到超时,返回false,此方法会响应线程中断操作。
boolean result = latch.await(4, TimeUnit.SECONDS);
long end = System.currentTimeMillis();
//System.out.println("主线程结束,耗时" + (end - start));
System.out.println("主线程结束,耗时" + (end - start)+"是否返回结果:"+result);
}
public static class T1 extends Thread {
private int workTime;
private CountDownLatch countDownLatch;
public T1(String name, int workTime, CountDownLatch countDownLatch) {
super(name);
this.workTime = workTime;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
long start = System.currentTimeMillis();
Thread t = Thread.currentThread();
System.out.println(t.getName() + "线程开始执行!");
try {
TimeUnit.SECONDS.sleep(workTime);
long end = System.currentTimeMillis();
System.out.println(t.getName() + "结束,耗时" + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}
}
输出:
sheet1线程开始执行!
sheet2线程开始执行!
sheet1结束,耗时3000
主线程结束,耗时4004是否返回结果:false
sheet2结束,耗时5001
主线程中调用 countDownLatch.await();会让主线程等待,t1、t2线程中模拟执行耗时操作,最终在finally中调用了 countDownLatch.countDown();,此方法每调用一次,CountDownLatch内部计数器会减1,当计数器变为0的时候,主线程中的await()会返回,然后继续执行。注意:上面的 countDown()这个是必须要执行的方法,所以放在finally中执行。
2个CountDown结合使用的示例——跑步比赛耗时统计
有3个人参见跑步比赛,需要先等指令员发指令枪后才能开跑,所有人都跑完之后,指令员喊一声,大家跑完了,计算耗时。
示例:
public class CountDownLatchImplRacingTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch fireGun = new CountDownLatch(1);
CountDownLatch latch = new CountDownLatch(3);
new T1("刘翔", 3, fireGun, latch).start();
new T1("拼嘻嘻", 5, fireGun, latch).start();
new T1("蜡笔小新", 7, fireGun, latch).start();
System.out.println("比赛 wait for ready!");
////主线程休眠3秒,模拟指令员准备发枪耗时操作
TimeUnit.SECONDS.sleep(3);
long start = System.currentTimeMillis();
System.out.println("发令枪响,比赛开始!");
fireGun.countDown();
latch.await();
long end = System.currentTimeMillis();
System.out.println("比赛结束,总耗时" + (end - start));
}
public static class T1 extends Thread {
private int workTime;
private CountDownLatch fireGun;
private CountDownLatch countDownLatch;
public T1(String name, int workTime, CountDownLatch fireGun, CountDownLatch countDownLatch) {
super(name);
this.workTime = workTime;
this.fireGun = fireGun;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
fireGun.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long start = System.currentTimeMillis();
Thread t = Thread.currentThread();
System.out.println(t.getName() + "运动员开始赛跑!");
try {
//模拟耗时操作,休眠workTime秒
TimeUnit.SECONDS.sleep(workTime);
long end = System.currentTimeMillis();
System.out.println(t.getName() + "跑完全程,耗时" + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
}
}
输出:
比赛 wait for ready!
发令枪响,比赛开始!
刘翔运动员开始赛跑!
蜡笔小新运动员开始赛跑!
拼嘻嘻运动员开始赛跑!
刘翔跑完全程,耗时3000
拼嘻嘻跑完全程,耗时5002
蜡笔小新跑完全程,耗时7001
比赛结束,总耗时7001
手写一个并行处理任务的工具类
示例:
public class TaskDisposeUtils {
private static final Integer POOL_SIZE = Integer.max(Runtime.getRuntime().availableProcessors(), 5);
public static void main(String[] args) {
List list = Stream.iterate(1, a -> a + 1).limit(10).collect(Collectors.toList());
try {
TaskDisposeUtils.dispose(list, item -> {
long start = System.currentTimeMillis();
//模拟耗时操作,休眠workTime秒
try {
TimeUnit.SECONDS.sleep(item);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(item + "任务完成,耗时" + (end - start));
});
} catch (InterruptedException e) {
e.printStackTrace();
}
//上面所有任务处理完毕完毕之后,程序才能继续
System.out.println(list + "任务全部执行完毕!");
}
public static void dispose(List taskList, Consumer consumer) throws InterruptedException {
dispose(true, POOL_SIZE, taskList, consumer);
}
private static void dispose(boolean moreThread, Integer poolSize, List taskList, Consumer consumer) throws InterruptedException {
if (CollectionUtils.isEmpty(taskList)) {
return;
}
if (moreThread && poolSize > 1) {
poolSize = Math.min(poolSize, taskList.size());
ExecutorService executorService = null;
try {
executorService = Executors.newFixedThreadPool(poolSize);
CountDownLatch latch = new CountDownLatch(taskList.size());
for (T t : taskList) {
executorService.execute(() -> {
try {
consumer.accept(t);
} finally {
latch.countDown();
}
});
}
latch.await();
} finally {
if (executorService != null) {
executorService.shutdown();
}
}
} else {
for (T t : taskList) {
consumer.accept(t);
}
}
}
}
输出:
1任务完成,耗时1376
2任务完成,耗时2014
3任务完成,耗时3179
4任务完成,耗时4449
5任务完成,耗时5000
6任务完成,耗时6001
7任务完成,耗时7000
8任务完成,耗时8000
9任务完成,耗时9000
10任务完成,耗时10001
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]任务全部执行完毕!
在实时系统中的使用场景
- 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。
- 开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。
- 死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁
疑问:
Q:这个notifyAll()方法是由jvm内部自动调用的,jdk源码中是看不到的,需要看jvm源码,有兴趣的同学可以去查一下。所以JDK不推荐在线程上调用wait、notify、notifyAll方法。 是因为没有源码所以才不推荐使用wait、notify、notifyAll方法么?还有其他缺点么?
JUC中的循环栅栏CyclicBarrier的6种使用场景
CyclicBarrier简介
CyclicBarrier通常称为循环屏障。它和CountDownLatch很相似,都可以使线程先等待然后再执行。不过CountDownLatch是使一批(一个)线程等待另一批(一个)线程执行完后再执行;而CyclicBarrier只是使等待的线程达到一定数目后再让它们继续执行。故而CyclicBarrier内部也有一个计数器,计数器的初始值在创建对象时通过构造参数指定,如下所示:
public CyclicBarrier(int parties) {
this(parties, null);
}
每调用一次await()方法都将使阻塞的线程数+1,只有阻塞的线程数达到设定值时屏障才会打开,允许阻塞的所有线程继续执行。除此之外,CyclicBarrier还有几点需要注意的地方:
-
CyclicBarrier的计数器可以重置而CountDownLatch不行,这意味着CyclicBarrier实例可以被重复使用而CountDownLatch只能被使用一次。而这也是循环屏障循环二字的语义所在。
-
CyclicBarrier允许用户自定义barrierAction操作,这是个可选操作,可以在创建CyclicBarrier对象时指定
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
一旦用户在创建CyclicBarrier对象时设置了barrierAction参数,则在阻塞线程数达到设定值屏障打开前,会调用barrierAction的run()方法完成用户自定义的操作。
CyclicBarrier内部相当于有个计数器(构造方法传入的),每次调用await();后,计数器会减1,并且await()方法会让当前线程阻塞,等待计数器减为0的时候,所有在await()上等待的线程被唤醒,然后继续向下执行,此时计数器又会被还原为创建时的值,然后可以继续再次使用。
CountDownLatch和CyclicBarrier的区别
CountDownLatch示例
主管相当于 CountDownLatch,干活的小弟相当于做事情的线程。
老板交给主管了一个任务,让主管搞完之后立即上报给老板。主管下面有10个小弟,接到任务之后将任务划分为10个小任务分给每个小弟去干,主管一直处于等待状态(主管会调用await()方法,此方法会阻塞当前线程),让每个小弟干完之后通知一下主管(调用countDown()方法通知主管,此方法会立即返回),主管等到所有的小弟都做完了,会被唤醒,从await()方法上苏醒,然后将结果反馈给老板。期间主管会等待,会等待所有小弟将结果汇报给自己。
而CyclicBarrier是一批线程让自己等待,等待所有的线程都准备好了,所有的线程才能继续。
CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBrrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
重复使用CyclicBarrier、自定义一个所有线程到齐后的处理动作实例:
public class CyclicBarrierTest {
/**
* 可以自定义一个所有线程到齐后的处理动作,再唤醒所有线程工作
*/
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{
System.out.println("人都到齐了,大家high起来!");
});
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new T1("驴友"+i, i).start();
}
}
public static class T1 extends Thread {
private int workTime;
public T1(String name, int workTime) {
super(name);
this.workTime = workTime;
}
@Override
public void run() {
//等待人齐吃饭
eat();
//等待人齐上车下一站旅游
travel();
}
private void eat(){
Thread t = Thread.currentThread();
//System.out.println(t.getName() + "号旅客开始准备吃饭!");
try {
TimeUnit.SECONDS.sleep(workTime);
long start = System.currentTimeMillis();
cyclicBarrier.await();
long end = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客吃饭了,sleep:"+workTime+",等待耗时" + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e1) {
e1.printStackTrace();
}
}
private void travel(){
Thread t = Thread.currentThread();
//System.out.println(t.getName() + "号旅客开始准备吃饭!");
try {
TimeUnit.SECONDS.sleep(workTime);
long start = System.currentTimeMillis();
cyclicBarrier.await();
long end = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客上车了,sleep:"+workTime+",等待耗时" + (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e1) {
e1.printStackTrace();
}
}
}
}
输出:
人都到齐了,大家high起来!
驴友5号旅客吃饭了,sleep:5,等待耗时0
驴友0号旅客吃饭了,sleep:0,等待耗时5002
驴友1号旅客吃饭了,sleep:1,等待耗时4000
驴友4号旅客吃饭了,sleep:4,等待耗时1000
驴友3号旅客吃饭了,sleep:3,等待耗时2000
驴友2号旅客吃饭了,sleep:2,等待耗时3000
人都到齐了,大家high起来!
驴友5号旅客上车了,sleep:5,等待耗时0
驴友4号旅客上车了,sleep:4,等待耗时999
驴友3号旅客上车了,sleep:3,等待耗时2000
驴友2号旅客上车了,sleep:2,等待耗时3000
驴友1号旅客上车了,sleep:1,等待耗时3999
驴友0号旅客上车了,sleep:0,等待耗时5000
其中一个线程被interrupt()打断实例:
public class CyclicBarrierBreakTest {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
public static class T1 extends Thread {
private int workTime;
public T1(String name, int workTime) {
super(name);
this.workTime = workTime;
}
@Override
public void run() {
//等待人齐吃饭
long start = 0, end = 0;
Thread t = Thread.currentThread();
try {
TimeUnit.SECONDS.sleep(workTime);
start = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客开始准备吃饭!");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
end = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客吃饭了,sleep:" + workTime + ",等待耗时" + (end - start));
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 6; i++) {
int sleep = 0;
//如果线程只是在睡眠过程时,中断的就不是cyclicBarrier.await();触发的,而是 TimeUnit.SECONDS.sleep(workTime);这时候就达不到效果
T1 t = new T1("驴友" + i, sleep);
t.start();
if (i == 3) {
TimeUnit.SECONDS.sleep(1);
System.out.println(t.getName() + ",有点急事,我先吃了!");
t.interrupt();
TimeUnit.SECONDS.sleep(2);
}
}
}
}
输出:
驴友2号旅客开始准备吃饭!
驴友1号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
驴友3,有点急事,我先吃了!
驴友3号旅客吃饭了,sleep:0,等待耗时1003
驴友2号旅客吃饭了,sleep:0,等待耗时1004
驴友1号旅客吃饭了,sleep:0,等待耗时1004
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(A
java.util.concurrent.BrokenBarrierException
驴友4号旅客开始准备吃饭!
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友5号旅客开始准备吃饭!
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
驴友6号旅客开始准备吃饭!
at com.self.current.CyclicBarrierBreakTest$T1.run(CyclicBarrierBreakTest.java:38)
驴友4号旅客吃饭了,sleep:0,等待耗时0
java.util.concurrent.BrokenBarrierException
驴友6号旅客吃饭了,sleep:0,等待耗时0
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友5号旅客吃饭了,sleep:0,等待耗时1
java.util.concurrent.BrokenBarrierException
结论:
- 内部有一个人把规则破坏了(接收到中断信号),其他人都不按规则来了,不会等待了
- 接收到中断信号的线程,await方法会触发InterruptedException异常,然后被唤醒向下运行
- 其他等待中 或者后面到达的线程,会在await()方法上触发BrokenBarrierException异常,然后继续执行
其中一个线程执行cyclicBarrier.await(2, TimeUnit.SECONDS);只执行超时等待2秒:
结论:
-
等待超时的方法
public int await(long timeout, TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException -
内部有一个人把规则破坏了(等待超时),其他人都不按规则来了,不会等待了
-
等待超时的线程,await方法会触发TimeoutException异常,然后被唤醒向下运行
-
其他等待中或者后面到达的线程,会在await()方法上触发BrokenBarrierException异常,然后继续执行
重建规则示例:
第一次规则被打乱了,过了一会导游重建了规则(cyclicBarrier.reset();),接着又重来来了一次模拟等待吃饭的操作,正常了。
public class CyclicBarrierResetTest {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
private static boolean onOrder = false;
public static class T1 extends Thread {
private int workTime;
public T1(String name, int workTime) {
super(name);
this.workTime = workTime;
}
@Override
public void run() {
//等待人齐吃饭
long start = 0, end = 0;
Thread t = Thread.currentThread();
try {
TimeUnit.SECONDS.sleep(workTime);
start = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客开始准备吃饭!");
if (!onOrder) {
if (this.getName().equals("驴友1")) {
cyclicBarrier.await(2, TimeUnit.SECONDS);
} else {
cyclicBarrier.await();
}
} else {
cyclicBarrier.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (TimeoutException e){
e.printStackTrace();
}
end = System.currentTimeMillis();
System.out.println(t.getName() + "号旅客吃饭了,sleep:" + workTime + ",等待耗时" + (end - start));
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 6; i++) {
T1 t = new T1("驴友" + i, i);
t.start();
}
//等待7秒之后,重置,重建规则
TimeUnit.SECONDS.sleep(7);
cyclicBarrier.reset();
onOrder = true;
System.out.println("---------------重新按按规则来,不遵守规则的没饭吃!------------------");
//再来一次
for (int i = 1; i <= 6; i++) {
T1 t = new T1("驴友" + i, i);
t.start();
}
}
}
输出:
驴友1号旅客开始准备吃饭!
驴友2号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
java.util.concurrent.BrokenBarrierException
驴友3号旅客吃饭了,sleep:3,等待耗时3
java.util.concurrent.TimeoutException
驴友1号旅客吃饭了,sleep:1,等待耗时2005
java.util.concurrent.BrokenBarrierException
驴友2号旅客吃饭了,sleep:2,等待耗时1006
java.util.concurrent.BrokenBarrierException
驴友4号旅客开始准备吃饭!
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驴友4号旅客吃饭了,sleep:4,等待耗时0
驴友5号旅客开始准备吃饭!
驴友5号旅客吃饭了,sleep:5,等待耗时0
java.util.concurrent.BrokenBarrierException
java.util.concurrent.BrokenBarrierException
驴友6号旅客开始准备吃饭!
驴友6号旅客吃饭了,sleep:6,等待耗时0
---------------重新按按规则来,不遵守规则的没饭吃!------------------
驴友1号旅客开始准备吃饭!
驴友2号旅客开始准备吃饭!
驴友3号旅客开始准备吃饭!
驴友4号旅客开始准备吃饭!
驴友5号旅客开始准备吃饭!
驴友6号旅客开始准备吃饭!
驴友6号旅客吃饭了,sleep:6,等待耗时0
驴友5号旅客吃饭了,sleep:5,等待耗时1000
驴友4号旅客吃饭了,sleep:4,等待耗时2000
驴友3号旅客吃饭了,sleep:3,等待耗时3000
驴友2号旅客吃饭了,sleep:2,等待耗时3999
驴友1号旅客吃饭了,sleep:1,等待耗时5000
JAVA线程池
线程池实现原理
类似于一个工厂的运作。
当向线程池提交一个任务之后,线程池的处理流程如下:
- 判断是否达到核心线程数,若未达到,则直接创建新的线程处理当前传入的任务,否则进入下个流程
- 线程池中的工作队列是否已满,若未满,则将任务丢入工作队列中先存着等待处理,否则进入下个流程
- 是否达到最大线程数,若未达到,则创建新的线程处理当前传入的任务,否则交给线程池中的饱和策略进行处理。
java中的线程池
jdk中提供了线程池的具体实现,实现类是:java.util.concurrent.ThreadPoolExecutor,主要构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:核心线程大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使有其他空闲线程可以处理任务也会创新线程,等到工作的线程数大于核心线程数时就不会在创建了。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前把核心线程都创造好,并启动。(prestartCoreThread:启动一个核心线程或 prestartAllCoreThreads:启动全部核心线程 )
maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且以创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果我们使用了无界队列(或者大小是Integer.MAX_VALUE,可能还没达到就OOM了),那么所有的任务会加入队列,这个参数就没有什么效果了。
keepAliveTime:线程池的工作线程空闲后,保持存活的时间。如果没有任务处理了,有些线程会空闲,空闲的时间超过了这个值,会被回收掉。如果任务很多,并且每个任务的执行时间比较短,避免线程重复创建和回收,可以调大这个时间,提高线程的利用率
unit:keepAliveTIme的时间单位,可以选择的单位有天、小时、分钟、毫秒、微妙、千分之一毫秒和纳秒。类型是一个枚举java.util.concurrent.TimeUnit,这个枚举也经常使用,有兴趣的可以看一下其源码
workQueue:工作队列,用于缓存待处理任务的阻塞队列,常见的有4种(ArrayBlockingQueue 、LinkedBlockingQueue 、SynchronousQueue 、PriorityBlockingQueue )
threadFactory:线程池中创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
handler:饱和策略,当线程池无法处理新来的任务了,那么需要提供一种策略处理提交的新任务,默认有4种策略(AbortPolicy 、CallerRunsPolicy 、DiscardOldestPolicy、DiscardPolicy )
调用线程池的execute方法处理任务,执行execute方法的过程:
- 判断线程池中运行的线程数是否小于corepoolsize,是:则创建新的线程来处理任务,否:执行下一步
- 试图将任务添加到workQueue指定的队列中,如果无法添加到队列,进入下一步
- 判断线程池中运行的线程数是否小于maximumPoolSize,是:则新增线程处理当前传入的任务,否:将任务传递给handler对象rejectedExecution方法处理
线程池的使用步骤:
- 调用构造方法创建线程池
- 调用线程池的方法处理任务
- 关闭线程池
线程池中常见5种工作队列
任务太多的时候,工作队列用于暂时缓存待处理的任务,jdk中常见的4种阻塞队列:
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按照先进先出原则对元素进行排序
LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按照先进先出排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool使用了这个队列。
SynchronousQueue :一个不存储元素的阻塞队列,每个插入操作必须等到另外一个线程调用移除操作,否则插入操作一直处理阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用这个队列。
PriorityBlockingQueue:优先级队列,进入队列的元素按照优先级会进行排序。
SynchronousQueue队列的线程池
使用Executors.newCachedThreadPool()创建线程池,看一下的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
newCachedThreadPool()使用了SynchronousQueue同步队列,这种队列比较特殊,放入元素必须要有另外一个线程去获取这个元素,否则放入元素会失败或者一直阻塞在那里直到有线程取走,示例中任务处理休眠了指定的时间,导致已创建的工作线程都忙于处理任务,所以新来任务之后,将任务丢入同步队列会失败,丢入队列失败之后,会尝试新建线程处理任务。使用上面的方式创建线程池需要注意,如果需要处理的任务比较耗时,会导致新来的任务都会创建新的线程进行处理,可能会导致创建非常多的线程,最终耗尽系统资源,触发OOM。
//SynchronousQueue队列默认是false,采用先进后出的栈处理,也可以是公平队列先进先出。
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue() : new TransferStack();
}
PriorityBlockingQueue优先级队列的线程池
输出中,除了第一个任务,其他任务按照优先级高低按顺序处理。原因在于:创建线程池的时候使用了优先级队列,进入队列中的任务会进行排序,任务的先后顺序由Task中的i变量决定。向PriorityBlockingQueue加入元素的时候,内部会调用代码中Task的compareTo方法决定元素的先后顺序。
示例:
public class ThreadPoolExecutorPriorityTest {
/**
* 优先级队列执行的任务要实现Comparable比较
*/
static class Task implements Runnable, Comparable {
private int i;
private String name;
public Task(int i, String name) {
this.i = i;
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "处理" + this.name);
}
@Override
public int compareTo(Task o) {
return Integer.compare(o.i, this.i);
}
}
//自定义线程工厂,优先级队列
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 60,
TimeUnit.SECONDS, new PriorityBlockingQueue<>(), new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
int j = i;
String taskName = "task" + j;
executor.execute(new Task(j,taskName));
}
for (int i = 90; i <= 100; i++) {
int j = i;
String taskName = "task" + j;
executor.execute(new Task(j,taskName));
}
executor.shutdown();
}
}
输出:
From DemoThreadFactory's 订单创建组-Worker-1处理task1
From DemoThreadFactory's 订单创建组-Worker-1处理task100
From DemoThreadFactory's 订单创建组-Worker-1处理task99
From DemoThreadFactory's 订单创建组-Worker-1处理task98
From DemoThreadFactory's 订单创建组-Worker-1处理task97
From DemoThreadFactory's 订单创建组-Worker-1处理task96
From DemoThreadFactory's 订单创建组-Worker-1处理task95
From DemoThreadFactory's 订单创建组-Worker-1处理task94
From DemoThreadFactory's 订单创建组-Worker-1处理task93
From DemoThreadFactory's 订单创建组-Worker-1处理task92
From DemoThreadFactory's 订单创建组-Worker-1处理task91
From DemoThreadFactory's 订单创建组-Worker-1处理task90
From DemoThreadFactory's 订单创建组-Worker-1处理task10
From DemoThreadFactory's 订单创建组-Worker-1处理task9
From DemoThreadFactory's 订单创建组-Worker-1处理task8
From DemoThreadFactory's 订单创建组-Worker-1处理task7
From DemoThreadFactory's 订单创建组-Worker-1处理task6
From DemoThreadFactory's 订单创建组-Worker-1处理task5
From DemoThreadFactory's 订单创建组-Worker-1处理task4
From DemoThreadFactory's 订单创建组-Worker-1处理task3
From DemoThreadFactory's 订单创建组-Worker-1处理task2
自定义创建线程的工厂
给线程池中线程起一个有意义的名字,在系统出现问题的时候,通过线程堆栈信息可以更容易发现系统中问题所在。通过jstack查看线程的堆栈信息,也可以看到我们自定义的名称 。
自定义创建工厂需要实现java.util.concurrent.ThreadFactory接口中的Thread newThread(Runnable r)方法,参数为传入的任务,需要返回一个工作线程。
示例:
public class ThreadPoolExecutorTest {
//默认线程创建
/* private static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(15), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());*/
//自定义线程工厂1
/* private static final AtomicInteger nextId = new AtomicInteger(1);
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(12), (r -> {
Thread t = new Thread(r);
t.setName("示范线程" + nextId.getAndIncrement());
return t;
}), new ThreadPoolExecutor.AbortPolicy());*/
//自定义线程工厂2 ,推荐
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(15), new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
//提前启动所有核心线程
executor.prestartAllCoreThreads();
//提前启动一个核心线程
executor.prestartCoreThread();
for (int i = 1; i <= 20; i++) {
int j = i;
String taskName = "task" + j;
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(j);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程执行" + taskName + "完毕!");
});
}
executor.shutdown();
}
}
输出:
From DemoThreadFactory's 订单创建组-Worker-1线程执行task1完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task2完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task3完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task4完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task5完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task6完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task7完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task8完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task9完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task10完毕!
From DemoThreadFactory's 订单创建组-Worker-6线程执行task17完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task11完毕!
From DemoThreadFactory's 订单创建组-Worker-7线程执行task20完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task12完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task13完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task14完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task15完毕!
四种常见饱和策略
当线程池中队列已满,并且线程池已达到最大线程数,线程池会将任务传递给饱和策略进行处理。这些策略都实现了RejectedExecutionHandler接口。接口中有个方法:
void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
参数说明:
r:需要执行的任务
executor:当前线程池对象
JDK中提供了4种常见的饱和策略:
AbortPolicy:直接抛出异常。
CallerRunsPolicy:在当前调用者的线程中运行任务,即随丢来的任务,由他自己去处理。
DiscardOldestPolicy:丢弃队列中最老的一个任务,即丢弃队列头部的一个任务,然后执行当前传入的任务。
DiscardPolicy:不处理,直接丢弃掉,方法内部为空。
解释:
//自定义线程工厂
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.CallerRunsPolicy());
AbortPolicy:直接抛出异常。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
输出:到饱和策略时抛出异常记录,丢弃掉任务11个。
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.self.current.ThreadPoolExecutorTest$$Lambda$1/1915503092@50134894 rejected from java.util.concurrent.ThreadPoolExecutor@2957fcb0[Running, pool size = 5, active threads = 4, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.self.current.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:47)
From DemoThreadFactory's 订单创建组-Worker-1线程执行task1完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task2完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task6完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task3完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task7完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task8完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task9完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task4完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task5完毕!
CallerRunsPolicy:在当前调用者的线程中运行任务,即随丢来的任务,由他自己去处理。如main方法调用的线程池,则如果走到饱和策略处理时,由main方法处理这个任务。不会丢弃任何一个任务,但执行会变得很慢。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
输出:
From DemoThreadFactory's 订单创建组-Worker-1线程执行task1完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task2完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task6完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task3完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task8完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task9完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task4完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task10完毕!
main线程执行task11完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task5完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task7完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task12完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task13完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task14完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task15完毕!
main线程执行task17完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task16完毕!
DiscardOldestPolicy:丢弃队列中最老的一个任务,即丢弃队列头部的一个任务,然后执行当前传入的任务。这时候线程池会在执行到饱和策略时丢弃掉头部最老的认为,没有任何记录,任务就丢掉了。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
输出:20个任务被无声无息地丢掉了11个
From DemoThreadFactory's 订单创建组-Worker-2线程执行task6完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task7完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task8完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task9完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task16完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task17完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task18完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task19完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task20完毕!
DiscardPolicy:不处理,直接丢弃掉,方法内部为空。没处理Runnable r就表示丢弃了。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
输出:20个任务被无声无息地丢掉了10个
From DemoThreadFactory's 订单创建组-Worker-1线程执行task1完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task2完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task3完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task7完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task8完毕!
From DemoThreadFactory's 订单创建组-Worker-4线程执行task9完毕!
From DemoThreadFactory's 订单创建组-Worker-5线程执行task10完毕!
From DemoThreadFactory's 订单创建组-Worker-1线程执行task4完毕!
From DemoThreadFactory's 订单创建组-Worker-2线程执行task5完毕!
From DemoThreadFactory's 订单创建组-Worker-3线程执行task6完毕!
自定义饱和策略
需要实现RejectedExecutionHandler接口。任务无法处理的时候,我们想记录一下日志,我们需要自定义一个饱和策略。记录了任务的日志,对于无法处理多任务,我们最好能够记录一下,让开发人员能够知道。 任务进入了饱和策略,说明线程池的配置可能不是太合理,或者机器的性能有限,需要做一些优化调整。
实例:
public class ThreadPoolExecutorRejectHandlerTest {
static class Task implements Runnable {
String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "处理" + this.name);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "Task{" +
"name='" + name + '\'' +
'}';
}
}
//自定义包含策略:可以直接用函数式方法定义,也可以实现RejectedExecutionHandler自定义
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), (r,executor)->{
//自定义饱和策略
//记录一下无法处理的任务
System.out.println("无法处理的任务:" + r.toString());
});
public static void main(String[] args) {
//提前启动所有核心线程
executor.prestartAllCoreThreads();
//提前启动一个核心线程
executor.prestartCoreThread();
for (int i = 1; i <= 20; i++) {
int j = i;
String taskName = "task" + j;
executor.execute(new Task(taskName));
}
executor.shutdown();
}
}
输出:
无法处理的任务:Task{name='task10'}
无法处理的任务:Task{name='task11'}
无法处理的任务:Task{name='task12'}
无法处理的任务:Task{name='task13'}
无法处理的任务:Task{name='task14'}
无法处理的任务:Task{name='task15'}
无法处理的任务:Task{name='task16'}
无法处理的任务:Task{name='task17'}
无法处理的任务:Task{name='task18'}
无法处理的任务:Task{name='task19'}
无法处理的任务:Task{name='task20'}
From DemoThreadFactory's 订单创建组-Worker-1处理task1
From DemoThreadFactory's 订单创建组-Worker-2处理task6
From DemoThreadFactory's 订单创建组-Worker-3处理task7
From DemoThreadFactory's 订单创建组-Worker-4处理task8
From DemoThreadFactory's 订单创建组-Worker-5处理task9
From DemoThreadFactory's 订单创建组-Worker-2处理task2
From DemoThreadFactory's 订单创建组-Worker-1处理task3
From DemoThreadFactory's 订单创建组-Worker-4处理task5
From DemoThreadFactory's 订单创建组-Worker-3处理task4
线程池中的2个关闭方法
线程池提供了2个关闭方法:shutdown和shutdownNow,当调用者两个方法之后,线程池会遍历内部的工作线程,然后调用每个工作线程的interrrupt方法给线程发送中断信号,内部如果无法响应中断信号的可能永远无法终止,所以如果内部有无线循环的,最好在循环内部检测一下线程的中断信号,合理的退出。调用者两个方法中任意一个,线程池的isShutdown方法(是否执行了关闭线程池命令)就会返回true,当所有的任务线程都关闭之后,才表示线程池关闭成功,这时调用isTerminaed方法(是否关闭成功)会返回true。
调用shutdown方法之后,线程池将不再接受新任务,内部会将所有已提交的任务处理完毕,处理完毕之后,工作线程自动退出。
而调用shutdownNow方法后,线程池会将还未处理的(在队里等待处理的任务)任务移除,将正在处理中的处理完毕之后,工作线程自动退出。
至于调用哪个方法来关闭线程,应该由提交到线程池的任务特性决定,多数情况下调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
扩展线程池
ThreadPoolExecutor内部提供了几个方法beforeExecute、afterExecute、terminated,可以由开发人员自己去重写实现这些方法。
看一下线程池内部的源码:
try {
beforeExecute(wt, task);//任务执行之前调用的方法
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x;
throw x;
} catch (Error x) {
thrown = x;
throw x;
} catch (Throwable x) {
thrown = x;
throw new Error(x);
} finally {
afterExecute(task, thrown);//任务执行完毕之后调用的方法
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
beforeExecute:任务执行之前调用的方法,有2个参数,第1个参数是执行任务的线程,第2个参数是任务
protected void beforeExecute(Thread t, Runnable r) { }
afterExecute:任务执行完成之后调用的方法,2个参数,第1个参数表示任务,第2个参数表示任务执行时的异常信息,如果无异常,第二个参数为null
protected void afterExecute(Runnable r, Throwable t) { }
terminated:线程池最终关闭之后调用的方法。所有的工作线程都退出了,最终线程池会退出,退出时调用该方法
实例:
public class ThreadPoolExecutorExtensionTest {
static class Task implements Runnable {
String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "处理" + this.name);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "Task{" +
"name='" + name + '\'' +
'}';
}
}
//扩展线程池,可以继承也可以直接重写
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(15),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy()){
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println(t.getName() + ",开始执行任务:" + r.toString());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println(Thread.currentThread().getName() + ",任务:" + r.toString() + ",执行完毕!");
}
@Override
protected void terminated() {
System.out.println(Thread.currentThread().getName() + ",关闭线程池!");
}
};
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
int j = i;
String taskName = "task" + j;
executor.execute(new Task(taskName));
}
executor.shutdown();
}
}
输出:
From DemoThreadFactory's 订单创建组-Worker-1,开始执行任务:Task{name='task1'}
From DemoThreadFactory's 订单创建组-Worker-1处理task1
From DemoThreadFactory's 订单创建组-Worker-2,开始执行任务:Task{name='task2'}
From DemoThreadFactory's 订单创建组-Worker-2处理task2
From DemoThreadFactory's 订单创建组-Worker-3,开始执行任务:Task{name='task3'}
From DemoThreadFactory's 订单创建组-Worker-3处理task3
From DemoThreadFactory's 订单创建组-Worker-1,任务:Task{name='task1'},执行完毕!
From DemoThreadFactory's 订单创建组-Worker-2,任务:Task{name='task2'},执行完毕!
From DemoThreadFactory's 订单创建组-Worker-3,任务:Task{name='task3'},执行完毕!
From DemoThreadFactory's 订单创建组-Worker-3,关闭线程池!
合理地配置线程池
要想合理的配置线程池,需要先分析任务的特性,可以冲一下四个角度分析:
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务
- 任务的优先级:高、中、低
- 任务的执行时间:长、中、短
- 任务的依赖性:是否依赖其他的系统资源,如数据库连接。
性质不同任务可以用不同规模的线程池分开处理。CPU密集型任务应该尽可能小的线程,如配置cpu数量+1个线程的线程池。由于IO密集型任务并不是一直在执行任务,不能让cpu闲着,则应配置尽可能多的线程,如:cup数量*2。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这2个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。可以通过Runtime.getRuntime().availableProcessors()方法获取cpu数量。优先级不同任务可以对线程池采用优先级队列来处理,让优先级高的先执行。
使用队列的时候建议使用有界队列,有界队列增加了系统的稳定性,如果采用无界队列,任务太多的时候可能导致系统OOM,直接让系统宕机。
线程池中线程数量的配置
线程池中总线程大小对系统的性能有一定的影响,我们的目标是希望系统能够发挥最好的性能,过多或者过小的线程数量无法有效的使用机器的性能。在Java Concurrency in Practice书中给出了估算线程池大小的公式:
Ncpu = CUP的数量
Ucpu = 目标CPU的使用率,0<=Ucpu<=1
W/C = 等待时间与计算时间的比例
为保存处理器达到期望的使用率,最优的线程池的大小等于:
Nthreads = Ncpu × Ucpu × (1+W/C)
线程池数量 = CUP的数量 * 目标CPU的使用率 * 等待时间与计算时间的比例
使用建议
在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1) FixedThreadPool和SingleThreadPool:
允许的请求队列:LinkedBlockingQueue长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2) CachedThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
疑问:
Q:LinkedBlockingQueue吞吐量通常要高于ArrayBlockingQueue,为什么?
JUC中的Executor框架
Excecutor框架主要包含3部分的内容:
- 任务相关的:包含被执行的任务要实现的接口:Runnable接口或Callable接口
- 任务的执行相关的:包含任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架中有两个关键的类实现了ExecutorService接口(ThreadPoolExecutor和ScheduleThreadPoolExecutor)
- 异步计算结果相关的:包含接口Future和实现Future接口的FutureTask类
Executors框架包括:
- Executor
- ExecutorService
- ThreadPoolExecutor
- Executors
- Future
- Callable
- FutureTask
- CompletableFuture
- CompletionService
- ExecutorCompletionService
Executor接口
Executor接口中定义了方法execute(Runable able)接口,该方法接受一个Runable实例,他来执行一个任务,任务即实现一个Runable接口的类。
ExecutorService接口
ExecutorService继承于Executor接口,他提供了更为丰富的线程实现方法,比如ExecutorService提供关闭自己的方法,以及为跟踪一个或多个异步任务执行状况而生成Future的方法。
ExecutorService有三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入了关闭状态,此时意味着ExecutorService不再接受新的任务,但是他还是会执行已经提交的任务,当所有已经提交了的任务执行完后,便达到终止状态。如果不调用shutdown方法,ExecutorService方法会一直运行下去,系统一般不会主动关闭。
ThreadPoolExecutor类
线程池类,实现了ExecutorService接口中所有方法,参考线程池的使用。
ScheduleThreadPoolExecutor定时器
ScheduleThreadPoolExecutor继承自ThreadPoolExecutor(实现了线程池的核心功能),实现了ScheduledExecutorService(实现了定时器调度功能),他主要用来延迟执行任务,或者定时执行任务。功能和Timer类似,但是ScheduleThreadPoolExecutor更强大、更灵活一些。Timer后台是单个线程,而ScheduleThreadPoolExecutor可以在创建的时候指定多个线程。
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
public ScheduledFuture schedule(Callable callable,
long delay, TimeUnit unit);
}
schedule:延迟执行任务1次
使用ScheduleThreadPoolExecutor的schedule方法,看一下这个方法的声明:
public ScheduledFuture> schedule(Runnable command, long delay, TimeUnit unit)
3个参数:
command:需要执行的任务
delay:需要延迟的时间
unit:参数2的时间单位,是个枚举,可以是天、小时、分钟、秒、毫秒、纳秒等
实例:
//只延迟调度一次
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3,
new DemoThreadFactory("延迟调度线程池"));
scheduledThreadPool.schedule(()->{
System.out.println(System.currentTimeMillis()+"开始执行调度!");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()+"执行调度结束!");
},3,TimeUnit.SECONDS);
}
输出:
1598509985652开始执行调度!
1598509990653执行调度结束!
scheduleAtFixedRate:固定的频率执行任务
使用ScheduleThreadPoolExecutor的scheduleAtFixedRate方法,该方法设置了执行周期,下一次执行时间相当于是上一次的执行时间加上period,任务每次执行完毕之后才会计算下次的执行时间。
看一下这个方法的声明:
public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
4个参数:
command:表示要执行的任务
initialDelay:表示延迟多久执行第一次
period:连续执行之间的时间间隔
unit:参数2和参数3的时间单位,是个枚举,可以是天、小时、分钟、秒、毫秒、纳秒等
假设系统调用scheduleAtFixedRate的时间是T1,那么执行时间如下:
第1次:T1+initialDelay
第2次:T1+initialDelay+period(这时候如果第一次执行完后时间大于固定频率的时间,就会被马上调度起来)
第3次:T1+initialDelay+2*period
第n次:T1+initialDelay+(n-1)*period
实例:
//scheduleAtFixedRate()表示每次方法的执行周期是多久关注的是执行周期,如果已经到了执行周期,就会立即开启调度任务,时间间隔是调度任务开始时间加周期
public static void main2(String[] args) throws ExecutionException, InterruptedException {
//任务执行计数器
AtomicInteger count = new AtomicInteger(1);
ScheduledExecutorService scheduledThreadPool = new ScheduledThreadPoolExecutor(3,
new DemoThreadFactory("延迟调度线程池"),new ThreadPoolExecutor.AbortPolicy());
ScheduledFuture> schedule = scheduledThreadPool.scheduleAtFixedRate(() -> {
int currCount = count.getAndIncrement();
System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "开始执行");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "结束执行");
}, 1,3, TimeUnit.SECONDS);
}
输出:
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:36:17 CST 2020 第1次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:36:22 CST 2020 第1次结束执行
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:36:22 CST 2020 第2次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:36:27 CST 2020 第2次结束执行
From DemoThreadFactory's 延迟调度线程池-Worker-2:Thu Aug 27 14:36:27 CST 2020 第3次开始执行
任务当前执行完毕之后会计算下次执行时间,下次执行时间为上次执行的开始时间+period,这个时间小于第一次结束的时间了,说明小于系统当前时间了,会立即执行。
scheduleWithFixedDelay:固定的间隔执行任务
使用ScheduleThreadPoolExecutor的scheduleWithFixedDelay方法,该方法设置了执行周期,与scheduleAtFixedRate方法不同的是,下一次执行时间是上一次任务执行完的系统时间加上period,因而具体执行时间不是固定的,但周期是固定的,是采用相对固定的延迟来执行任务。看一下这个方法的声明:
public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
4个参数:
command:表示要执行的任务
initialDelay:表示延迟多久执行第一次
period:表示下次执行时间和上次执行结束时间之间的间隔时间
unit:参数2和参数3的时间单位,是个枚举,可以是天、小时、分钟、秒、毫秒、纳秒等
假设系统调用scheduleAtFixedRate的时间是T1,那么执行时间如下:
第1次:T1+initialDelay,执行结束时间:E1(执行结束时间是不固定的)
第2次:E1+period,执行结束时间:E2
第3次:E2+period,执行结束时间:E3
第4次:E3+period,执行结束时间:E4
第n次:上次执行结束时间+period
实例:
//scheduleWithFixedDelay()表示每次方法执行完后延迟多久执行,关注的是延迟时间,时间间隔是调度任务结束时间加延迟时间
public static void main(String[] args) throws ExecutionException, InterruptedException {
//任务执行计数器
AtomicInteger count = new AtomicInteger(1);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4,
new DemoThreadFactory("延迟调度线程池"));
ScheduledFuture> schedule = scheduledThreadPool.scheduleWithFixedDelay(() -> {
int currCount = count.getAndIncrement();
System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "开始执行");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "结束执行");
}, 1,3, TimeUnit.SECONDS);
}
输出:
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:39:16 CST 2020 第1次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:39:22 CST 2020 第1次结束执行
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:39:25 CST 2020 第2次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:39:30 CST 2020 第2次结束执行
From DemoThreadFactory's 延迟调度线程池-Worker-2:Thu Aug 27 14:39:33 CST 2020 第3次开始执行
延迟1秒之后执行第1次,后面每次的执行时间和上次执行结束时间间隔3秒。
定时任务有异常——没有对异常处理则定时任务会结束
先说补充点知识:schedule、scheduleAtFixedRate、scheduleWithFixedDelay这几个方法有个返回值ScheduledFuture,通过ScheduledFuture可以对执行的任务做一些操作,如判断任务是否被取消、是否执行完成。
再回到上面代码,任务中有个10/0的操作,会触发异常,发生异常之后没有任何现象,被ScheduledExecutorService内部给吞掉了,然后这个任务再也不会执行了,scheduledFuture.isDone()输出true,表示这个任务已经结束了,再也不会被执行了。所以如果程序有异常,开发者自己注意try-catch处理一下,不然跑着跑着发现任务怎么不跑了,也没有异常输出。
实例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
//任务执行计数器
AtomicInteger count = new AtomicInteger(1);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4,
new DemoThreadFactory("延迟调度线程池"));
ScheduledFuture> schedule = scheduledThreadPool.scheduleWithFixedDelay(() -> {
int currCount = count.getAndIncrement();
System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "开始执行");
/* try {
System.out.println(10/0);
} catch (Exception e) {
e.printStackTrace();
}*/
System.out.println(10/0);
System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "结束执行");
}, 1,3, TimeUnit.SECONDS);
TimeUnit.SECONDS.sleep(3);
System.out.println(schedule.isCancelled());
System.out.println(schedule.isDone());
}
输出:
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:45:09 CST 2020 第1次开始执行
false
true
取消定时任务的执行——调用ScheduledFuture的cancel方法
可能任务执行一会,想取消执行,可以调用ScheduledFuture的cancel方法,参数表示是否给任务发送中断信号。
示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
//任务执行计数器
AtomicInteger count = new AtomicInteger(1);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4,
new DemoThreadFactory("延迟调度线程池"));
ScheduledFuture> schedule = scheduledThreadPool.scheduleWithFixedDelay(() -> {
int currCount = count.getAndIncrement();
System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "开始执行");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "结束执行");
}, 1,3, TimeUnit.SECONDS);
TimeUnit.SECONDS.sleep(5);
schedule.cancel(false);
TimeUnit.SECONDS.sleep(1);
System.out.println("任务是否被取消:"+schedule.isCancelled());
System.out.println("任务是否已完成:"+schedule.isDone());
}
}
输出:
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:53:12 CST 2020 第1次开始执行
任务是否被取消:true
任务是否已完成:true
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:53:17 CST 2020 第1次结束执行
Executors类——线程池工具类
Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。常用的方法有:
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。内部使用了无限容量的LinkedBlockingQueue阻塞队列来缓存任务,任务如果比较多,单线程如果处理不过来,会导致队列堆满,引发OOM。
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,在提交新任务,任务将会进入等待队列中等待。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。内部使用了无限容量的LinkedBlockingQueue阻塞队列来缓存任务,任务如果比较多,如果处理不过来,会导致队列堆满,引发OOM。
newCachedThreadPool
public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒处于等待任务到来)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池的最大值是Integer的最大值(2^31-1)。内部使用了SynchronousQueue同步队列来缓存任务,此队列的特性是放入任务时必须要有对应的线程获取任务,任务才可以放入成功。如果处理的任务比较耗时,任务来的速度也比较快,会创建太多的线程引发OOM。
newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
Future、Callable接口
Future、Callable接口需要结合ExecutorService来使用,需要有线程池的支持。
Future接口定义了操作异步异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
Callable接口中定义了需要有返回的任务需要实现的方法。——相当于有返回值的Runnable
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,过了一会才去获取子任务的执行结果。
Future其他方法介绍一下
cancel:取消在执行的任务,参数表示是否对执行的任务发送中断信号,方法声明如下:
boolean cancel(boolean mayInterruptIfRunning);
isCancelled:用来判断任务是否被取消
isDone:判断任务是否执行完毕。
调用线程池的submit方法执行任务,submit参数为Callable接口:表示需要执行的任务有返回值,submit方法返回一个Future对象,Future相当于一个凭证,可以在任意时间拿着这个凭证去获取对应任务的执行结果(调用其get方法),代码中调用了result.get()方法之后,此方法会阻塞当前线程直到任务执行结束。
实例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
String taskName = "task";
Future future = executor.submit(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println(Thread.currentThread().getName() + "线程执行" + taskName + "完毕!");
return "finished";
});
TimeUnit.SECONDS.sleep(1);
//取消正在执行的任务,mayInterruptIfRunning:是否发送中断信息
future.cancel(false);
System.out.println(future.isCancelled());
System.out.println(future.isDone());
//System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",结果:" + future.get());
try {
//超时获取异步任务执行结果
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",结果:" + future.get(10,TimeUnit.SECONDS));
} catch (TimeoutException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
输出:
Exception in thread "main" java.util.concurrent.CancellationException
at java.util.concurrent.FutureTask.report(FutureTask.java:121)
at java.util.concurrent.FutureTask.get(FutureTask.java:206)
at com.self.current.FutureTest.main(FutureTest.java:46)
true
true
FutureTask类
FutureTask除了实现Future接口,还实现了Runnable接口,因此FutureTask可以交给Executor执行,也可以交给线程执行执行(Thread有个Runnable的构造方法),FutureTask表示带返回值结果的任务。线程池的submit方法返回的Future实际类型正是FutureTask对象 .
疑问:
Q:线程池执行submit()方法是如何调用Callable任务的?
A:Callable通过线程池执行的过程,封装为Runnable。线程池执行submit()方法会把Callable包装成FutrueTask对象,此对象实现了Runnable接口,当调用FutrueTask的run方法时,会把其属性中的Callable拿出来执行call()方法。示例代码如下:
public Future submit(Callable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
public void run() {
try {
Callable c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
}
}
Q:多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题。是因为ScheduledExecutorService是多线程么?
A:是因为Timer只有一个线程在运行,while(true)循环不断地从队列中获取任务执行,而当线程被被杀死或者中断时,就相当于关闭了Timer.
Q: ScheduleThreadPoolExecutor定时器并不关心线程数多少,他不是并发的执行多任务,只关心调度一个定时任务,线程数的多少只是影响多个任务再调度时需要多个线程,这样理解对么?
A:我认为这样理解是对的,而这样也可以解释上面Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行的原因,是因为Timer只有一个线程在运行,while(true)循环不断地从队列中获取任务执行,而当线程被被杀死或者中断时,就相当于关闭了Timer.下面是多个任务调度时会创建多个线程去执行。
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:22:22 CST 2020 第1次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-2:Thu Aug 27 14:22:22 CST 2020 第2次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-3:Thu Aug 27 14:22:22 CST 2020 第3次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:22:27 CST 2020 第1次结束执行
From DemoThreadFactory's 延迟调度线程池-Worker-2:Thu Aug 27 14:22:27 CST 2020 第2次结束执行
From DemoThreadFactory's 延迟调度线程池-Worker-3:Thu Aug 27 14:22:27 CST 2020 第3次结束执行
From DemoThreadFactory's 延迟调度线程池-Worker-1:Thu Aug 27 14:22:30 CST 2020 第4次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-4:Thu Aug 27 14:22:30 CST 2020 第5次开始执行
From DemoThreadFactory's 延迟调度线程池-Worker-2:Thu Aug 27 14:22:30 CST 2020 第6次开始执行
CompletionService接口——获取线程池中已经完成的任务
CompletionService相当于一个执行任务的服务,通过submit丢任务给这个服务,服务内部去执行任务,可以通过服务提供的一些方法获取服务中已经完成的任务。
接口内的几个方法:
Future submit(Callable task);
用于向服务中提交有返回结果的任务,并返回Future对象
Future submit(Runnable task, V result);
用户向服务中提交有返回值的任务去执行,并返回Future对象。Runnable会被包装成有返回值的Callable,返回值为传入的result。
Future take() throws InterruptedException;
从服务中返回并移除一个已经完成的任务,如果获取不到,会一致阻塞到有返回值为止。此方法会响应线程中断。
Future poll();
从服务中返回并移除一个已经完成的任务,如果内部没有已经完成的任务,则返回空,此方法会立即响应。
Future poll(long timeout, TimeUnit unit) throws InterruptedException;
尝试在指定的时间内从服务中返回并移除一个已经完成的任务,等待的时间超时还是没有获取到已完成的任务,则返回空。此方法会响应线程中断
通过submit向内部提交任意多个任务,通过take方法可以获取已经执行完成的任务,如果获取不到将等待。
ExecutorCompletionService
ExecutorCompletionService类是CompletionService接口的具体实现。
说一下其内部原理,ExecutorCompletionService创建的时候会传入一个线程池,调用submit方法传入需要执行的任务,任务由内部的线程池来处理;ExecutorCompletionService内部有个阻塞队列,任意一个任务完成之后,会将任务的执行结果(Future类型)放入阻塞队列中,然后其他线程可以调用它take、poll方法从这个阻塞队列中获取一个已经完成的任务,获取任务返回结果的顺序和任务执行完成的先后顺序一致,所以最先完成的任务会先返回。
看一下构造方法:
public ExecutorCompletionService(Executor executor) {
if (executor == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue>();
}
构造方法需要传入一个Executor对象,这个对象表示任务执行器,所有传入的任务会被这个执行器执行。
completionQueue是用来存储任务结果的阻塞队列,默认用采用的是LinkedBlockingQueue,也支持开发自己设置。通过submit传入需要执行的任务,任务执行完成之后,会放入completionQueue中。
任务完成入队操作原理:
还是通过线程池execute()方法执行一个FutureTask包装的Callable任务,FutureTask里的run方法会调用Callable任务call()方法执行具体的认为,并在执行结算后执行set(result);设置返回值操作,而设置返回值操作中的finishCompletion()方法会调用钩子方法done(),ExecutorCompletionService里定义的QueueingFuture继承了FutureTask,重写了钩子方法,把完成的方法入队保存起来了。
场景:买新房了,然后在网上下单买冰箱、洗衣机,电器商家不同,所以送货耗时不一样,然后等他们送货,快递只愿送到楼下,然后我们自己将其搬到楼上的家中。 这时候我们需要根据异步先完成的快递,拿个先到对其获取做处理——搬上楼。
示例:
public class ExecutorCompletionServiceTest {
static class GoodsModel {
//商品名称
String name;
//购物开始时间
long startime;
//送到的时间
long endtime;
public GoodsModel(String name, long startime, long endtime) {
this.name = name;
this.startime = startime;
this.endtime = endtime;
}
@Override
public String toString() {
return name + ",下单时间[" + this.startime + "," + endtime + "],耗时:" + (this.endtime - this.startime);
}
}
/**
* 将商品搬上楼
*
* @param goodsModel
* @throws InterruptedException
*/
static void moveUp(GoodsModel goodsModel) throws InterruptedException {
//休眠5秒,模拟搬上楼耗时
TimeUnit.SECONDS.sleep(5);
System.out.println("将商品搬上楼,商品信息:" + goodsModel);
}
/**
* 模拟下单
*
* @param name 商品名称
* @param costTime 耗时
* @return
*/
static Callable buyGoods(String name, long costTime) {
return () -> {
long startTime = System.currentTimeMillis();
System.out.println(startTime + "购买" + name + "下单!");
//模拟送货耗时
try {
TimeUnit.SECONDS.sleep(costTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime + name + "送到了!");
return new GoodsModel(name, startTime, endTime);
};
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
long st = System.currentTimeMillis();
System.out.println(st + "开始购物!");
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(10),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
ExecutorCompletionService completionService = new ExecutorCompletionService<>(executor);
//异步下单购买
completionService.submit(buyGoods("电视机", 3));
completionService.submit(buyGoods("洗碗机", 5));
executor.shutdown();
for (int i = 0; i < 2; i++) {
//可以获取到最先到的商品
GoodsModel goodsModel = completionService.take().get();
//将最先到的商品送上楼
moveUp(goodsModel);
}
long et = System.currentTimeMillis();
System.out.println(et + "货物已送到家里咯,哈哈哈!");
System.out.println("总耗时:" + (et - st));
}
}
1598583792616开始购物!
1598583792707购买电视机下单!
1598583792708购买洗碗机下单!
1598583795708电视机送到了!
1598583797709洗碗机送到了!
将商品搬上楼,商品信息:电视机,下单时间[1598583792707,1598583795708],耗时:3001
将商品搬上楼,商品信息:洗碗机,下单时间[1598583792708,1598583797709],耗时:5001
1598583805710货物已送到家里咯,哈哈哈!
总耗时:13094
异步执行一批任务,有一个完成立即返回,其他取消——线程池invokeAny ()方法
如果是要返回所有的任务结果,则调用 invokeAll(Collection extends Callable > tasks)方法,invokeAny ()和invokeAll()都有超时调用方法。如果超时时间到了,调用结束后还没有全部完成,会对所有工作线程发送中断信号中断操作。
方法声明如下:
T invokeAny(Collection extends Callable> tasks)
throws InterruptedException, ExecutionException;
示例:
public static void main(String[] args) throws InterruptedException, ExecutionException {
long st = System.currentTimeMillis();
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(10),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
List> list = new ArrayList<>();
int taskCount = 5;
for (int i = taskCount; i > 0; i--) {
int j = i * 2;
String taskName = "任务"+i;
list.add(() -> {
TimeUnit.SECONDS.sleep(j);
System.out.println(taskName+"执行完毕!");
return j;
});
}
//Integer integer = invokeAny(executor, list);
//ExecutorService提供异步执行一批任务,有一个完成立即返回,其他取消
Integer integer = executor.invokeAny(list);
System.out.println("耗时:" + (System.currentTimeMillis() - st) + ",执行结果:" + integer);
executor.shutdown();
}
private static T invokeAny(ThreadPoolExecutor executor, List> list) throws InterruptedException, ExecutionException {
ExecutorCompletionService completionService = new ExecutorCompletionService(executor);
List> futureList = new ArrayList<>();
for (Callable s : list) {
futureList.add(completionService.submit(s));
}
int n = list.size();
try {
for (int i = 0; i < n; ++i) {
T r = completionService.take().get();
if (r != null) {
return r;
}
}
} finally {
for (Future future : futureList) {
future.cancel(true);
}
}
return null;
}
}
输出:
任务1执行完毕!
耗时:2053,执行结果:2
CompletableFuture——当异步任务完成或者发生异常时,自动调用回调对象的回调方法,主线程无需等待获取结果,异步是以守护线程执行的,如果是用线程池作为执行器则不是守护线程
使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。
从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
我们以获取股票价格为例,看看如何使用CompletableFuture:
CompletableFuture的优点是:
- 异步任务结束时,会自动回调某个对象的方法;
- 异步任务出错时,会自动回调某个对象的方法;
- 主线程设置好回调后,不再关心异步任务的执行。
如果只是实现了异步回调机制,我们还看不出CompletableFuture相比Future的优势。CompletableFuture更强大的功能是,多个CompletableFuture可以串行执行,多个CompletableFuture还可以并行执行。
除了anyOf()可以实现“任意个CompletableFuture只要一个成功”,allOf()可以实现“所有CompletableFuture都必须成功”,这些组合操作可以实现非常复杂的异步流程控制。
最后我们注意CompletableFuture的命名规则:
- xxx():表示该方法将继续在已有的线程中执行;
- xxxAsync():表示将异步在线程池中执行。
示例:
public class CompletableFutureTest {
public static void main(String[] args) throws Exception {
// 创建异步执行任务:
CompletableFuture cf = CompletableFuture.supplyAsync(CompletableFutureTest::fetchPrice);
// 如果执行成功:
cf.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 如果执行异常:
cf.exceptionally((e) -> {
e.printStackTrace();
return null;
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}
static Double fetchPrice() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
if (Math.random() < 0.3) {
throw new RuntimeException("fetch price failed!");
}
return 5 + Math.random() * 20;
}
}
定义两个CompletableFuture,第一个CompletableFuture根据证券名称查询证券代码,第二个CompletableFuture根据证券代码查询证券价格,这两个CompletableFuture实现串行操作如下:
public class CompletableFutureSerialTest {
public static void main(String[] args) throws InterruptedException {
//先获取股票代码
CompletableFuture tesla = CompletableFuture.supplyAsync(() -> {
return CompletableFutureSerialTest.queryCode("tesla");
});
//再获取股票代码对应的股价
CompletableFuture priceFuture = tesla.thenApplyAsync((code) -> {
return CompletableFutureSerialTest.fetchPrice(code);
});
//打印结果
priceFuture.thenAccept((price)->{
System.out.println("price: " + price);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(2000);
}
static String queryCode(String name) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
return "601857";
}
static Double fetchPrice(String code) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
return 5 + Math.random() * 20;
}
}
输出:
price: 23.116752498711122
示例:同时从新浪和网易查询证券代码,只要任意一个返回结果,就进行下一步查询价格,查询价格也同时从新浪和网易查询,只要任意一个返回结果,就完成操作。
public class CompletableFutureParallelTest {
public static void main(String[] args) throws InterruptedException {
// 两个CompletableFuture执行异步查询:
CompletableFuture teslaSina = CompletableFuture.supplyAsync(() -> {
return CompletableFutureParallelTest.queryCode("tesla","https://finance.sina.com.cn/code/");
});
CompletableFuture tesla163 = CompletableFuture.supplyAsync(() -> {
return CompletableFutureParallelTest.queryCode("tesla","https://money.163.com/code/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture
java中的CAS
需求:我们开发了一个网站,需要对访问量进行统计,用户每次发一次请求,访问量+1,如何实现呢?
我们在看一下count++操作,count++操作实际上是被拆分为3步骤执行:
1. 获取count的值,记做A:A=count
2. 将A的值+1,得到B:B = A+1
3. 让B赋值给count:count = B
方式2中我们通过加锁的方式让上面3步骤同时只能被一个线程操作,从而保证结果的正确性。
我们是否可以只在第3步加锁,减少加锁的范围,对第3步做以下处理:
获取锁
第三步获取一下count最新的值,记做LV
判断LV是否等于A,如果相等,则将B的值赋给count,并返回true,否者返回false
释放锁
如果我们发现第3步返回的是false,我们就再次去获取count,将count赋值给A,对A+1赋值给B,然后再将A、B的值带入到上面的过程中执行,直到上面的结果返回true为止。
示例:(自己实现一个CAS)
public class CASTest {
private static volatile int count = 0;
private static void request() throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
int execeptVal;
do {
execeptVal = getCount();
} while (!compareAndSet(execeptVal, execeptVal + 1));
}
private static synchronized boolean compareAndSet(int execeptVal, int newVal) {
if (getCount() == execeptVal) {
count = newVal;
return true;
}
return false;
}
public static int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
ExecutorService threadPool = Executors.newCachedThreadPool();
int userCount = 100;
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < userCount; i++) {
threadPool.execute(() -> {
try {
for (int j = 0; j < 10; j++) {
CASTest.request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);
threadPool.shutdown();
}
}
输出:
main,耗时:133,count=1000
代码中用了volatile关键字修饰了count,可以保证count在多线程情况下的可见性。
咱们再看一下代码,compareAndSwap方法,我们给起个简称吧叫CAS.这个方法使用synchronized修饰了,能保证此方法是线程安全的,多线程情况下此方法是串行执行的。方法由两个参数,expectCount:表示期望的值,newCount:表示要给count设置的新值。方法内部通过getCount()获取count当前的值,然后与期望的值expectCount比较,如果期望的值和count当前的值一致,则将新值newCount赋值给count。
再看一下request()方法,方法中有个do-while循环,循环内部获取count当前值赋值给了expectCount,循环结束的条件是compareAndSwap返回true,也就是说如果compareAndSwap如果不成功,循环再次获取count的最新值,然后+1,再次调用compareAndSwap方法,直到compareAndSwap返回成功为止。
代码中相当于将count++拆分开了,只对最后一步加锁了,减少了锁的范围,此代码的性能是不是比方式2快不少,还能保证结果的正确性。大家是不是感觉这个compareAndSwap方法挺好的,这东西确实很好,java中已经给我们提供了CAS的操作,功能非常强大,我们继续向下看。
CAS
CAS,compare and swap的缩写,中文翻译成比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
很多地方说CAS操作是非阻塞的,其实系统底层进行CAS操作的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,所以同一芯片上的其他处理器就暂时不能通过总线访问内存,保证了该指令在多处理器环境下的原子性。总线上锁的,其他线程执行CAS还是会被阻塞一下,只是时间可能会非常短暂,所以说CAS是非阻塞的并不正确,只能说阻塞的时间是非常短的。
java中提供了对CAS操作的支持,具体在sun.misc.Unsafe类中,声明如下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
上面三个方法都是类似的,主要对4个参数做一下说明。
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5:表示需要修改为的新值
悲观锁 (ReentrantLock)VS 乐观锁 (CAS)
synchronized、ReentrantLock这种独占锁属于悲观锁,它是在假设需要操作的代码一定会发生冲突的,执行代码的时候先对代码加锁,让其他线程在外面等候排队获取锁。悲观锁如果锁的时间比较长,会导致其他线程一直处于等待状态,像我们部署的web应用,一般部署在tomcat中,内部通过线程池来处理用户的请求,如果很多请求都处于等待获取锁的状态,可能会耗尽tomcat线程池,从而导致系统无法处理后面的请求,导致服务器处于不可用状态。
除此之外,还有乐观锁,乐观锁的含义就是假设系统没有发生并发冲突,先按无锁方式执行业务,到最后了检查执行业务期间是否有并发导致数据被修改了,如果有并发导致数据被修改了 ,就快速返回失败,这样的操作使系统并发性能更高一些。cas中就使用了这样的操作。
关于乐观锁这块,想必大家在数据库中也有用到过,给大家举个例子,可能以后会用到。
如果你们的网站中有调用支付宝充值接口的,支付宝那边充值成功了会回调商户系统,商户系统接收到请求之后怎么处理呢?假设用户通过支付宝在商户系统中充值100,支付宝那边会从用户账户中扣除100,商户系统接收到支付宝请求之后应该在商户系统中给用户账户增加100,并且把订单状态置为成功。
那我们可以用乐观锁来实现,给订单表加个版本号version,要求每次更新订单数据,将版本号+1,那么上面的过程可以改为:
获取订单信息,将version的值赋值给V_A
if(订单状态==待处理){
开启事务
给用户账户增加100
update影响行数 = update 订单表 set version = version + 1 where id = 订单号 and version = V_A;
if(update影响行数==1){
提交事务
}else{
回滚事务
}
}
返回订单处理成功
上面的update语句相当于我们说的CAS操作,执行这个update语句的时候,多线程情况下,数据库会对当前订单记录加锁,保证只有一条执行成功,执行成功的,影响行数为1,执行失败的影响行数为0,根据影响行数来决定提交还是回滚事务。上面操作还有一点是将事务范围缩小了,也提升了系统并发处理的性能。
CAS 的问题
cas这么好用,那么有没有什么问题呢?
ABA问题
CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大
上面我们说过如果CAS不成功,则会原地循环(自旋操作),如果长时间自旋会给CPU带来非常大的执行开销。并发量比较大的情况下,CAS成功概率可能比较低,可能会重试很多次才会成功。
并发量大的情况下应该改为悲观锁避免自旋带来的CPU的大量开销。
使用JUC中的类实现计数器
juc框架中提供了一些原子操作,底层是通过Unsafe类中的cas操作实现的。通过原子操作可以保证数据在并发情况下的正确性。
示例:
public class CASTest1 {
private static AtomicInteger count = new AtomicInteger(0);
private static void request() throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
count.getAndIncrement();
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
ExecutorService threadPool = Executors.newCachedThreadPool();
int userCount = 100;
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < userCount; i++) {
threadPool.execute(() -> {
try {
for (int j = 0; j < 10; j++) {
CASTest1.request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);
threadPool.shutdown();
}
}
JUC底层工具类Unsafe
juc中大部分类都是依赖于Unsafe来实现的,主要用到了Unsafe中的CAS、线程挂起、线程恢复等相关功能。所以如果打算深入了解JUC原理的,必须先了解一下Unsafe类。
Unsafe类的功能图:
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
从Unsafe功能图上看出,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,本文主要介绍3个常用的操作:CAS、线程调度、对象操作。
看一下UnSafe的源码部分:
public final class Unsafe {
// 单例对象
private static final Unsafe theUnsafe;
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
// 仅在引导类加载器`BootstrapClassLoader`加载时才合法
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
从代码中可以看出,Unsafe类为单例实现,提供静态方法getUnsafe获取Unsafe实例,内部会判断当前调用者是否是由系统类加载器加载的,如果不是系统类加载器加载的,会抛出SecurityException异常。
获取Unsafe的两种方式:
- 可以把我们的类放在jdk的lib目录下,那么启动的时候会自动加载,这种方式不是很好。
- 通过反射可以获取到Unsafe中的theUnsafe字段的值,这样可以获取到Unsafe对象的实例。
通过反射获取Unsafe实例
public class UnsafeTest {
//通过反射获取Unsafe实例
private static Unsafe unsafe;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
//基本上你通过反射得到的字段就像任何其他字段一样,但是当你调用get方法时,你传递的是null,因为没有实例可以作用。
//field.get(null)方法参数传递的是实例,而静态域是没有实例的,获取静态变量直接用field.get(null)。
unsafe = (Unsafe) field.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println(unsafe);
}
}
Unsafe中的CAS操作
看一下Unsafe中CAS相关方法定义:
/**
* CAS 操作
*
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
什么是CAS? 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作,多个线程同时执行cas操作,只有一个会成功。我们都知道,CAS是一条CPU的原子指令(cmpxchg指令:读作compare and change),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好。
说一下offset,offeset为字段的偏移量,每个对象有个地址,offset是字段相对于对象地址的偏移量,对象地址记为baseAddress,字段偏移量记为offeset,那么字段对应的实际地址就是baseAddress+offeset,所以cas通过对象、偏移量就可以去操作字段对应的值了。
CAS在AtomicInteger上的应用
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//初始化时获取到AtomicInteger的字段value的字段的偏移量offeset
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//原子加1,并返回加之前的值
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//原子加delta,并返回加之前的值
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
}
Unsafe中原子操作相关方法介绍
5个方法,内部通过自旋的CAS操作实现的,这些方法都可以保证操作的数据在多线程环境中的原子性,正确性。 看一下实现:
/**
* int类型值原子操作,对var2地址对应的值做原子增加操作(增加var4)
*
* @param var1 操作的对象
* @param var2 var2字段内存地址偏移量
* @param var4 需要加的值
* @return
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
/**
* long类型值原子操作,对var2地址对应的值做原子增加操作(增加var4)
*
* @param var1 操作的对象
* @param var2 var2字段内存地址偏移量
* @param var4 需要加的值
* @return 返回旧值
*/
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
/**
* int类型值原子操作方法,将var2地址对应的值置为var4
*
* @param var1 操作的对象
* @param var2 var2字段内存地址偏移量
* @param var4 新值
* @return 返回旧值
*/
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while (!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
/**
* long类型值原子操作方法,将var2地址对应的值置为var4
*
* @param var1 操作的对象
* @param var2 var2字段内存地址偏移量
* @param var4 新值
* @return 返回旧值
*/
public final long getAndSetLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while (!this.compareAndSwapLong(var1, var2, var6, var4));
return var6;
}
/**
* Object类型值原子操作方法,将var2地址对应的值置为var4
*
* @param var1 操作的对象
* @param var2 var2字段内存地址偏移量
* @param var4 新值
* @return 返回旧值
*/
public final Object getAndSetObject(Object var1, long var2, Object var4) {
Object var5;
do {
var5 = this.getObjectVolatile(var1, var2);
} while (!this.compareAndSwapObject(var1, var2, var5, var4));
return var5;
}
使用Unsafe实现一个网站计数功能:
public class UnsafeCountTest {
//通过反射获取Unsafe实例
private static Unsafe unsafe;
//count在Demo.class对象中的地址偏移量
private static long valueOffset;
//用来记录网站访问量,每次访问+1
private static int count;
//private volatile static int count;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
//基本上你通过反射得到的字段就像任何其他字段一样,但是当你调用get方法时,你传递的是null,因为没有实例可以作用。
//field.get(null)方法参数传递的是实例,而静态域是没有实例的,获取静态变量直接用field.get(null)。
unsafe = (Unsafe) field.get(null);
Field fieldC = UnsafeCountTest.class.getDeclaredField("count");
valueOffset = unsafe.staticFieldOffset(fieldC);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private static void request() throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//对count原子加1
unsafe.getAndAddInt(UnsafeCountTest.class,valueOffset,1);
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
ExecutorService threadPool = Executors.newCachedThreadPool();
int userCount = 100;
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < userCount; i++) {
threadPool.execute(() -> {
try {
for (int j = 0; j < 10; j++) {
UnsafeCountTest.request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);
threadPool.shutdown();
}
}
输出:
main,耗时:157,count=1000
Unsafe中线程调度相关方法
这部分,包括线程挂起、恢复、锁机制等方法。
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程,isAbsolute:是否是绝对时间,如果为true,time是一个绝对时间,如果为false,time是一个相对时间,time表示纳秒
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
调用park后,线程将被阻塞,直到unpark调用或者超时,如果之前调用过unpark,不会进行阻塞,即park和unpark不区分先后顺序。monitorEnter、monitorExit、tryMonitorEnter 3个方法已过期,不建议使用了。
线程中相当于有个许可,许可默认是0,调用park的时候,发现是0会阻塞当前线程,调用unpark之后,许可会被置为1,并会唤醒当前线程。如果在park之前先调用了unpark方法,执行park方法的时候,不会阻塞。park方法被唤醒之后,许可又会被置为0。多次调用unpark的效果是一样的,许可还是1。
juc中的LockSupport类是通过unpark和park方法实现的。
实例:
//表示一直阻塞等待
unsafe.park(false, 0);
//取消阻塞线程
unsafe.unpark(thread);
//线程挂起3秒,超时等待
unsafe.park(false, TimeUnit.SECONDS.toNanos(3));
Unsafe锁示例——已废弃
//模拟访问一次
public static void request() {
unsafe.monitorEnter(Demo4.class);
try {
count++;
} finally {
unsafe.monitorExit(Demo4.class);
}
}
注意:
- monitorEnter、monitorExit、tryMonitorEnter 3个方法已过期,不建议使用了
- monitorEnter、monitorExit必须成对出现,出现的次数必须一致,也就是说锁了n次,也必须释放n次,否则会造成死锁
Unsafe中保证变量的可见性的方法——相当于对要读取和修改的变量加volatile
关于变量可见性需要先了解java内存模型JMM。
java中操作内存分为主内存和工作内存,共享数据在主内存中,线程如果需要操作主内存的数据,需要先将主内存的数据复制到线程独有的工作内存中,操作完成之后再将其刷新到主内存中。如线程A要想看到线程B修改后的数据,需要满足:线程B修改数据之后,需要将数据从自己的工作内存中刷新到主内存中,并且A需要去主内存中读取数据。
被关键字volatile修饰的数据,有2点语义:
- 如果一个变量被volatile修饰,读取这个变量时候,会强制从主内存中读取,然后将其复制到当前线程的工作内存中使用
- 给volatile修饰的变量赋值的时候,会强制将赋值的结果从工作内存刷新到主内存
上面2点语义保证了被volatile修饰的数据在多线程中的可见性。
Unsafe中提供了和volatile语义一样的功能的方法,如下:
//设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void putIntVolatile(Object o, long offset, int x);
//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);
putIntVolatile方法,2个参数:
o:表示需要操作的对象
offset:表示操作对象中的某个字段地址偏移量
x:将offset对应的字段的值修改为x,并且立即刷新到主存中
调用这个方法,会强制将工作内存中修改的数据刷新到主内存中。
getIntVolatile方法,2个参数
o:表示需要操作的对象
offset:表示操作对象中的某个字段地址偏移量
每次调用这个方法都会强制从主内存读取值,将其复制到工作内存中使用。
其他的还有几个putXXXVolatile、getXXXVolatile方法和上面2个类似。
JUC中原子类
JUC中原子类介绍
什么是原子操作?
atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰,所以,所谓原子类说简单点就是具有原子操作特征的类,原子操作类提供了一些修改数据的方法,这些方法都是原子操作的,在多线程情况下可以确保被修改数据的正确性。
JUC中对原子操作提供了强大的支持,这些类位于java.util.concurrent.atomic包中.
JUC中原子类思维导图
基本类型原子类
使用原子的方式更新基本类型
- AtomicInteger:int类型原子类
- AtomicLong:long类型原子类
- AtomicBoolean :boolean类型原子类
上面三个类提供的方法几乎相同,这里以 AtomicInteger 为例子来介绍。
AtomicInteger 类常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
部分源码
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
2个关键字段说明:
value:使用volatile修饰,可以确保value在多线程中的可见性。
valueOffset:value属性在AtomicInteger中的偏移量,通过这个偏移量可以快速定位到value字段,这个是实现AtomicInteger的关键。
getAndIncrement源码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
内部调用的是Unsafe类中的getAndAddInt方法,我们看一下getAndAddInt源码:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
说明:
this.getIntVolatile:可以确保从主内存中获取变量最新的值。
compareAndSwapInt:CAS操作,CAS的原理是拿期望的值和原本的值作比较,如果相同则更新成新的值,可以确保在多线程情况下只有一个线程会操作成功,不成功的返回false。
上面有个do-while循环,compareAndSwapInt返回false之后,会再次从主内存中获取变量的值,继续做CAS操作,直到成功为止。
getAndAddInt操作相当于线程安全的count++操作,如同:
synchronize(lock){
count++;
}
count++操作实际上是被拆分为3步骤执行:
- 获取count的值,记做A:A=count
- 将A的值+1,得到B:B = A+1
- 让B赋值给count:count = B
多线程情况下会出现线程安全的问题,导致数据不准确。
synchronize的方式会导致占时无法获取锁的线程处于阻塞状态,性能比较低。CAS的性能比synchronize要快很多。
数组类型原子类介绍
使用原子的方式更新数组里的某个元素,可以确保修改数组中数据的线程安全性。
- AtomicIntegerArray:整形数组原子操作类
- AtomicLongArray:长整形数组原子操作类
- AtomicReferenceArray :引用类型数组原子操作类
上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。
AtomicIntegerArray 类常用方法
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
示例
统计网站页面访问量,假设网站有10个页面,现在模拟100个人并行访问每个页面10次,然后将每个页面访问量输出,应该每个页面都是1000次,代码如下:
public class AtomicIntegerArrayTest {
private static AtomicIntegerArray array = new AtomicIntegerArray(10);
private static void request(int page) throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
array.getAndIncrement(page-1);
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
ExecutorService threadPool = Executors.newCachedThreadPool();
int userCount = 100;
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < userCount; i++) {
threadPool.execute(() -> {
try {
for (int j = 0; j < 10; j++) {
for (int k = 1; k <= 10; k++) {
AtomicIntegerArrayTest.request(k);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",array=" + array.toString());
threadPool.shutdown();
}
}
输出:
main,耗时:672,array=[1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000]
引用类型原子类介绍
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。
- AtomicReference:引用类型原子类
- AtomicStampedRerence:原子更新引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类型
AtomicReference 和 AtomicInteger 非常类似,不同之处在于 AtomicInteger是对整数的封装,而AtomicReference则是对应普通的对象引用,它可以确保你在修改对象引用时的线程安全性。在介绍AtomicReference的同时,我们先来了解一个有关原子操作逻辑上的不足。
ABA问题
之前我们说过,线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望值是否一致。这个逻辑从一般意义上来说是正确的,但是可能出现一个小小的例外,就是当你获得当前数据后,在准备修改为新值钱,对象的值被其他线程连续修改了两次,而经过这2次修改后,对象的值又恢复为旧值,这样,当前线程就无法正确判断这个对象究竟是否被修改过,这就是所谓的ABA问题,可能会引发一些问题。
举个例子
有一家蛋糕店,为了挽留客户,决定为贵宾卡客户一次性赠送20元,刺激客户充值和消费,但条件是,每一位客户只能被赠送一次,现在我们用AtomicReference来实现这个功能,代码如下:
public class AtomicReferenceTest1 {
//账户原始余额
static int accountMoney = 19;
//用于对账户余额做原子操作
static AtomicReference money = new AtomicReference<>(accountMoney);
/**
* 模拟2个线程同时更新后台数据库,为用户充值
*/
static void recharge() {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
Integer m = money.get();
if (m == accountMoney) {
if (money.compareAndSet(m, m + 20)) {
System.out.println("当前余额:" + m + ",充值20元成功,余额:" + money.get() + "元");
}
}
//休眠100ms
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 模拟用户消费
*/
static void consume() throws InterruptedException {
for (int i = 0; i < 5; i++) {
Integer m = money.get();
if (m > 20) {
if (money.compareAndSet(m, m - 20)) {
System.out.println("当前余额:" + m + ",成功消费10元,余额:" + money.get() + "元");
}
}
//休眠50ms
TimeUnit.MILLISECONDS.sleep(50);
}
}
public static void main(String[] args) throws InterruptedException {
recharge();
consume();
}
}
输出:
当前余额:19,充值20元成功,余额:39元
当前余额:39,成功消费10元,余额:19元
当前余额:39,成功消费10元,余额:19元
当前余额:19,充值20元成功,余额:19元
当前余额:19,充值20元成功,余额:39元
当前余额:39,成功消费10元,余额:19元
当前余额:19,充值20元成功,余额:39元
从输出中可以看到,这个账户被先后反复多次充值。其原因是账户余额被反复修改,修改后的值和原有的数值19一样,使得CAS操作无法正确判断当前数据是否被修改过(是否被加过20)。虽然这种情况出现的概率不大,但是依然是有可能出现的,因此,当业务上确实可能出现这种情况时,我们必须多加防范。JDK也为我们考虑到了这种情况,使用AtomicStampedReference可以很好地解决这个问题。
AtomicStampedReference内部不仅维护了对象的值,还维护了一个版本号(我们这里把他称为时间戳,实际上它可以使用任何一个整形来表示状态值),当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新版本号。当AtomicStampedReference设置对象值时,对象值及版本号都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要版本号发生变化,就能防止不恰当的写入。
AtomicStampedReference的几个Api在AtomicStampedReference的基础上新增了有关版本号的信息。
//比较设置,参数依次为:期望值、写入新值、期望版本号、新版本号
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp);
//获得当前对象引用
public V getReference();
//获得当前版本号
public int getStamp();
//设置当前对象引用和版本号
public void set(V newReference, int newStamp);
AtomicStampedReference内部维护了一个Pair对象存放值,绑定了当前值和版本号。
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
private static class Pair {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
private volatile Pair pair;
现在我们使用AtomicStampedRerence来修改一下上面充值的问题,代码如下:
public class AtomicStampedReferenceTest1 {
//账户原始余额
static int accountMoney = 19;
//用于对账户余额做原子操作
static AtomicStampedReference money = new AtomicStampedReference<>(accountMoney, 0);
/**
* 模拟2个线程同时更新后台数据库,为用户充值
*/
static void recharge() {
for (int i = 0; i < 2; i++) {
int stamp = money.getStamp();
new Thread(() -> {
for (int j = 0; j < 50; j++) {
Integer m = money.getReference();
if (m == accountMoney) {
if (money.compareAndSet(m, m + 20, stamp, stamp + 1)) {
System.out.println("当前时间戳:" + money.getStamp() + ",当前余额:" + m + ",充值20元成功,余额:" + money.getReference() + "元");
}
}
//休眠100ms
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 模拟用户消费
*/
static void consume() throws InterruptedException {
for (int i = 0; i < 50; i++) {
Integer m = money.getReference();
int stamp = money.getStamp();
if (m > 20) {
if (money.compareAndSet(m, m - 20, stamp, stamp + 1)) {
System.out.println("当前时间戳:" + money.getStamp() + ",当前余额:" + m + ",成功消费20元,余额:" + money.getReference() + "元");
}
}
//休眠50ms
TimeUnit.MILLISECONDS.sleep(50);
}
}
public static void main(String[] args) throws InterruptedException {
recharge();
consume();
}
}
输出:
当前时间戳:1,当前余额:19,充值20元成功,余额:39元
当前时间戳:2,当前余额:39,成功消费20元,余额:19元
关于这个时间戳的,在数据库修改数据中也有类似的用法,比如2个编辑同时编辑一篇文章,同时提交,只允许一个用户提交成功,提示另外一个用户:博客已被其他人修改,如何实现呢?
数据库对于乐观锁的ABA问题也是同样的道理,加个版本号或者时间戳解决ABA问题。
博客表:t_blog(id,content,stamp),stamp默认值为0,每次更新+1
A、B 二个编辑同时对一篇文章进行编辑,stamp都为0,当点击提交的时候,将stamp和id作为条件更新博客内容,执行的sql如下:
update t_blog set content = 更新的内容,stamp = stamp+1 where id = 博客id and stamp = 0;
这条update会返回影响的行数,只有一个会返回1,表示更新成功,另外一个提交者返回0,表示需要修改的数据已经不满足条件了,被其他用户给修改了。这种修改数据的方式也叫乐观锁。
对象的属性修改原子类介绍
如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改原子类。
- AtomicIntegerFieldUpdater:原子更新整形字段的值
- AtomicLongFieldUpdater:原子更新长整形字段的值
- AtomicReferenceFieldUpdater :原子更新应用类型字段的值
要想原子地更新对象的属性需要两步:
- 第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
- 第二步,更新的对象属性必须使用 public volatile 修饰符。
上面三个类提供的方法几乎相同,所以我们这里以AtomicReferenceFieldUpdater为例子来介绍。
调用AtomicReferenceFieldUpdater静态方法newUpdater创建AtomicReferenceFieldUpdater对象
public static AtomicReferenceFieldUpdater newUpdater(Class tclass, Class vclass, String fieldName)
说明:
三个参数
tclass:需要操作的字段所在的类
vclass:操作字段的类型
fieldName:字段名称
示例
多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
public class AtomicReferenceFieldUpdaterTest {
static AtomicReferenceFieldUpdaterTest updaterTest = new AtomicReferenceFieldUpdaterTest();
//不能操作static修饰的字段,会报Caused by: java.lang.IllegalArgumentException错误。compareAndSet操作的是对象实例的偏移值字段,static修饰的字段不属于对象实例
//必须被volatile修饰
private volatile Boolean isInit = Boolean.FALSE;
private static AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.
newUpdater(AtomicReferenceFieldUpdaterTest.class, Boolean.class, "isInit");
public static void init() {
if (referenceFieldUpdater.compareAndSet(updaterTest, false, true)) {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",开始初始化!");
//模拟休眠3秒
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",初始化完毕!");
} else {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",有其他线程已经执行了初始化!");
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
AtomicReferenceFieldUpdaterTest.init();
}).start();
}
}
}
输出:
1599030805588,Thread-0,开始初始化!
1599030805588,Thread-1,有其他线程已经执行了初始化!
1599030805588,Thread-2,有其他线程已经执行了初始化!
1599030805589,Thread-3,有其他线程已经执行了初始化!
1599030805589,Thread-4,有其他线程已经执行了初始化!
1599030808590,Thread-0,初始化完毕!
说明:
- isInit属性必须要volatille修饰,可以确保变量的可见性
- 可以看出多线程同时执行init()方法,只有一个线程执行了初始化的操作,其他线程跳过了。多个线程同时到达updater.compareAndSet,只有一个会成功。
ThreadLocal、InheritableThreadLocal
使用技巧,可以用static方法包装ThreadLocal的get/set方法,这样就可以直接调用了。也可以在抽象类中定义ThreadLocal,这样所有的继承类也能调用到。
ThreadLocal
线程就相当于一个人一样,每个请求相当于一个任务,任务来了,人来处理,处理完毕之后,再处理下一个请求任务。人身上是不是有很多口袋,人刚开始准备处理任务的时候,我们把任务的编号放在处理者的口袋中,然后处理中一路携带者,处理过程中如果需要用到这个编号,直接从口袋中获取就可以了。那么刚好java中线程设计的时候也考虑到了这些问题,Thread对象中就有很多口袋,用来放东西。Thread类中有这么一个变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
这个就是用来操作Thread中所有口袋的东西,ThreadLocalMap源码中有一个数组(有兴趣的可以去看一下源码),对应处理者身上很多口袋一样,数组中的每个元素对应一个口袋。
如何来操作Thread中的这些口袋呢,java为我们提供了一个类ThreadLocal,ThreadLocal对象用来操作Thread中的某一个口袋,可以向这个口袋中放东西、获取里面的东西、清除里面的东西,这个口袋一次性只能放一个东西,重复放东西会将里面已经存在的东西覆盖掉。
常用的3个方法:
//向Thread中某个口袋中放东西
public void set(T value);
//获取这个口袋中目前放的东西
public T get();
//清空这个口袋中放的东西
public void remove()
ThreadLocal的官方API解释为:
“该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。”
InheritableThreadLocal
如果一个线程还做并发处理开启多个线程时,这时候子线程如果也想要父线程保留在口袋里的东西,就要使用InheritableThreadLocal来代替ThreadLocal。
父线程相当于主管,子线程相当于干活的小弟,主管让小弟们干活的时候,将自己兜里面的东西复制一份给小弟们使用,主管兜里面可能有很多牛逼的工具,为了提升小弟们的工作效率,给小弟们都复制一个,丢到小弟们的兜里,然后小弟就可以从自己的兜里拿去这些东西使用了,也可以清空自己兜里面的东西。
Thread对象中有个inheritableThreadLocals变量,代码如下:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
inheritableThreadLocals相当于线程中另外一种兜,这种兜有什么特征呢,当创建子线程的时候,子线程会将父线程这种类型兜的东西全部复制一份放到自己的inheritableThreadLocals兜中,使用InheritableThreadLocal对象可以操作线程中的inheritableThreadLocals兜。
InheritableThreadLocal常用的方法也有3个:
//向Thread中某个口袋中放东西
public void set(T value);
//获取这个口袋中目前放的东西
public T get();
//清空这个口袋中放的东西
public void remove()
实例:
@Slf4j
public class ThreadLocalTest {
//private static ThreadLocal threadLocal = new ThreadLocal<>();
//
private static InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();
//自定义包含策略
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
//需要插入的数据
List dataList = new ArrayList<>();
for (int i = 0; i < 3; i++) {
dataList.add("数据" + i);
}
for (int i = 0; i < 5; i++) {
String traceId = String.valueOf(i);
executor.execute(() -> {
threadLocal.set(traceId);
try {
ThreadLocalTest.controller(dataList);
} finally {
threadLocal.remove();
}
});
}
}
//模拟controller
public static void controller(List dataList) {
log.error("接受请求: " + "traceId:" + threadLocal.get());
service(dataList);
}
//模拟service
public static void service(List dataList) {
log.error("执行业务:" + "traceId:" + threadLocal.get());
//dao(dataList);
daoMuti(dataList);
}
//模拟dao
public static void dao(List dataList) {
log.error("执行数据库操作" + "traceId:" + threadLocal.get());
//模拟插入数据
for (String s : dataList) {
log.error("插入数据" + s + "成功" + "traceId:" + threadLocal.get());
}
}
//模拟dao--多线程
public static void daoMuti(List dataList) {
CountDownLatch countDownLatch = new CountDownLatch(dataList.size());
log.error("执行数据库操作" + "traceId:" + threadLocal.get());
String threadName = Thread.currentThread().getName();
//模拟插入数据
for (String s : dataList) {
new Thread(() -> {
try {
//模拟数据库操作耗时100毫秒
TimeUnit.MILLISECONDS.sleep(100);
log.error("插入数据" + s + "成功" + threadName + ",traceId:" + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
}
//等待上面的dataList处理完毕
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
17:35:30.465 [From DemoThreadFactory's 订单创建组-Worker-2] ERROR com.self.current.ThreadLocalTest - 接受请求: traceId:1
17:35:30.465 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.ThreadLocalTest - 接受请求: traceId:0
17:35:30.465 [From DemoThreadFactory's 订单创建组-Worker-3] ERROR com.self.current.ThreadLocalTest - 接受请求: traceId:2
17:35:30.471 [From DemoThreadFactory's 订单创建组-Worker-3] ERROR com.self.current.ThreadLocalTest - 执行业务:traceId:2
17:35:30.471 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.ThreadLocalTest - 执行业务:traceId:0
17:35:30.471 [From DemoThreadFactory's 订单创建组-Worker-2] ERROR com.self.current.ThreadLocalTest - 执行业务:traceId:1
17:35:30.471 [From DemoThreadFactory's 订单创建组-Worker-3] ERROR com.self.current.ThreadLocalTest - 执行数据库操作traceId:2
17:35:30.471 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.ThreadLocalTest - 执行数据库操作traceId:0
17:35:30.471 [From DemoThreadFactory's 订单创建组-Worker-2] ERROR com.self.current.ThreadLocalTest - 执行数据库操作traceId:1
17:35:30.574 [Thread-3] ERROR com.self.current.ThreadLocalTest - 插入数据数据2成功From DemoThreadFactory's 订单创建组-Worker-3,traceId:2
17:35:30.574 [Thread-4] ERROR com.self.current.ThreadLocalTest - 插入数据数据0成功From DemoThreadFactory's 订单创建组-Worker-2,traceId:1
17:35:30.574 [Thread-1] ERROR com.self.current.ThreadLocalTest - 插入数据数据0成功From DemoThreadFactory's 订单创建组-Worker-3,traceId:2
17:35:30.574 [Thread-2] ERROR com.self.current.ThreadLocalTest - 插入数据数据1成功From DemoThreadFactory's 订单创建组-Worker-3,traceId:2
17:35:30.574 [From DemoThreadFactory's 订单创建组-Worker-3] ERROR com.self.current.ThreadLocalTest - 接受请求: traceId:3
17:35:30.574 [From DemoThreadFactory's 订单创建组-Worker-3] ERROR com.self.current.ThreadLocalTest - 执行业务:traceId:3
17:35:30.574 [From DemoThreadFactory's 订单创建组-Worker-3] ERROR com.self.current.ThreadLocalTest - 执行数据库操作traceId:3
17:35:30.575 [Thread-9] ERROR com.self.current.ThreadLocalTest - 插入数据数据2成功From DemoThreadFactory's 订单创建组-Worker-1,traceId:0
17:35:30.575 [Thread-8] ERROR com.self.current.ThreadLocalTest - 插入数据数据1成功From DemoThreadFactory's 订单创建组-Worker-1,traceId:0
17:35:30.575 [Thread-7] ERROR com.self.current.ThreadLocalTest - 插入数据数据0成功From DemoThreadFactory's 订单创建组-Worker-1,traceId:0
17:35:30.575 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.ThreadLocalTest - 接受请求: traceId:4
17:35:30.575 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.ThreadLocalTest - 执行业务:traceId:4
17:35:30.575 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.ThreadLocalTest - 执行数据库操作traceId:4
17:35:30.575 [Thread-6] ERROR com.self.current.ThreadLocalTest - 插入数据数据2成功From DemoThreadFactory's 订单创建组-Worker-2,traceId:1
17:35:30.575 [Thread-5] ERROR com.self.current.ThreadLocalTest - 插入数据数据1成功From DemoThreadFactory's 订单创建组-Worker-2,traceId:1
17:35:30.682 [Thread-10] ERROR com.self.current.ThreadLocalTest - 插入数据数据0成功From DemoThreadFactory's 订单创建组-Worker-3,traceId:3
17:35:30.682 [Thread-13] ERROR com.self.current.ThreadLocalTest - 插入数据数据0成功From DemoThreadFactory's 订单创建组-Worker-1,traceId:4
17:35:30.682 [Thread-14] ERROR com.self.current.ThreadLocalTest - 插入数据数据1成功From DemoThreadFactory's 订单创建组-Worker-1,traceId:4
17:35:30.682 [Thread-12] ERROR com.self.current.ThreadLocalTest - 插入数据数据2成功From DemoThreadFactory's 订单创建组-Worker-3,traceId:3
17:35:30.683 [Thread-15] ERROR com.self.current.ThreadLocalTest - 插入数据数据2成功From DemoThreadFactory's 订单创建组-Worker-1,traceId:4
17:35:30.683 [Thread-11] ERROR com.self.current.ThreadLocalTest - 插入数据数据1成功From DemoThreadFactory's 订单创建组-Worker-3,traceId:3
JUC中的阻塞队列
Queue接口
队列是一种先进先出(FIFO)的数据结构,java中用Queue接口来表示队列。
Queue接口中定义了6个方法:
public interface Queue extends Collection {
boolean add(e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
每个Queue方法都有两种形式:
(1)如果操作失败则抛出异常,
(2)如果操作失败,则返回特殊值(null或false,具体取决于操作),接口的常规结构如下表所示。
操作类型 抛出异常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
检查 element() peek()
Queue从Collection继承的add方法插入一个元素,除非它违反了队列的容量限制,在这种情况下它会抛出IllegalStateException;offer方法与add不同之处仅在于它通过返回false来表示插入元素失败。
remove和poll方法都移除并返回队列的头部,确切地移除哪个元素是由具体的实现来决定的,仅当队列为空时,remove和poll方法的行为才有所不同,在这些情况下,remove抛出NoSuchElementException,而poll返回null。
element和peek方法返回队列头部的元素,但不移除,它们之间的差异与remove和poll的方式完全相同,如果队列为空,则element抛出NoSuchElementException,而peek返回null。
队列一般不要插入空元素。
BlockingQueue接口
BlockingQueue位于juc中,熟称阻塞队列, 阻塞队列首先它是一个队列,继承Queue接口,是队列就会遵循先进先出(FIFO)的原则,又因为它是阻塞的,故与普通的队列有两点区别:
- 当一个线程向队列里面添加数据时,如果队列是满的,那么将阻塞该线程,暂停添加数据
- 当一个线程从队列里面取出数据时,如果队列是空的,那么将阻塞该线程,暂停取出数据
BlockingQueue相关方法:
操作类型 抛出异常 返回特殊值 一直阻塞 超时退出
插入 add(e) offer(e) put(e) offer(e,timeuout,unit)
移除 remove() poll() take() poll(timeout,unit)
检查 element() peek() 不支持 不支持
重点,再来解释一下,加深印象:
- 3个可能会有异常的方法,add、remove、element;这3个方法不会阻塞(是说队列满或者空的情况下是否会阻塞);队列满的情况下,add抛出异常;队列为空情况下,remove、element抛出异常
- offer、poll、peek 也不会阻塞(是说队列满或者空的情况下是否会阻塞);队列满的情况下,offer返回false;队列为空的情况下,pool、peek返回null
- 队列满的情况下,调用put方法会导致当前线程阻塞
- 队列为空的情况下,调用take方法会导致当前线程阻塞
- offer(e,timeuout,unit),超时之前,插入成功返回true,否者返回false
- poll(timeout,unit),超时之前,获取到头部元素并将其移除,返回true,否者返回false
BlockingQueue常见的实现类
ArrayBlockingQueue
基于数组的阻塞队列实现,其内部维护一个定长的数组,用于存储队列元素。线程阻塞的实现是通过ReentrantLock来完成的,数据的插入与取出共用同一个锁,因此ArrayBlockingQueue并不能实现生产、消费同时进行。而且在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
LinkedBlockingQueue
基于单向链表的阻塞队列实现,在初始化LinkedBlockingQueue的时候可以指定大小,也可以不指定,默认类似一个无限大小的容量(Integer.MAX_VALUE),不指队列容量大小也是会有风险的,一旦数据生产速度大于消费速度,系统内存将有可能被消耗殆尽,因此要谨慎操作。另外LinkedBlockingQueue中用于阻塞生产者、消费者的锁是两个(锁分离),因此生产与消费是可以同时进行的。
PriorityBlockingQueue
一个支持优先级排序的无界阻塞队列,进入队列的元素会按照优先级进行排序
SynchronousQueue
同步阻塞队列,SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue,每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然
DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列,里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行,也就是说只有在延迟期到时才能够从队列中取元素
LinkedTransferQueue
LinkedTransferQueue是基于链表的FIFO无界阻塞队列,它出现在JDK7中,Doug Lea 大神说LinkedTransferQueue是一个聪明的队列,它是ConcurrentLinkedQueue、SynchronousQueue(公平模式下)、无界的LinkedBlockingQueues等的超集,LinkedTransferQueue包含了ConcurrentLinkedQueue、SynchronousQueue、LinkedBlockingQueues三种队列的功能
ArrayBlockingQueue
有界阻塞队列,内部使用数组存储元素,有2个常用构造方法:
//capacity表示容量大小,默认内部采用非公平锁
public ArrayBlockingQueue(int capacity)
//capacity:容量大小,fair:内部是否是使用公平锁
public ArrayBlockingQueue(int capacity, boolean fair)
注意:ArrayBlockingQueue如果队列容量设置的太小,消费者发送的太快,消费者消费的太慢的情况下,会导致队列空间满,调用put方法会导致发送者线程阻塞,所以注意设置合理的大小,协调好消费者的速度。
LinkedBlockingQueue
内部使用单向链表实现的阻塞队列,3个构造方法:
//默认构造方法,容量大小为Integer.MAX_VALUE
public LinkedBlockingQueue();
//创建指定容量大小的LinkedBlockingQueue
public LinkedBlockingQueue(int capacity);
//容量为Integer.MAX_VALUE,并将传入的集合丢入队列中
public LinkedBlockingQueue(Collection extends E> c);
LinkedBlockingQueue的用法和ArrayBlockingQueue类似,建议使用的时候指定容量,如果不指定容量,插入的太快,移除的太慢,可能会产生OOM。
PriorityBlockingQueue
无界的优先级阻塞队列,内部使用数组存储数据,达到容量时,会自动进行扩容,放入的元素会按照优先级进行排序,4个构造方法:
//默认构造方法,默认初始化容量是11
public PriorityBlockingQueue();
//指定队列的初始化容量
public PriorityBlockingQueue(int initialCapacity);
//指定队列的初始化容量和放入元素的比较器
public PriorityBlockingQueue(int initialCapacity,Comparator super E> comparator);
//传入集合放入来初始化队列,传入的集合可以实现SortedSet接口或者PriorityQueue接口进行排序,如果没有实现这2个接口,按正常顺序放入队列
public PriorityBlockingQueue(Collection extends E> c);
优先级队列放入元素的时候,会进行排序,所以我们需要指定排序规则,有2种方式:
- 创建PriorityBlockingQueue指定比较器Comparator
- 放入的元素需要实现Comparable接口
上面2种方式必须选一个,如果2个都有,则走第一个规则排序。
示例:
public class PriorityBlockingQueueTest {
private static PriorityBlockingQueue priorityBlockingQueue = new PriorityBlockingQueue<>();
private static class Msg implements Comparable{
private String msg;
private int priority;
public Msg(String msg, int priority) {
this.msg = msg;
this.priority = priority;
}
@Override
public String toString() {
return "Msg{" +
"priority=" + priority +
", msg='" + msg + '\'' +
'}';
}
@Override
public int compareTo(Msg o) {
//return this.priority-o.priority;
return o.priority-this.priority;
//return Integer.compare(this.priority,o.priority);
}
}
public static void putMsg(Msg msg) throws InterruptedException {
priorityBlockingQueue.put(msg);
System.out.println("已推送"+msg);
}
static {
new Thread(() -> {
while (true) {
Msg msg;
try {
long starTime = System.currentTimeMillis();
//获取一条推送消息,此方法会进行阻塞,直到返回结果
msg = priorityBlockingQueue.take();
long endTime = System.currentTimeMillis();
//模拟推送耗时
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(String.format("[%s,%s,take耗时:%s],%s,发送消息:%s", starTime, endTime, (endTime - starTime), Thread.currentThread().getName(), msg));
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
PriorityBlockingQueueTest.putMsg(new Msg("消息" + i,i));
}
}
}
输出:
已推送Msg{priority=0, msg='消息0'}
已推送Msg{priority=1, msg='消息1'}
已推送Msg{priority=2, msg='消息2'}
已推送Msg{priority=3, msg='消息3'}
已推送Msg{priority=4, msg='消息4'}
[1599459824306,1599459824307,take耗时:1],Thread-0,发送消息:Msg{priority=0, msg='消息0'}
[1599459824829,1599459824829,take耗时:0],Thread-0,发送消息:Msg{priority=4, msg='消息4'}
[1599459825330,1599459825330,take耗时:0],Thread-0,发送消息:Msg{priority=3, msg='消息3'}
[1599459825831,1599459825831,take耗时:0],Thread-0,发送消息:Msg{priority=2, msg='消息2'}
[1599459826331,1599459826331,take耗时:0],Thread-0,发送消息:Msg{priority=1, msg='消息1'}
SynchronousQueue
同步阻塞队列,SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue,每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。SynchronousQueue 在现实中用的不多,线程池中有用到过,Executors.newCachedThreadPool()实现中用到了这个队列,当有任务丢入线程池的时候,如果已创建的工作线程都在忙于处理任务,则会新建一个线程来处理丢入队列的任务。
调用queue.put方法向队列中丢入一条数据,调用的时候产生了阻塞,从输出结果中可以看出,直到take方法被调用时,put方法才从阻塞状态恢复正常。
DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列,里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行,也就是说只有在延迟期到时才能够从队列中取元素。
- DelayQueue是一个内部依靠AQS队列同步器所实现的无界延迟阻塞队列。
- 延迟对象需要覆盖 getDelay()与compareTo()方法,并且要注意 getDelay()的时间单位的统一,compareTo()根据业务逻辑进行合理的比较逻辑重写。
- DelayQueue中内聚的重入锁是非公平的。
- DelayQueue是实现定时任务的关键,ScheduledThreadPoolExecutor中就用到了DelayQueue。
示例:
public class DelayQueueTest {
//推送信息封装
static class Msg implements Delayed {
//优先级,越小优先级越高
private int priority;
//推送的信息
private String msg;
//定时发送时间,毫秒格式
private long sendTimeMs;
public Msg(int priority, String msg, long sendTimeMs) {
this.priority = priority;
this.msg = msg;
this.sendTimeMs = sendTimeMs;
}
@Override
public String toString() {
return "Msg{" +
"priority=" + priority +
", msg='" + msg + '\'' +
", sendTimeMs=" + sendTimeMs +
'}';
}
//@Override
//public long getDelay(TimeUnit unit) {
// return unit.convert(this.sendTimeMs - Calendar.getInstance().getTimeInMillis(), TimeUnit.MILLISECONDS);
//}
/**
* 需要实现的接口,获得延迟时间 用过期时间-当前时间
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.sendTimeMs - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
}
/**
* 用于延迟队列内部比较排序 当前时间的延迟时间 - 比较对象的延迟时间
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS));
}
}
//推送队列
static DelayQueue pushQueue = new DelayQueue();
static {
//启动一个线程做真实推送
new Thread(() -> {
while (true) {
Msg msg;
try {
//获取一条推送消息,此方法会进行阻塞,直到返回结果
msg = pushQueue.take();
//此处可以做真实推送
long endTime = System.currentTimeMillis();
System.out.println(String.format("定时发送时间:%s,实际发送时间:%s,发送消息:%s", msg.sendTimeMs, endTime, msg));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
//推送消息,需要发送推送消息的调用该方法,会将推送信息先加入推送队列
public static void pushMsg(int priority, String msg, long sendTimeMs) throws InterruptedException {
pushQueue.put(new Msg(priority, msg, sendTimeMs));
}
public static void main(String[] args) throws InterruptedException {
for (int i = 5; i >= 1; i--) {
String msg = "一起来学java高并发,第" + i + "天";
DelayQueueTest.pushMsg(i, msg, Calendar.getInstance().getTimeInMillis() + i * 2000);
}
}
}
输出:
定时发送时间:1599462287589,实际发送时间:1599462287590,发送消息:Msg{priority=1, msg='一起来学java高并发,第1天', sendTimeMs=1599462287589}
定时发送时间:1599462289589,实际发送时间:1599462289589,发送消息:Msg{priority=2, msg='一起来学java高并发,第2天', sendTimeMs=1599462289589}
定时发送时间:1599462291589,实际发送时间:1599462291590,发送消息:Msg{priority=3, msg='一起来学java高并发,第3天', sendTimeMs=1599462291589}
定时发送时间:1599462293588,实际发送时间:1599462293589,发送消息:Msg{priority=4, msg='一起来学java高并发,第4天', sendTimeMs=1599462293588}
定时发送时间:1599462295571,实际发送时间:1599462295571,发送消息:Msg{priority=5, msg='一起来学java高并发,第5天', sendTimeMs=1599462295571}
LinkedTransferQueue
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
LinkedTransferQueue类继承自AbstractQueue抽象类,并且实现了TransferQueue接口:
public interface TransferQueue extends BlockingQueue {
// 如果存在一个消费者已经等待接收它,则立即传送指定的元素,否则返回false,并且不进入队列。
boolean tryTransfer(E e);
// 如果存在一个消费者已经等待接收它,则立即传送指定的元素,否则等待直到元素被消费者接收。
void transfer(E e) throws InterruptedException;
// 在上述方法的基础上设置超时时间
boolean tryTransfer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
// 如果至少有一位消费者在等待,则返回true
boolean hasWaitingConsumer();
// 获取所有等待获取元素的消费线程数量
int getWaitingConsumerCount();
}
再看一下上面的这些方法,transfer(E e)方法和SynchronousQueue的put方法类似,都需要等待消费者取走元素,否者一直等待。其他方法和ArrayBlockingQueue、LinkedBlockingQueue中的方法类似。
总结
- 重点需要了解BlockingQueue中的所有方法,以及他们的区别
- 重点掌握ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue的使用场景
- 需要处理的任务有优先级的,使用PriorityBlockingQueue
- 处理的任务需要延时处理的,使用DelayQueue
疑问:
Q:有界阻塞队列和无界阻塞队列的区别?是不是以是否定义队列大小来作为区分,没有定义的就是无界的,有定义的就算是容量(Integer.MAX_VALUE)也是有界的?
JUC中常见的集合
JUC集合框架图
图可以看到,JUC的集合框架也是从Map、List、Set、Queue、Collection等超级接口中继承而来的。所以,大概可以知道JUC下的集合包含了一一些基本操作,并且变得线程安全。
Map
ConcurrentHashMap
功能和HashMap基本一致,内部使用红黑树实现的。
特性:
- 迭代结果和存入顺序不一致
- key和value都不能为空
- 线程安全的
ConcurrentSkipListMap
内部使用跳表实现的,放入的元素会进行排序,排序算法支持2种方式来指定:
- 通过构造方法传入一个Comparator
- 放入的元素实现Comparable接口
上面2种方式必选一个,如果2种都有,走规则1。
特性:
- 迭代结果和存入顺序不一致
- 放入的元素会排序
- key和value都不能为空
- 线程安全的
List
CopyOnWriteArrayList
实现List的接口的,一般我们使用ArrayList、LinkedList、Vector,其中只有Vector是线程安全的,可以使用Collections静态类的synchronizedList方法对ArrayList、LinkedList包装为线程安全的List,不过这些方式在保证线程安全的情况下性能都不高。
CopyOnWriteArrayList是线程安全的List,内部使用数组存储数据,集合中多线程并行操作一般存在4种情况:读读、读写、写写、写读,这个只有在写写操作过程中会导致其他线程阻塞,其他3种情况均不会阻塞,所以读取的效率非常高。
可以看一下这个类的名称:CopyOnWrite,意思是在写入操作的时候,进行一次自我复制,换句话说,当这个List需要修改时,并不修改原有内容(这对于保证当前在读线程的数据一致性非常重要),而是在原有存放数据的数组上产生一个副本,在副本上修改数据,修改完毕之后,用副本替换原来的数组,这样也保证了写操作不会影响读。
特性:
- 迭代结果和存入顺序一致
- 元素不重复
- 元素可以为空
- 线程安全的
- 读读、读写、写读3种情况不会阻塞;写写会阻塞
- 无界的
Set
ConcurrentSkipListSet
有序的Set,内部基于ConcurrentSkipListMap实现的,放入的元素会进行排序,排序算法支持2种方式来指定:
- 通过构造方法传入一个Comparator
- 放入的元素实现Comparable接口
上面2种方式需要实现一个,如果2种都有,走规则1
特性:
- 迭代结果和存入顺序不一致
- 放入的元素会排序
- 元素不重复
- 元素不能为空
- 线程安全的
- 无界的
CopyOnWriteArraySet
内部使用CopyOnWriteArrayList实现的,将所有的操作都会转发给CopyOnWriteArrayList。
特性:
- 迭代结果和存入顺序不一致
- 元素不重复
- 元素可以为空
- 线程安全的
- 读读、读写、写读 不会阻塞;写写会阻塞
- 无界的
Queue
Queue接口中的方法,我们再回顾一下:
操作类型 抛出异常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
检查 element() peek()
3种操作,每种操作有2个方法,不同点是队列为空或者满载时,调用方法是抛出异常还是返回特殊值,大家按照表格中的多看几遍,加深记忆。
ConcurrentLinkedQueue
高效并发队列,内部使用链表实现的。
特性:
- 线程安全的
- 迭代结果和存入顺序一致
- 元素可以重复
- 元素不能为空
- 线程安全的
- 无界队列
Deque
先介绍一下Deque接口,双向队列(Deque)是Queue的一个子接口,双向队列是指该队列两端的元素既能入队(offer)也能出队(poll),如果将Deque限制为只能从一端入队和出队,则可实现栈的数据结构。对于栈而言,有入栈(push)和出栈(pop),遵循先进后出原则。
一个线性 collection,支持在两端插入和移除元素。名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。大多数 Deque 实现对于它们能够包含的元素数没有固定限制,但此接口既支持有容量限制的双端队列,也支持没有固定大小限制的双端队列。
此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。插入操作的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现中,插入操作不能失败。
下表总结了上述 12 种方法:
此接口扩展了 Queue接口。在将双端队列用作队列时,将得到 FIFO(先进先出)行为。将元素添加到双端队列的末尾,从双端队列的开头移除元素。从 Queue 接口继承的方法完全等效于 Deque 方法,如下表所示:
Queue 方法 等效 Deque 方法
add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()
ConcurrentLinkedDeque
实现了Deque接口,内部使用链表实现的高效的并发双端队列。
特性:
- 线程安全的
- 迭代结果和存入顺序一致
- 元素可以重复
- 元素不能为空
- 线程安全的
- 无界队列
BlockingQueue
关于阻塞队列,上一篇有详细介绍。
疑问:
Q:跳表是什么?
接口性能提升实战篇
需求:电商app的商品详情页,需要给他们提供一个接口获取商品相关信息:
- 商品基本信息(名称、价格、库存、会员价格等)
- 商品图片列表
- 商品描述信息(描述信息一般是由富文本编辑的大文本信息)
普通接口实现伪代码如下:
public Map detail(long goodsId){
//创建一个map
//step1:查询商品基本信息,放入map
map.put("goodsModel",(select * from t_goods where id = #gooldsId#));
//step2:查询商品图片列表,返回一个集合放入map
map.put("goodsImgsModelList",(select * from t_goods_imgs where goods_id = #gooldsId#));
//step3:查询商品描述信息,放入map
map.put("goodsExtModel",(select * from t_goods_ext where goods_id = #gooldsId#));
return map;
}
上面这种写法应该很常见,代码很简单,假设上面每个步骤耗时200ms,此接口总共耗时>=600毫秒
整个过程是按顺序执行的,实际上3个查询之间是没有任何依赖关系,所以说3个查询可以同时执行,那我们对这3个步骤采用多线程并行执行实现如下:
示例:
public class GetProductDetailTest {
//自定义包含策略
private ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 20, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
/**
* 获取商品基本信息
*
* @param goodsId 商品id
* @return 商品基本信息
* @throws InterruptedException
*/
public String goodsDetailModel(long goodsId) throws InterruptedException {
//模拟耗时,休眠200ms
TimeUnit.MILLISECONDS.sleep(200);
return "商品id:" + goodsId + ",商品基本信息....";
}
/**
* 获取商品图片列表
*
* @param goodsId 商品id
* @return 商品图片列表
* @throws InterruptedException
*/
public List goodsImgsModelList(long goodsId) throws InterruptedException {
//模拟耗时,休眠200ms
TimeUnit.MILLISECONDS.sleep(200);
return Arrays.asList("图1", "图2", "图3");
}
/**
* 获取商品描述信息
*
* @param goodsId 商品id
* @return 商品描述信息
* @throws InterruptedException
*/
public String goodsExtModel(long goodsId) throws InterruptedException {
//模拟耗时,休眠200ms
TimeUnit.MILLISECONDS.sleep(200);
return "商品id:" + goodsId + ",商品描述信息......";
}
public Map getGoodsDetail(long goodsId) throws ExecutionException, InterruptedException {
Map result = new HashMap<>();
Future gooldsDetailModelFuture = executor.submit(() -> goodsDetailModel(goodsId));
Future> goodsImgsModelFuture = executor.submit(() -> goodsImgsModelList(goodsId));
//异步获取商品描述信息
Future goodsExtModelFuture = executor.submit(() -> goodsExtModel(goodsId));
result.put("gooldsDetailModel", gooldsDetailModelFuture.get());
result.put("goodsImgsModelList", goodsImgsModelFuture.get());
result.put("goodsExtModel", goodsExtModelFuture.get());
return result;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
GetProductDetailTest detailTest = new GetProductDetailTest();
long starTime = System.currentTimeMillis();
Map map = detailTest.getGoodsDetail(1L);
System.out.println(map);
System.out.println("耗时(ms):" + (System.currentTimeMillis() - starTime));
}
}
输出:
{goodsImgsModelList=[图1, 图2, 图3], gooldsDetailModel=商品id:1,商品基本信息...., goodsExtModel=商品id:1,商品描述信息......}
耗时(ms):255
可以看出耗时200毫秒左右,性能提升了2倍,假如这个接口中还存在其他无依赖的操作,性能提升将更加显著,上面使用了线程池并行去执行3次查询的任务,最后通过Future获取异步执行结果。
整个优化过程:
- 先列出无依赖的一些操作
- 将这些操作改为并行的方式
总结
- 对于无依赖的操作尽量采用并行方式去执行,可以很好的提升接口的性能
解决微服务日志的痛点
日志有什么用?
- 系统出现故障的时候,可以通过日志信息快速定位问题,修复bug,恢复业务
- 提取有用数据,做数据分析使用
本文主要讨论通过日志来快速定位并解决问题。
日志存在的痛点
先介绍一下多数公司采用的方式:目前比较流行的是采用springcloud(或者dubbo)做微服务,按照业务拆分为多个独立的服务,服务采用集群的方式部署在不同的机器上,当一个请求过来的时候,可能会调用到很多服务进行处理,springcloud一般采用logback(或者log4j)输出日志到文件中。当系统出问题的时候,按照系统故障的严重程度,严重的会回退版本,然后排查bug,轻的,找运维去线上拉日志,然后排查问题。
这个过程中存在一些问题:
- 日志文件太大太多,不方便查找
- 日志分散在不同的机器上,也不方便查找
- 一个请求可能会调用多个服务,完整的日志难以追踪(没有完整的链路日志)
- 系统出现了问题,只能等到用户发现了,自己才知道(没有报错预警)
本文要解决上面的几个痛点,构建我们的日志系统,达到以下要求:
- 方便追踪一个请求完整的日志
- 方便快速检索日志
- 系统出现问题自动报警,通知相关人员
构建日志系统
方便追踪一个请求完整的日志
当一个请求过来的时候,可能会调用多个服务,多个服务内部可能又会产生子线程处理业务,所以这里面有两个问题需要解决:
- 多个服务之间日志的追踪
- 服务内部子线程和主线程日志的追踪,这个地方举个例子,比如一个请求内部需要给10000人发送推送,内部开启10个线程并行处理,处理完毕之后响应操作者,这里面有父子线程,我们要能够找到这个里面所有的日志
需要追踪一个请求完整日志,我们需要给每个请求设置一个全局唯一编号,可以使用UUID或者其他方式也行。
多个服务之间日志追踪的问题:当一个请求过来的时候,在入口处生成一个trace_id,然后放在ThreadLocal中,如果内部设计到多个服务之间相互调用,调用其他服务的时,将trace_id顺便携带过去。
父子线程日志追踪的问题:可以采用InheritableThreadLocal来存放trace_id,这样可以在线程中获取到父线程中的trace_id。
所以此处我们需要使用InheritableThreadLocal来存储trace_id。
使用了线程池处理请求的,由于线程池中的线程采用的是复用的方式,所以需要对执行的任务Runable做一些改造 包装。
public class TraceRunnable implements Runnable {
private String tranceId;
private Runnable target;
public TraceRunnable(Runnable target) {
this.tranceId = TraceUtil.get();
this.target = target;
}
@Override
public void run() {
try {
TraceUtil.set(this.tranceId);
MDC.put(TraceUtil.MDC_TRACE_ID, TraceUtil.get());
this.target.run();
} finally {
MDC.remove(TraceUtil.MDC_TRACE_ID);
TraceUtil.remove();
}
}
public static Runnable trace(Runnable target) {
return new TraceRunnable(target);
}
}
需要用线程池执行的任务使用TraceRunnable封装一下就可以了。
TraceUtil代码:
public class TraceUtil {
public static final String REQUEST_HEADER_TRACE_ID = "com.ms.header.trace.id";
public static final String MDC_TRACE_ID = "trace_id";
private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>();
/**
* 获取traceid
*
* @return
*/
public static String get() {
String traceId = inheritableThreadLocal.get();
if (traceId == null) {
traceId = IDUtil.getId();
inheritableThreadLocal.set(traceId);
}
return traceId;
}
public static void set(String trace_id) {
inheritableThreadLocal.set(trace_id);
}
public static void remove() {
inheritableThreadLocal.remove();
}
}
日志输出中携带上trace_id,这样最终我们就可以通过trace_id找到一个请求的完整日志了。
方便快速检索日志
日志分散在不同的机器上,如果要快速检索,需要将所有服务产生的日志汇集到一个地方。
关于检索日志的,列一下需求:
- 我们将收集日志发送到消息中间件中(可以是kafka、rocketmq),消息中间件这块不介绍,选择玩的比较溜的就可以了
- 系统产生日志尽量不要影响接口的效率
- 带宽有限的情况下,发送日志也尽量不要去影响业务
- 日志尽量低延次,产生的日志,尽量在生成之后1分钟后可以检索到
- 检索日志功能要能够快速响应
关于上面几点,我们需要做的:日志发送的地方进行改造,引入消息中间件,将日志异步发送到消息中间件中,查询的地方采用elasticsearch,日志系统需要订阅消息中间件中的日志,然后丢给elasticsearch建索引,方便快速检索,咱们来一点点的介绍。
日志发送端的改造
日志是由业务系统产生的,一个请求过来的时候会产生很多日志,日志产生时,我们尽量减少日志输出对业务耗时的影响,我们的过程如下:
- 业务系统内部引用一个线程池来异步处理日志,线程池内部可以使用一个容量稍微大一点的阻塞队列
- 业务系统将日志丢给线程池进行处理
- 线程池中将需要处理的日志先压缩一下,然后发送至mq
线程池的使用可以参考:JAVA线程池,这一篇就够了
引入mq存储日志
业务系统将日志先发送到mq中,后面由其他消费者订阅进行消费。日志量比较大的,对mq的要求也比较高,可以选择kafka,业务量小的,也可以选取activemq。
使用elasticsearch来检索日志
elasticsearch(以下简称es)是一个全文检索工具,具体详情可以参考其官网相关文档。使用它来检索数据效率非常高。日志系统中需要我们开发一个消费端来拉取mq中的消息,将其存储到es中方便快速检索,关于这块有几点说一下:
- 建议按天在es中建立数据库,日质量非常大的,也可以按小时建立数据库。查询的时候,时间就是必选条件了,这样可以快速让es定位到日志库进行检索,提升检索效率
- 日志常见的需要收集的信息:trace_id、时间、日志级别、类、方法、url、调用的接口开始时间、调用接口的结束时间、接口耗时、接口状态码、异常信息、日志信息等等,可以按照这些在es中建立索引,方便检索。
日志监控报警——可自定义配置报警
日志监控报警是非常重要的,这个必须要有,日志系统中需要开发监控报警功能,这块我们可以做成通过页面配置的方式,支持报警规则的配置,如日志中产生了某些异常、接口响应时间大于多少、接口返回状态码404等异常信息的时候能够报警,具体的报警可以是语音电话、短信通知、钉钉机器人报警等等,这些也做成可以配置的。
日志监控模块从mq中拉取日志,然后去匹配我们启用的一些规则进行报警。
日志处理结构图如下:
高并发中常见的限流方式
常见的限流的场景
- 秒杀活动,数量有限,访问量巨大,为了防止系统宕机,需要做限流处理
- 国庆期间,一般的旅游景点人口太多,采用排队方式做限流处理
- 医院看病通过发放排队号的方式来做限流处理。
常见的限流算法
- 通过控制最大并发数来进行限流
- 使用漏桶算法来进行限流
- 使用令牌桶算法来进行限流
通过控制最大并发数来进行限流
以秒杀业务为例,10个iphone,100万人抢购,100万人同时发起请求,最终能够抢到的人也就是前面几个人,后面的基本上都没有希望了,那么我们可以通过控制并发数来实现,比如并发数控制在10个,其他超过并发数的请求全部拒绝,提示:秒杀失败,请稍后重试。
单机中的JUC中提供了这样的工具类:Semaphore:如果是集群,则可以用redis或者zk代替Semaphore
示例:
public class MaxAccessLimiter {
private static Semaphore limiter = new Semaphore(5);
//自定义包含策略
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 20, 60,
TimeUnit.SECONDS, new SynchronousQueue(),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
executor.submit(() -> {
boolean flag = false;
try {
flag = limiter.tryAcquire(100, TimeUnit.MICROSECONDS);
if (flag) {
//休眠2秒,模拟下单操作
System.out.println(Thread.currentThread() + ",尝试下单中。。。。。");
TimeUnit.SECONDS.sleep(2);
} else {
System.out.println(Thread.currentThread() + ",秒杀失败,请稍微重试!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (flag) {
limiter.release();
}
}
});
}
executor.shutdown();
}
}
输出:
Thread[From DemoThreadFactory's 订单创建组-Worker-1,5,main],尝试下单中。。。。。
Thread[From DemoThreadFactory's 订单创建组-Worker-2,5,main],尝试下单中。。。。。
Thread[From DemoThreadFactory's 订单创建组-Worker-3,5,main],尝试下单中。。。。。
Thread[From DemoThreadFactory's 订单创建组-Worker-4,5,main],尝试下单中。。。。。
Thread[From DemoThreadFactory's 订单创建组-Worker-5,5,main],尝试下单中。。。。。
Thread[From DemoThreadFactory's 订单创建组-Worker-9,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-14,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-16,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-17,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-18,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-20,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-12,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-11,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-7,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-8,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-6,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-10,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-19,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-15,5,main],秒杀失败,请稍微重试!
Thread[From DemoThreadFactory's 订单创建组-Worker-13,5,main],秒杀失败,请稍微重试!
使用漏桶算法来进行限流
国庆期间比较火爆的景点,人流量巨大,一般入口处会有限流的弯道,让游客进去进行排队,排在前面的人,每隔一段时间会放一拨进入景区。排队人数超过了指定的限制,后面再来的人会被告知今天已经游客量已经达到峰值,会被拒绝排队,让其明天或者以后再来,这种玩法采用漏桶限流的方式。
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
漏桶算法示意图:
示例:代码中BucketLimit.build(10, 60, TimeUnit.MINUTES);创建了一个容量为10,流水为60/分钟的漏桶。
public class BucketLimitTest {
public static class BucketLimit {
static AtomicInteger threadNum = new AtomicInteger(1);
//容量
private int capcity;
//流速
private int flowRate;
//流速时间单位
private TimeUnit flowRateUnit;
private BlockingQueue queue;
//漏桶流出的任务时间间隔(纳秒)
private long flowRateNanosTime;
public BucketLimit(int capcity, int flowRate, TimeUnit flowRateUnit) {
this.capcity = capcity;
this.flowRate = flowRate;
this.flowRateUnit = flowRateUnit;
this.bucketThreadWork();
}
//漏桶线程
public void bucketThreadWork() {
this.queue = new ArrayBlockingQueue(capcity);
//漏桶流出的任务时间间隔(纳秒)
this.flowRateNanosTime = flowRateUnit.toNanos(1) / flowRate;
System.out.println(TimeUnit.NANOSECONDS.toSeconds(this.flowRateNanosTime));
Thread thread = new Thread(this::bucketWork);
thread.setName("漏桶线程-" + threadNum.getAndIncrement());
thread.start();
}
//漏桶线程开始工作
public void bucketWork() {
while (true) {
Node node = this.queue.poll();
if (Objects.nonNull(node)) {
//唤醒任务线程
LockSupport.unpark(node.thread);
}
//阻塞当前线程,最长不超过nanos纳秒
//休眠flowRateNanosTime
LockSupport.parkNanos(this.flowRateNanosTime);
}
}
//返回一个漏桶
public static BucketLimit build(int capcity, int flowRate, TimeUnit flowRateUnit) {
if (capcity < 0 || flowRate < 0) {
throw new IllegalArgumentException("capcity、flowRate必须大于0!");
}
return new BucketLimit(capcity, flowRate, flowRateUnit);
}
//当前线程加入漏桶,返回false,表示漏桶已满;true:表示被漏桶限流成功,可以继续处理任务
public boolean acquire() {
Thread thread = Thread.currentThread();
Node node = new Node(thread);
if (this.queue.offer(node)) {
LockSupport.park();
return true;
}
return false;
}
//漏桶中存放的元素
class Node {
private Thread thread;
public Node(Thread thread) {
this.thread = thread;
}
}
}
//自定义包含策略
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(15, 15, 60,
TimeUnit.SECONDS, new SynchronousQueue(),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
//容量为10,流速为1个/秒,即60/每分钟
BucketLimit bucketLimit = BucketLimit.build(10, 60, TimeUnit.MINUTES);
for (int i = 0; i < 15; i++) {
executor.submit(() -> {
boolean acquire = bucketLimit.acquire();
System.out.println(Thread.currentThread().getName()+ " ," +System.currentTimeMillis() + " " + acquire);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
输出:
From DemoThreadFactory's 订单创建组-Worker-11 ,1599545066963 false
From DemoThreadFactory's 订单创建组-Worker-12 ,1599545066963 false
From DemoThreadFactory's 订单创建组-Worker-13 ,1599545066963 false
From DemoThreadFactory's 订单创建组-Worker-14 ,1599545066964 false
From DemoThreadFactory's 订单创建组-Worker-15 ,1599545066964 false
From DemoThreadFactory's 订单创建组-Worker-3 ,1599545067961 true
From DemoThreadFactory's 订单创建组-Worker-1 ,1599545068962 true
From DemoThreadFactory's 订单创建组-Worker-2 ,1599545069963 true
From DemoThreadFactory's 订单创建组-Worker-4 ,1599545070964 true
From DemoThreadFactory's 订单创建组-Worker-5 ,1599545071965 true
From DemoThreadFactory's 订单创建组-Worker-6 ,1599545072966 true
From DemoThreadFactory's 订单创建组-Worker-7 ,1599545073966 true
From DemoThreadFactory's 订单创建组-Worker-8 ,1599545074967 true
From DemoThreadFactory's 订单创建组-Worker-9 ,1599545075967 true
From DemoThreadFactory's 订单创建组-Worker-10 ,1599545076968 true
使用令牌桶算法来进行限流
令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。这种算法可以应对突发程度的请求,因此比漏桶算法好。
令牌桶算法示意图:
限流工具类RateLimiter
Google开源工具包Guava提供了限流工具类RateLimiter,可以非常方便的控制系统每秒吞吐量.
示例:RateLimiter.create(5)创建QPS为5的限流对象,后面又调用rateLimiter.setRate(10);将速率设为10,输出中分2段,第一段每次输出相隔200毫秒,第二段每次输出相隔100毫秒,可以非常精准的控制系统的QPS。
public class RateLimiterTest {
public static void main(String[] args) {
//permitsPerSecond=1 即QPS=1
RateLimiter rateLimiter = RateLimiter.create(1);
for (int i = 0; i < 10; i++) {
//调用acquire会根据QPS计算需要睡眠多久,返回耗时时间
double acquire = rateLimiter.acquire();
System.out.println(System.currentTimeMillis()+"耗时"+acquire);
}
System.out.println("----------");
//可以随时调整速率,我们将qps调整为10
rateLimiter.setRate(10);
for (int i = 0; i < 10; i++) {
//rateLimiter.acquire();
double acquire = rateLimiter.acquire();
System.out.println(System.currentTimeMillis()+"耗时"+acquire);
}
}
}
输出:
1599545866820耗时0.0
1599545867820耗时0.998552
1599545868819耗时0.997836
1599545869820耗时0.999819
1599545870820耗时0.998723
1599545871819耗时0.999232
1599545872819耗时0.999328
1599545873819耗时1.000024
1599545874819耗时0.99995
1599545875820耗时0.999597
----------
1599545876819耗时0.998575
1599545876920耗时0.099593
1599545877020耗时0.098779
1599545877119耗时0.098661
1599545877220耗时0.099558
1599545877319耗时0.098965
1599545877419耗时0.099139
1599545877520耗时0.099768
1599545877620耗时0.098729
1599545877720耗时0.0986
JUC中工具类CompletableFuture
CompletableFuture是java8中新增的一个类,算是对Future的一种增强,用起来很方便,也是会经常用到的一个工具类,熟悉一下。
CompletionStage接口
- CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段
- 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。比如:stage.thenApply(x -> square(x)).thenAccept(x -> System.out.print(x)).thenRun(() -> System.out.println())
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
CompletableFuture类
- 在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合 CompletableFuture 的方法。
- 它可能代表一个明确完成的Future,也有可能代表一个完成阶段( CompletionStage ),它支持在计算完成以后触发一些函数或执行某些动作。
- 它实现了Future和CompletionStage接口
常见的方法,熟悉一下:
runAsync 和 supplyAsync方法
CompletableFuture 提供了四个静态方法来创建一个异步操作。
public static CompletableFuture runAsync(Runnable runnable)
public static CompletableFuture runAsync(Runnable runnable, Executor executor)
public static CompletableFuture supplyAsync(Supplier supplier)
public static CompletableFuture supplyAsync(Supplier supplier, Executor executor)
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。
- runAsync方法不支持返回值。
- supplyAsync可以支持返回值。
示例:
public class CompletableFutureTest1 {
public static void main(String[] args) throws Exception {
CompletableFutureTest1.runAsync();
CompletableFutureTest1.supplyAsync();
}
//runAsync方法不支持返回值 Runnable
public static void runAsync() throws Exception {
CompletableFuture future = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println("run end ...");
});
future.get();
}
//supplyAsync可以支持返回值 Supplier
public static void supplyAsync() throws Exception {
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
System.out.println("run end ...");
return System.currentTimeMillis();
});
//如果没有future.get()阻塞等待结果的话,因为CompletableFuture.supplyAsync()方法默认是守护线程形式执行任务,在主线程结束后会跟着退出,
// 如果传入的是线程池去执行,这不是守护线程,不会导致退出
long time = future.get();
System.out.println("time = "+time);
}
}
输出:
run end ...
run end ...
time = 1599556248764
计算结果完成时的回调方法
当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:
public CompletableFuture whenComplete(BiConsumer super T,? super Throwable> action)
public CompletableFuture whenCompleteAsync(BiConsumer super T,? super Throwable> action)
public CompletableFuture whenCompleteAsync(BiConsumer super T,? super Throwable> action, Executor executor)
public CompletableFuture exceptionally(Function fn)
可以看到Action的类型是BiConsumer它可以处理正常的计算结果,或者异常情况。
whenComplete 和 whenCompleteAsync 的区别:
- whenComplete:当前任务的线程继续执行 whenComplete 的任务。
- whenCompleteAsync:把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
示例:
public class CompletableFutureTest1 {
public static void main(String[] args) throws Exception {
CompletableFutureTest1.whenComplete();
CompletableFutureTest1.whenCompleteBySupply();
}
public static void whenComplete() throws Exception {
CompletableFuture future = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
if (new Random().nextInt() % 2 >= 0) {
int i = 12 / 0;
//run end ...
//执行完成!
//int i = 12 / 0;
}
System.out.println("run end ...");
});
//对执行成功或者执行异常做处理的回调方法
future.whenComplete((avoid, throwable) -> {
//先判断是否抛异常分开处理
if (throwable != null) {
System.out.println("执行失败!" + throwable.getMessage());
} else {
System.out.println("执行完成!");
}
});
//对执行异常做处理的回调方法
future.exceptionally(throwable -> {
System.out.println("执行失败!" + throwable.getMessage());
return null;
}
);
TimeUnit.SECONDS.sleep(2);
}
public static void whenCompleteBySupply() throws Exception {
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
if (new Random().nextInt() % 2 >= 0) {
//int i = 12 / 0;
//run end ...
//执行完成!
//int i = 12 / 0;
}
System.out.println("run end ...");
return "success";
});
//whenComplete在thenAccept之前执行
future.thenAccept(result -> {
System.out.println(result);
});
//对执行成功或者执行异常做处理的回调方法
future.whenComplete((avoid, throwable) -> {
//先判断是否抛异常分开处理
if (throwable != null) {
System.out.println("执行失败!" + throwable.getMessage());
} else {
System.out.println("执行完成!");
}
});
//对执行异常做处理的回调方法
future.exceptionally(throwable -> {
System.out.println("执行失败!" + throwable.getMessage());
return null;
}
);
TimeUnit.SECONDS.sleep(2);
}
}
输出:
执行失败!java.lang.ArithmeticException: / by zero
执行失败!java.lang.ArithmeticException: / by zero
run end ...
执行完成!
success
thenApply 方法
当一个线程依赖另一个线程时,可以使用 thenApply、thenApplyAsync 方法来把这两个线程串行化。
public CompletableFuture thenApply(Function super T,? extends U> fn)
public CompletableFuture thenApplyAsync(Function super T,? extends U> fn)
public CompletableFuture thenApplyAsync(Function super T,? extends U> fn, Executor executor)
Function super T,? extends U>
T:上一个任务返回结果的类型
U:当前任务的返回值类型
示例:
public class CompletableFutureTest2 {
public static void main(String[] args) throws Exception {
CompletableFutureTest2.thenApply();
}
//多个CompletableFuture可以串行执行
//当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。
//多个任务串行执行,第二个任务依赖第一个任务的结果。
private static void thenApply() throws Exception {
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
long result = new Random().nextInt(100);
System.out.println("result1=" + result);
return result;
}
).thenApply((t -> {
long result = t * 5;
System.out.println("result2=" + result);
return result;
}));
//方式一:阻塞等待结果
long result = future.get();
System.out.println("result2: " + result);
//方式二:调用成功后接收任务的处理结果,并消费处理,无返回结果
future.thenAccept((r) -> {
System.out.println("result2: " + r);
});
}
}
输出:
result1=41
result2=205
result2: 205
result2: 205
handle 方法——可以处理正常和异常情况的thenApply 方法
handle 是执行任务完成时对结果的处理。
handle 方法和 thenApply 方法处理方式基本一样。不同的是 handle 是在任务完成后再执行,还可以处理异常的任务。thenApply 只可以执行正常的任务,任务出现异常则不执行 thenApply 方法。
public CompletionStage handle(BiFunction super T, Throwable, ? extends U> fn);
public CompletionStage handleAsync(BiFunction super T, Throwable, ? extends U> fn);
public CompletionStage handleAsync(BiFunction super T, Throwable, ? extends U> fn,Executor executor);
示例:在 handle 中可以根据任务是否有异常来进行做相应的后续处理操作。而 thenApply 方法,如果上个任务出现错误,则不会执行 thenApply 方法。
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
CompletableFutureTest3.handle();
}
public static void handle() throws Exception {
CompletableFuture future = CompletableFuture.supplyAsync(new Supplier() {
@Override
public Integer get() {
int i = 10 / 0;
return new Random().nextInt(10);
}
}).handle(
(param, throwable) -> {
int result = -1;
if (throwable == null) {
result = param * 2;
} else {
System.out.println(throwable.getMessage());
}
return result;
}
/*new BiFunction() {
@Override
public Integer apply(Integer param, Throwable throwable) {
int result = -1;
if(throwable==null){
result = param * 2;
}else{
System.out.println(throwable.getMessage());
}
return result;
}
}*/);
System.out.println(future.get());
}
}
输出:
java.lang.ArithmeticException: / by zero
-1
thenAccept 消费处理结果——无返回结果
接收任务的处理结果,并消费处理,无返回结果。
public CompletionStage thenAccept(Consumer super T> action);
public CompletionStage thenAcceptAsync(Consumer super T> action);
public CompletionStage thenAcceptAsync(Consumer super T> action,Executor executor);
示例:
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
//CompletableFutureTest3.handle();
CompletableFutureTest3.thenAccept();
}
public static void thenAccept() throws Exception {
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
return new Random().nextInt(10);
}
).thenAccept(integer -> {
System.out.println(integer);
});
future.get();
}
}
//输出:5
thenRun 方法——继续执行下一个Runnable任务,不获取上一个任务的处理结果
跟 thenAccept 方法不一样的是,不关心任务的处理结果。只要上面的任务执行完成,就开始执行 thenRun 。
public CompletionStage thenRun(Runnable action);
public CompletionStage thenRunAsync(Runnable action);
public CompletionStage thenRunAsync(Runnable action,Executor executor);
示例:
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
CompletableFutureTest3.thenRun();
}
public static void thenRun() throws Exception{
CompletableFuture future = CompletableFuture.supplyAsync(new Supplier() {
@Override
public Integer get() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new Random().nextInt(10);
}
}).thenRun(() -> {
System.out.println("thenRun ...");
});
future.get();
}
}
//2秒后输出:thenRun ...
thenCombine 合并任务
thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。
public CompletionStage thenCombine(CompletionStage extends U> other,BiFunction super T,? super U,? extends V> fn);
public CompletionStage thenCombineAsync(CompletionStage extends U> other,BiFunction super T,? super U,? extends V> fn);
public CompletionStage thenCombineAsync(CompletionStage extends U> other,BiFunction super T,? super U,? extends V> fn,Executor executor);
示例:
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
CompletableFutureTest3.thenCombine();
}
private static void thenCombine() throws Exception {
CompletableFuture future1 = CompletableFuture.supplyAsync(() -> {
return "hello";
});
CompletableFuture future2 = CompletableFuture.supplyAsync(() -> {
return "world";
});
CompletableFuture result = future1.thenCombine(future2, (result1, result2) -> {
return result1 + " " + result2;
});
System.out.println(result.get());
}
}
//输出:hello world
thenAcceptBoth
当两个CompletionStage都执行完成后,把结果一块交给thenAcceptBoth来进行消耗。
public CompletionStage thenAcceptBoth(CompletionStage extends U> other,BiConsumer super T, ? super U> action);
public CompletionStage thenAcceptBothAsync(CompletionStage extends U> other,BiConsumer super T, ? super U> action);
public CompletionStage thenAcceptBothAsync(CompletionStage extends U> other,BiConsumer super T, ? super U> action, Executor executor);
示例:
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
CompletableFutureTest3.thenAcceptBoth();
//等待守护进程执行完
TimeUnit.SECONDS.sleep(5);
}
private static void thenAcceptBoth() throws Exception {
CompletableFuture f1 = CompletableFuture.supplyAsync(() -> {
int t = new Random().nextInt(3);
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f1=" + t);
return t;
});
CompletableFuture f2 = CompletableFuture.supplyAsync(() -> {
int t = new Random().nextInt(3);
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f2=" + t);
return t;
});
f1.thenAcceptBoth(f2, (result1, result2) -> {
System.out.println("f1=" + result1 + ";f2=" + result2 + ";");
});
}
}
输出:
f1=1
f2=1
f1=1;f2=1;
applyToEither 方法——有返回值消耗
两个CompletionStage,谁执行返回的结果快,我就用那个CompletionStage的结果进行下一步的转化操作。
public CompletionStage applyToEither(CompletionStage extends T> other,Function super T, U> fn);
public CompletionStage applyToEitherAsync(CompletionStage extends T> other,Function super T, U> fn);
public CompletionStage applyToEitherAsync(CompletionStage extends T> other,Function super T, U> fn,Executor executor);
示例:
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
CompletableFutureTest3.applyToEither();
}
private static void applyToEither() throws Exception {
CompletableFuture f1 = CompletableFuture.supplyAsync(()->{
int t = 1;
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f1="+t);
return t;
});
CompletableFuture f2 = CompletableFuture.supplyAsync(()->{
int t = 2;
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f2="+t);
return t;
});
CompletableFuture result = f1.applyToEither(f2, (r)->{
System.out.println(r);
return r * 2;
});
System.out.println(result.get());
}
输出:
f1=1
1
2
acceptEither 方法——无返回值消耗
两个CompletionStage,谁执行返回的结果快,我就用那个CompletionStage的结果进行下一步的消耗操作。注意,这时候其实两个CompletionStage都是会执行完的,只是我们只获取其中的一个比较快的结果而已,参考示例的输出。
public CompletionStage acceptEither(CompletionStage extends T> other,Consumer super T> action);
public CompletionStage acceptEitherAsync(CompletionStage extends T> other,Consumer super T> action);
public CompletionStage acceptEitherAsync(CompletionStage extends T> other,Consumer super T> action,Executor executor);
示例:
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
//CompletableFutureTest3.applyToEither();
CompletableFutureTest3.acceptEither();
TimeUnit.SECONDS.sleep(5);
}
private static void acceptEither() throws Exception {
CompletableFuture f1 = CompletableFuture.supplyAsync(() -> {
int t = new Random().nextInt(3);
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f1=" + t);
return t;
});
CompletableFuture f2 = CompletableFuture.supplyAsync(() -> {
int t = new Random().nextInt(3);
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f2=" + t);
return t;
});
f1.acceptEither(f2, (t) -> {
System.out.println(t);
});
}
}
输出:
f1=1
1
f2=2
runAfterEither 方法
两个CompletionStage,任何一个完成了都会执行下一步的操作(Runnable),两个CompletionStage都是会执行完的.
public CompletionStage runAfterEither(CompletionStage> other,Runnable action);
public CompletionStage runAfterEitherAsync(CompletionStage> other,Runnable action);
public CompletionStage runAfterEitherAsync(CompletionStage> other,Runnable action,Executor executor);
示例代码
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
//CompletableFutureTest3.applyToEither();
//CompletableFutureTest3.acceptEither();
CompletableFutureTest3.runAfterEither();
TimeUnit.SECONDS.sleep(5);
}
private static void runAfterEither() throws Exception {
CompletableFuture f1 = CompletableFuture.supplyAsync(new Supplier() {
@Override
public Integer get() {
int t = new Random().nextInt(3);
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f1=" + t);
return t;
}
});
CompletableFuture f2 = CompletableFuture.supplyAsync(new Supplier() {
@Override
public Integer get() {
int t = new Random().nextInt(3);
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f2=" + t);
return t;
}
});
f1.runAfterEither(f2, ()->{
System.out.println("上面有一个已经完成了。");
});
}
}
输出:
f1=0
上面有一个已经完成了。
f2=1
runAfterBoth
两个CompletionStage,都完成了计算才会执行下一步的操作(Runnable),注意输出顺序,runAfterBoth方法要等两个CompletionStage都执行完了才会执行。
public CompletionStage runAfterBoth(CompletionStage> other,Runnable action);
public CompletionStage runAfterBothAsync(CompletionStage> other,Runnable action);
public CompletionStage runAfterBothAsync(CompletionStage> other,Runnable action,Executor executor);
示例代码
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
//CompletableFutureTest3.applyToEither();
//CompletableFutureTest3.acceptEither();
//CompletableFutureTest3.runAfterEither();
CompletableFutureTest3.runAfterBoth();
TimeUnit.SECONDS.sleep(5);
}
private static void runAfterBoth() throws Exception {
CompletableFuture f1 = CompletableFuture.supplyAsync(new Supplier() {
@Override
public Integer get() {
int t = new Random().nextInt(3);
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f1="+t);
return t;
}
});
CompletableFuture f2 = CompletableFuture.supplyAsync(new Supplier() {
@Override
public Integer get() {
int t = new Random().nextInt(3);
try {
TimeUnit.SECONDS.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("f2="+t);
return t;
}
});
f1.runAfterBoth(f2, new Runnable() {
@Override
public void run() {
System.out.println("上面两个任务都执行完成了。");
}
});
}
}
输出:
f1=1
f2=2
上面两个任务都执行完成了。
thenCompose 方法
thenCompose 方法允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。
public CompletableFuture thenCompose(Function super T, ? extends CompletionStage> fn);
public CompletableFuture thenComposeAsync(Function super T, ? extends CompletionStage> fn) ;
public CompletableFuture thenComposeAsync(Function super T, ? extends CompletionStage> fn, Executor executor) ;
示例代码
public class CompletableFutureTest3 {
public static void main(String[] args) throws Exception {
CompletableFutureTest3.thenCompose();
TimeUnit.SECONDS.sleep(3);
}
private static void thenCompose() throws Exception {
CompletableFuture f = CompletableFuture.supplyAsync(() -> {
int t = new Random().nextInt(3);
System.out.println("t1=" + t);
return t;
}).thenCompose((param) -> {
return CompletableFuture.supplyAsync(() -> {
int t = param * 2;
System.out.println("t2=" + t);
return t;
});
});
System.out.println("thenCompose result : " + f.get());
}
}
输出:
t1=1
t2=2
thenCompose result : 2
疑问:
Q:thenAcceptBoth与thenCombine 的区别是什么?
A:thenAcceptBoth无返回值消耗执行,thenCombine 会有返回值。一般accept都是没有返回值的,apply是有返回值的。
Q:thenCompose 与thenApply 方法 的区别是什么?不都是串行执行下一个任务,并把第一个任务作为参数传递给第二个任务么?
获取线程执行结果的6种方法
方式1:Thread的join()方法实现
代码中通过join方式阻塞了当前主线程,当thread线程执行完毕之后,join方法才会继续执行。
join的方式,只能阻塞一个线程,如果其他线程中也需要获取thread线程的执行结果,join方法无能为力了。
示例:
public class ThreadJoinTest {
//用于封装结果
static class Result {
T result;
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result<>();
Thread t = new Thread(() -> {
System.out.println("start thread!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
result.setResult("success");
System.out.println("end thread!");
});
t.start();
//让主线程等待thread线程执行完毕之后再继续,join方法会让当前线程阻塞
t.join();
System.out.println("main get result="+result.getResult());
}
}
输出:
start thread!
end thread!
main get result=success
方式2:CountDownLatch实现
使用CountDownLatch可以让一个或者多个线程等待一批线程完成之后,自己再继续.
示例:
public class CountDownLatchTest2 {
static class Result{
private T result;
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result<>();
CountDownLatch latch = new CountDownLatch(1);
Thread t = new Thread(() -> {
System.out.println("start thread!");
try {
TimeUnit.SECONDS.sleep(1);
result.setResult("success");
System.out.println("end thread!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
latch.countDown();
}
});
t.start();
latch.await();
System.out.println("main get result="+result.getResult());
}
}
输出:
start thread!
end thread!
main get result=success
方式3:ExecutorService.submit方法实现——ThreadPoolExecutor
使用ExecutorService.submit方法实现的,此方法返回一个Future,future.get()会让当前线程阻塞,直到Future关联的任务执行完毕。
示例:
public class ThreadPoolExecutorTest2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//自定义包含策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
Future future = executor.submit(() -> {
System.out.println("start thread!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end thread!");
return "success";
});
executor.shutdown();
System.out.println("main get result="+future.get());
}
}
输出同上。
方式4:FutureTask方式1——作为Runnable传给Thread执行
线程池的submit方法传入的Callable对象本质上也是包装成一个FutureTask来执行。
代码中使用FutureTask实现的,FutureTask实现了Runnable接口,并且内部带返回值,所以可以传递给Thread直接运行,futureTask.get()会阻塞当前线程,直到FutureTask构造方法传递的任务执行完毕,get方法才会返回。
示例:
public class FutureTaskTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个FutureTask
FutureTask futureTask = new FutureTask<>(() -> {
System.out.println("start thread!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end thread!");
return "success";
});
//将futureTask传递一个线程运行
new Thread(futureTask).start();
//futureTask.get()会阻塞当前线程,直到futureTask执行完毕
String result = futureTask.get();
System.out.println("main get result=" + result);
}
}
方式5:FutureTask方式2——构造FutureTask对象及执行内容,直接在Thread里面跑run方法
当futureTask的run()方法执行完毕之后,futureTask.get()会从阻塞中返回。
示例:
public class FutureTaskTest1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个FutureTask
FutureTask futureTask = new FutureTask<>(() -> {
System.out.println("start thread!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end thread!");
return "success";
});
//将futureTask传递一个线程运行
new Thread(()->futureTask.run()).start();
//futureTask.get()会阻塞当前线程,直到futureTask执行完毕
String result = futureTask.get();
System.out.println("main get result=" + result);
}
}
方式6:CompletableFuture方式实现
CompletableFuture.supplyAsync可以用来异步执行一个带返回值的任务,调用completableFuture.get()
会阻塞当前线程,直到任务执行完毕,get方法才会返回。
public class CompletableFutureTest4 {
public static void main(String[] args) throws Exception {
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
System.out.println("start thread!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end thread!");
return "success";
});
// future.get()会阻塞当前线程直到获得结果
System.out.println("main get result="+future.get());
}
}
高并发中计数器的四种实现方式
需求:一个jvm中实现一个计数器功能,需保证多线程情况下数据正确性。
我们来模拟50个线程,每个线程对计数器递增100万次,最终结果应该是5000万。
我们使用4种方式实现,看一下其性能,然后引出为什么需要使用LongAdder、LongAccumulator。
方式一:使用加锁的方式实现——synchronized或Lock
从示例输出结果看,ReentrantLock的效率明显比synchronized差了2-3倍。
示例:
public class SynchronizeCalculator {
private static long count = 0;
private static Lock lock = new ReentrantLock();
public synchronized static void incrment() {
count++;
}
public static void incrmentByLock() {
lock.lock();
try {
count++;
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
count = 0;
averageTest();
}
}
public static void averageTest() throws InterruptedException {
long t1 = System.currentTimeMillis();
//自定义包含策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
CountDownLatch latch = new CountDownLatch(50);
for (int i = 0; i < 50; i++) {
executor.execute(() -> {
try {
for (int j = 0; j < 1000000; j++) {
incrment();
//incrmentByLock();
}
} finally {
latch.countDown();
}
});
}
latch.await();
long t2 = System.currentTimeMillis();
System.out.println(String.format("结果:%s,耗时(ms):%s", count, (t2 - t1)));
executor.shutdown();
}
}
输出:
//synchronized
结果:50000000,耗时(ms):490
结果:50000000,耗时(ms):1574
结果:50000000,耗时(ms):399
结果:50000000,耗时(ms):395
结果:50000000,耗时(ms):396
//lock
结果:50000000,耗时(ms):1289
结果:50000000,耗时(ms):1239
结果:50000000,耗时(ms):1224
结果:50000000,耗时(ms):1219
结果:50000000,耗时(ms):1246
方式2:AtomicLong实现
AtomicLong内部采用CAS的方式实现,并发量大的情况下,CAS失败率比较高,导致性能比synchronized还低一些。并发量不是太大的情况下,CAS性能还是可以的。
示例:
public class AtomicLongTest {
private static AtomicLong count = new AtomicLong(0);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
count.set(0);
averageTest();
}
}
public static void averageTest() throws InterruptedException {
long t1 = System.currentTimeMillis();
//自定义包含策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
CountDownLatch latch = new CountDownLatch(50);
for (int i = 0; i < 50; i++) {
executor.execute(() -> {
try {
for (int j = 0; j < 1000000; j++) {
count.getAndIncrement();
}
} finally {
latch.countDown();
}
});
}
latch.await();
long t2 = System.currentTimeMillis();
System.out.println(String.format("结果:%s,耗时(ms):%s", count.get(), (t2 - t1)));
executor.shutdown();
}
}
输出:
结果:50000000,耗时(ms):1018
结果:50000000,耗时(ms):1442
结果:50000000,耗时(ms):1033
结果:50000000,耗时(ms):935
结果:50000000,耗时(ms):1320
方式3:LongAdder实现——相当于锁分段技术
先介绍一下LongAdder,说到LongAdder,不得不提的就是AtomicLong,AtomicLong是JDK1.5开始出现的,里面主要使用了一个long类型的value作为成员变量,然后使用循环的CAS操作去操作value的值,并发量比较大的情况下,CAS操作失败的概率较高,内部失败了会重试,导致耗时可能会增加。
LongAdder是JDK1.8开始出现的,所提供的API基本上可以替换掉原先的AtomicLong。LongAdder在并发量比较大的情况下,操作数据的时候,相当于把这个数字分成了很多份数字,然后交给多个人去管控,每个管控者负责保证部分数字在多线程情况下操作的正确性。当多线程访问的时,通过hash算法映射到具体管控者去操作数据,最后再汇总所有的管控者的数据,得到最终结果。相当于降低了并发情况下锁的粒度,所以效率比较高,看一下下面的图,方便理解:
示例:
代码中new LongAdder()创建一个LongAdder对象,内部数字初始值是0,调用increment()方法可以对LongAdder内部的值原子递增1。reset()方法可以重置LongAdder的值,使其归0。
public class LongAdderTest {
private static LongAdder count = new LongAdder();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
count.reset();
averageTest();
}
}
public static void averageTest() throws InterruptedException {
long t1 = System.currentTimeMillis();
//自定义包含策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
CountDownLatch latch = new CountDownLatch(50);
for (int i = 0; i < 50; i++) {
executor.execute(() -> {
try {
for (int j = 0; j < 1000000; j++) {
count.increment();
}
} finally {
latch.countDown();
}
});
}
latch.await();
long t2 = System.currentTimeMillis();
System.out.println(String.format("结果:%s,耗时(ms):%s", count.sum(), (t2 - t1)));
executor.shutdown();
}
}
输出:
结果:50000000,耗时(ms):209
结果:50000000,耗时(ms):133
结果:50000000,耗时(ms):149
结果:50000000,耗时(ms):146
结果:50000000,耗时(ms):148
方式4:LongAccumulator实现
LongAccumulator介绍
LongAccumulator是LongAdder的功能增强版。LongAdder的API只有对数值的加减,而LongAccumulator提供了自定义的函数操作,其构造函数如下:
/**
* accumulatorFunction:需要执行的二元函数(接收2个long作为形参,并返回1个long)
* identity:初始值
**/
public LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) {
this.function = accumulatorFunction;
base = this.identity = identity;
}
示例:
LongAccumulator的效率和LongAdder差不多,不过更灵活一些。
调用new LongAdder()等价于new LongAccumulator((x, y) -> x + y, 0L)。
从上面4个示例的结果来看,LongAdder、LongAccumulator全面超越同步锁及AtomicLong的方式,建议在使用AtomicLong的地方可以直接替换为LongAdder、LongAccumulator,吞吐量更高一些。
public class LongAccumulatorTest {
private static volatile LongAccumulator count = new LongAccumulator((x, y) -> x + y, 0);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
count.reset();
averageTest();
}
}
public static void averageTest() throws InterruptedException {
long t1 = System.currentTimeMillis();
//自定义包含策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
CountDownLatch latch = new CountDownLatch(50);
for (int i = 0; i < 50; i++) {
executor.execute(() -> {
try {
for (int j = 0; j < 1000000; j++) {
count.accumulate(1);
}
} finally {
latch.countDown();
}
});
}
latch.await();
long t2 = System.currentTimeMillis();
System.out.println(String.format("结果:%s,耗时(ms):%s", count.longValue(), (t2 - t1)));
executor.shutdown();
}
}
输出:
结果:50000000,耗时(ms):152
结果:50000000,耗时(ms):148
结果:50000000,耗时(ms):137
结果:50000000,耗时(ms):137
结果:50000000,耗时(ms):144
疑问:
Q:LongAccumulator.reset方法并不能重置重置LongAccumulator的identity:初始值正确,使其恢复原来的初始值。当初始值为0是不会发生这个问题,而当我们设置初始值如1时,就会导致后续的计算操作增加了5份初始值,目前猜测原因是因为代码中LongAccumulator在并发量比较大的情况下,操作数据的时候,相当于把这个数字分成了很多份数字 ,而初始化的时候也是初始化了多份数据,导致初始值叠加了多份。不知道这是个bug么?待解惑。
在此记录下来希望有遇到这种情况的同学注意。解决方法便是要么初始值identity=0不会有这种问题;或者有需要使用reset方法重置的改为重新创建个LongAccumulator处理。
源码:
public void reset() {
Cell[] as = cells; Cell a;
base = identity;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
//对多个cell进行初始值赋值导致后面计算叠加了多份初始值
a.value = identity;
}
}
}
示例:
public class LongAccumulatorTest {
//设置初始值为1查看输出结果
private static volatile LongAccumulator count = new LongAccumulator((x, y) -> x + y, 1);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
count.reset();
averageTest();
}
}
public static void averageTest() throws InterruptedException {
long t1 = System.currentTimeMillis();
//自定义包含策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
CountDownLatch latch = new CountDownLatch(50);
for (int i = 0; i < 50; i++) {
executor.execute(() -> {
try {
for (int j = 0; j < 1000000; j++) {
count.accumulate(1);
}
} finally {
latch.countDown();
}
});
}
latch.await();
long t2 = System.currentTimeMillis();
System.out.println(String.format("结果:%s,耗时(ms):%s", count.longValue(), (t2 - t1)));
executor.shutdown();
}
}
输出:这时候你会发现只有第一次计算是正确的,只要使用了rest方法重置就会导致这个错误。
结果:50000001,耗时(ms):185
结果:50000005,耗时(ms):143
结果:50000005,耗时(ms):139
结果:50000005,耗时(ms):162
结果:50000005,耗时(ms):142
演示公平锁和非公平锁
先理解一下什么是公平锁、非公平锁?
公平锁和非公平锁体现在别人释放锁的一瞬间,如果前面已经有排队的,新来的是否可以插队,如果可以插队表示是非公平的,如果不可用插队,只能排在最后面,是公平的方式。
示例:
@Slf4j
public class ReentrantLockTest2 {
public static void main(String[] args) throws InterruptedException {
ReentrantLockTest2.LockTest(false);
TimeUnit.SECONDS.sleep(4);
log.error("-------------------------------");
ReentrantLockTest2.LockTest(true);
}
public static void LockTest(boolean fair) throws InterruptedException {
ReentrantLock lock = new ReentrantLock(fair);
new Thread(() -> {
lock.lock();
try {
log.error(Thread.currentThread().getName() + " start!");
TimeUnit.SECONDS.sleep(3);
new Thread(() -> {
//注意线程池要当前线程创建的才能使用,如果传给新开的线程会获取不到线程池
test("后到组",lock);
}).start();
//test(executorAfter,lock);
log.error(Thread.currentThread().getName() + "end!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Hold Lock 4 Test Thread").start();
test("先到组",lock);
TimeUnit.SECONDS.sleep(3);
}
private static void test(String name,Lock lock){
//自定义包含策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
new DemoThreadFactory(name), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
lock.lock();
try {
log.error("获取到锁!");
} finally {
lock.unlock();
}
});
}
executor.shutdown();
}
}
输出:
14:45:23.204 [Hold Lock 4 Test Thread] ERROR com.self.current.ReentrantLockTest2 - Hold Lock 4 Test Thread start!
14:45:26.211 [Hold Lock 4 Test Thread] ERROR com.self.current.ReentrantLockTest2 - Hold Lock 4 Test Threadend!
14:45:26.211 [From DemoThreadFactory's 先到组-Worker-1] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.211 [From DemoThreadFactory's 先到组-Worker-2] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.212 [From DemoThreadFactory's 先到组-Worker-3] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.212 [From DemoThreadFactory's 先到组-Worker-4] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.212 [From DemoThreadFactory's 先到组-Worker-5] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.212 [From DemoThreadFactory's 先到组-Worker-6] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.212 [From DemoThreadFactory's 先到组-Worker-7] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.212 [From DemoThreadFactory's 先到组-Worker-8] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.212 [From DemoThreadFactory's 后到组-Worker-4] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.212 [From DemoThreadFactory's 先到组-Worker-9] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.213 [From DemoThreadFactory's 后到组-Worker-8] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.218 [From DemoThreadFactory's 后到组-Worker-10] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.218 [From DemoThreadFactory's 先到组-Worker-10] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.218 [From DemoThreadFactory's 后到组-Worker-2] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.218 [From DemoThreadFactory's 后到组-Worker-1] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.219 [From DemoThreadFactory's 后到组-Worker-3] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.219 [From DemoThreadFactory's 后到组-Worker-5] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.219 [From DemoThreadFactory's 后到组-Worker-6] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.219 [From DemoThreadFactory's 后到组-Worker-7] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:26.219 [From DemoThreadFactory's 后到组-Worker-9] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:30.205 [main] ERROR com.self.current.ReentrantLockTest2 - -------------------------------
14:45:30.205 [Hold Lock 4 Test Thread] ERROR com.self.current.ReentrantLockTest2 - Hold Lock 4 Test Thread start!
14:45:33.206 [Hold Lock 4 Test Thread] ERROR com.self.current.ReentrantLockTest2 - Hold Lock 4 Test Threadend!
14:45:33.206 [From DemoThreadFactory's 先到组-Worker-1] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.206 [From DemoThreadFactory's 先到组-Worker-2] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.209 [From DemoThreadFactory's 先到组-Worker-3] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.209 [From DemoThreadFactory's 先到组-Worker-4] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.209 [From DemoThreadFactory's 先到组-Worker-5] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.209 [From DemoThreadFactory's 先到组-Worker-6] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.210 [From DemoThreadFactory's 先到组-Worker-7] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.210 [From DemoThreadFactory's 先到组-Worker-8] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.210 [From DemoThreadFactory's 先到组-Worker-9] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.210 [From DemoThreadFactory's 先到组-Worker-10] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.210 [From DemoThreadFactory's 后到组-Worker-2] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.210 [From DemoThreadFactory's 后到组-Worker-1] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.211 [From DemoThreadFactory's 后到组-Worker-6] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.211 [From DemoThreadFactory's 后到组-Worker-7] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.211 [From DemoThreadFactory's 后到组-Worker-5] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.211 [From DemoThreadFactory's 后到组-Worker-4] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.211 [From DemoThreadFactory's 后到组-Worker-3] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.211 [From DemoThreadFactory's 后到组-Worker-9] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.212 [From DemoThreadFactory's 后到组-Worker-10] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
14:45:33.212 [From DemoThreadFactory's 后到组-Worker-8] ERROR com.self.current.ReentrantLockTest2 - 获取到锁!
google提供的一些好用的并发工具类
关于并发方面的,juc已帮我们提供了很多好用的工具,而谷歌在此基础上做了扩展,使并发编程更容易,这些工具放在guava.jar包中。
guava maven配置
com.google.guava
guava
27.0-jre
guava中常用几个类
MoreExecutors:提供了一些静态方法,是对juc中的Executors类的一个扩展。
Futures:也提供了很多静态方法,是对juc中Future的一个扩展。
案例1:异步执行任务完毕之后回调——相当于CompletableFuture的whenComplete方法
ListeningExecutorService接口继承于juc中的ExecutorService接口,对ExecutorService做了一些扩展,看其名字中带有Listening,说明这个接口自带监听的功能,可以监听异步执行任务的结果。通过MoreExecutors.listeningDecorator创建一个ListeningExecutorService对象,需传递一个ExecutorService参数,传递的ExecutorService负责异步执行任务。
ListeningExecutorService的submit方法用来异步执行一个任务,返回ListenableFuture,ListenableFuture接口继承于juc中的Future接口,对Future做了扩展,使其带有监听的功能。调用submit.addListener可以在执行的任务上添加监听器,当任务执行完毕之后会回调这个监听器中的方法。
ListenableFuture的get方法会阻塞当前线程直到任务执行完毕。
另一种回调方式是通过调用Futures的静态方法addCallback在异步执行的任务中添加回调,回调的对象是一个FutureCallback,此对象有2个方法,任务执行成功调用onSuccess,执行失败调用onFailure。
失败的情况可以将代码中int i = 10 / 0;注释去掉,执行一下可以看看效果。
示例:
@Slf4j
public class GuavaTest {
//相当于CompletableFuture的whenComplete方法
public static void main1(String[] args) throws ExecutionException, InterruptedException {
//创建一个线程池
ExecutorService delegate = Executors.newFixedThreadPool(5);
try {
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(delegate);
//异步执行一个任务
ListenableFuture submit = executorService.submit(() -> {
log.error("{}", System.currentTimeMillis());
//休眠2秒,默认耗时
TimeUnit.SECONDS.sleep(2);
log.error("{}", System.currentTimeMillis());
return 10;
});
//当任务执行完毕之后回调对应的方法
submit.addListener(() -> {
log.error("任务执行完毕了,我被回调了");
}, MoreExecutors.directExecutor());
log.error("{}", submit.get());
} finally {
delegate.shutdown();
}
}
//相当于CompletableFuture的whenComplete方法
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(10),
new DemoThreadFactory("订单创建组"), new ThreadPoolExecutor.AbortPolicy());
ListeningExecutorService service = MoreExecutors.listeningDecorator(executor);
try {
ListenableFuture future = service.submit(() -> {
log.error("{}", System.currentTimeMillis());
//休眠2秒,默认耗时
TimeUnit.SECONDS.sleep(2);
//int i = 10 / 0;
log.error("{}", System.currentTimeMillis());
return 10;
});
Futures.addCallback(future, new FutureCallback() {
@Override
public void onSuccess(Integer integer) {
log.error("执行成功:{}", integer);
}
@Override
public void onFailure(Throwable throwable) {
log.error("执行失败:{}", throwable.getMessage());
}
});
log.error("{}", future.get());
} finally {
service.shutdown();
}
}
}
输出:
15:32:54.480 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.GuavaTest - 1599809574477
15:32:56.487 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.GuavaTest - 1599809576487
15:32:56.488 [main] ERROR com.self.current.GuavaTest - 10
15:32:56.488 [From DemoThreadFactory's 订单创建组-Worker-1] ERROR com.self.current.GuavaTest - 执行成功:10
示例2:获取一批异步任务的执行结果——Futures.allAsList(futureList).get()
结果中按顺序输出了6个异步任务的结果,此处用到了Futures.allAsList方法,看一下此方法的声明:
public static ListenableFuture> allAsList(
Iterable extends ListenableFuture extends V>> futures)
传递一批ListenableFuture,返回一个ListenableFuture,内部将一批结果转换为了一个ListenableFuture对象。
示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
log.error("star");
ExecutorService delegate = Executors.newFixedThreadPool(5);
try {
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(delegate);
List> futureList = new ArrayList<>();
for (int i = 5; i >= 0; i--) {
int j = i;
futureList.add(executorService.submit(() -> {
TimeUnit.SECONDS.sleep(j);
return j;
}));
}
//把多个ListenableFuture转换为一个ListenableFuture
//ListenableFuture> listListenableFuture = Futures.allAsList(futureList);
//获取一批任务的执行结果
List resultList = Futures.allAsList(futureList).get();
//输出
resultList.forEach(item -> {
log.error("{}", item);
});
} finally {
delegate.shutdown();
}
}
输出:
15:45:51.160 [main] ERROR com.self.current.GuavaTest - star
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 5
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 4
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 3
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 2
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 1
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 0
示例3:一批任务异步执行完毕之后回调——包装futureList传递给Futures.addCallback 方法
异步执行一批任务,最后计算其和,代码中异步执行了一批任务,所有任务完成之后,回调了上面的onSuccess方法,内部对所有的结果进行sum操作。
示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
log.error("start");
ExecutorService delegate = Executors.newFixedThreadPool(5);
try {
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(delegate);
List> futureList = new ArrayList<>();
for (int i = 5; i >= 0; i--) {
int j = i;
futureList.add(executorService.submit(() -> {
TimeUnit.SECONDS.sleep(j);
return j;
}));
}
//把多个ListenableFuture转换为一个ListenableFuture
ListenableFuture> listListenableFuture = Futures.allAsList(futureList);
Futures.addCallback(listListenableFuture, new FutureCallback>() {
@Override
public void onSuccess(List result) {
log.error("result中所有结果之和:"+result.stream().reduce(Integer::sum).get());
}
@Override
public void onFailure(Throwable throwable) {
log.error("执行任务发生异常:" + throwable.getMessage(), throwable);
}
});
} finally {
delegate.shutdown();
}
}
输出:
15:57:00.539 [main] ERROR com.self.current.GuavaTest - start
15:57:05.560 [pool-2-thread-1] ERROR com.self.current.GuavaTest - result中所有结果之和:15
其他疑问:
Q:运行下面这个例子结束不了,debug倒是可以,这是为什么呢?Thread[Monitor Ctrl-Break,5,main]是哪来的?
public class VolatileTest1 {
public static volatile int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (Thread thread : threads) {
thread = new Thread(()->{
for (int i = 0; i < 1000; i++) {
VolatileTest1.add();
}
});
thread.start();
thread.join();
}
//2
//java.lang.ThreadGroup[name=main,maxpri=10]
// Thread[main,5,main]
// Thread[Monitor Ctrl-Break,5,main]
//结束不了,debug倒是可以,这是为什么呢?Thread[Monitor Ctrl-Break,5,main]是哪来的?
while (Thread.activeCount() >1){
Thread.yield();
System.out.println(Thread.activeCount());
ThreadGroup parent = Thread.currentThread().getThreadGroup();
parent.list();
}
System.out.println("num="+num);
}
}