基础概念
进程与线程
什么是进程?
进程是指运行中的程序。比如使用钉钉,浏览器,需要启动这个程序,操作系统会给这个线程分配一定的资源(占用内存资源)
什么是线程?
线程是CPU调度的基本单位,每个线程执行的都是某一个进程的代码片段
进程和线程的区别:
根本不同:进程是操作系统分配的资源,而线程是CPU调度的基本单位
资源方面:同一个进程下的线程共享进程的一些资源。线程同时拥有自身的独立存储空间。进程之间的资源通常是独立的。
数量不同:进程一般指的是一个进程。而线程是依附于某个进程的,而且一个进程中至少会有一个或多个线程
开销不同:毕竟进程和线程不是一个级别的内容,线程的创建和终止的时间是比较短的。而且线程之间的切换比进程之间的切换速度要快很多。而且进程之间的通讯很麻烦,一般要借助内核才可以实现,而线程之间通信,比较方便
串行、并行、并发
什么是串行:
串行就是一个一个排队,第一个做完,第二个才能上。
什么是并行:
并行就是同时处理。(一起上!!!)
什么是并发:
这里的并发并不是三高中的高并发问题,这里是多线程中的并发概念(CPU调度线程的概念)。CPU在极短的时间内,反复切换执行不同的线程,看似好像是并行,但是只是CPU高速的切换。
并行囊括并发。
并行就是多核CPU同时调度多个线程,是真正的多个线程同时执行。
单核CPU无法实现并行效果,单核CPU是并发。
同步异步、阻塞非阻塞
同步与异步:执行某个功能后,被调用者是否会主动反馈
信息
阻塞和非阻塞:执行某个功能后,调用者是否需要一直等待结果
的反馈
两个概念看似相似,但是侧重点是完全不一样的。
同步阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,需要一直等待水烧开。
同步非阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能,但是需要时不时的查看水开了没。
异步阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,需要一直等待水烧开。
异步非阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能。
异步非阻塞这个效果是最好的,平时开发时,提升效率最好的方式就是采用异步非阻塞的方式处理一些多线程的任务。
线程的创建
-
继承Thread类 重写run方法
-
实现Runnable接口 重写run方法
public class MiTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
t1.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main:" + i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("MyRunnable:" + i);
}
}
}
匿名内部类方式:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("匿名内部类:" + i);
}
}
});
lambda方式:
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("lambda:" + i);
}
});
- 实现Callable 重写call方法,配合FutureTask
public class MiTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建MyCallable
MyCallable myCallable = new MyCallable();
//2. 创建FutureTask,传入Callable
FutureTask futureTask = new FutureTask(myCallable);
//3. 创建Thread线程
Thread t1 = new Thread(futureTask);
//4. 启动线程
t1.start();
//5. 做一些操作
//6. 要结果
Object count = futureTask.get();
System.out.println("总和为:" + count);
}
}
class MyCallable implements Callable{
@Override
public Object call() throws Exception {
int count = 0;
for (int i = 0; i < 100; i++) {
count += i;
}
return count;
}
}
- 基于线程池构建线程
追其底层,其实只有一种,实现Runnble
线程的状态
操作系统层面
- 新建状态(new)
- 就绪状态(ready)
- 运行状态(running)
- 等待状态(waiting)
- 结束状态(terminated)
java运行层面 - 新建状态(new)
- 运行状态(runnable)
- 阻塞状态(blocked)
- 等待状态(waiting)
- 有时等待状态(Timed waiting)
- 结束状态(terminated)
NEW:Thread对象被创建出来了,但是还没有执行start方法。
RUNNABLE:Thread对象调用了start方法,就为RUNNABLE状态(CPU调度/没有调度)
BLOCKED、WAITING、TIME_WAITING:都可以理解为是阻塞、等待状态,因为处在这三种状态下,CPU不会调度当前线程
BLOCKED:synchronized没有拿到同步锁,被阻塞的情况
WAITING:调用wait方法就会处于WAITING状态,需要被手动唤醒
TIME_WAITING:调用sleep方法或者join方法,会被自动唤醒,无需手动唤醒
TERMINATED:run方法执行完毕,线程生命周期到头了
线程的常用方法
获取当前线程
Thread main = Thread.currentThread();
线程的名字
Thread.currentThread().getName()
t1.setName("模块-功能-计数器");
线程的优先级
其实就是CPU调度线程的优先级、
java中给线程设置的优先级别有10个级别,从1~10任取一个整数。
如果超出这个范围,会排除参数异常的错误
t1.setPriority(1);
t2.setPriority(10);
线程的让步
Thread.yield();
线程的休眠
Thread的静态方法,让线程从运行状态转变为等待状态
sleep有两个方法重载:
- 第一个就是native修饰的,让线程转为等待状态的效果
- 第二个是可以传入毫秒和一个纳秒的方法(如果纳秒值大于等于0.5毫秒,就给休眠的毫秒值+1。如果传入的毫秒值是0,纳秒值不为0,就休眠1毫秒)
sleep会抛出一个InterruptedException
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(System.currentTimeMillis());
}
线程的强占
Thread的非静态方法join方法
需要在某一个线程下去调用这个方法
如果在main线程中调用了t1.join(),那么main线程会进入到等待状态,需要等待t1线程全部执行完毕,在恢复到就绪状态等待CPU调度。
如果在main线程中调用了t1.join(2000),那么main线程会进入到等待状态,需要等待t1执行2s后,在恢复到就绪状态等待CPU调度。如果在等待期间,t1已经结束了,那么main线程自动变为就绪状态等待CPU调度。
守护线程
t1.setDaemon(true);
线程的等待和唤醒
wait()
notify()
notifyAll()
线程的结束方式
stop方法(不用)
使用共享变量(很少会用) static volatile boolean flag = true;
interrupt方式
wait和sleep的区别?
- sleep属于Thread类的static方法、wait属于Object类的方法
- sleep属于TIMED_WAITING,自动被唤醒、wait属于WAITING,需要手动唤醒。
- sleep可以在持有锁时,执行,不会释放锁资源、wait在执行后,会释放资源
- sleep可以在持有锁或不持有锁时执行。 wait方法必须在只有锁时才可以执行。
wait方法会将持有锁的线程从owner扔到WaitSet集合中,这个操作是在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。
并发编程的三大特性
原子性
JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定差异的。Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。
原子性定义:原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。
原子性:多线程操作临界资源,预期的结果与最终结果一致。
synchronized
因为++操作可以从指令中查看到
可以在方法上追加synchronized关键字或者采用同步代码块的形式来保证原子性
synchronized可以让避免多线程同时操作临街资源,同一时间点,只会有一个线程正在操作临界资源
CAS
到底什么是CAS
compare and swap也就是比较和交换,他是一条CPU的并发原语。
本质上是不可被线程调度机制所打断的操作,基于Unsafe类实现,有一个旧值、预期值和新值
Doug Lea在CAS的基础上实现了一些原子类,其中就包括AtomicInteger
,还有其他很多原子类
他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。
java中基于Unsafe的类提供了CAS的操作方法,JVM会实现CAS汇编指令
CAS的缺点:CAS只能保证对一个变量的操作是原子的,无法实现对多行代码实现原子性
CAS的问题:
ABA问题:问题如下,可以引入版本号的方式,来解决ABA的问题。Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReference
AtomicStampedReference在CAS时,不但会判断原值,还会比较版本信息。
Lock锁
Lock锁是在JDK1.5由Doug Lea研发的,他的性能相比synchronized在JDK1.5的时期,性能好了很多多,但是在JDK1.6对synchronized优化之后,性能相差不大,但是如果涉及并发比较多时,推荐ReentrantLock锁,性能会更好。
ReentrantLock可以直接对比synchronized,在功能上来说,都是锁。
但是ReentrantLock的功能性相比synchronized更丰富。
ReentrantLock底层是基于AQS实现的,有一个基于CAS维护的state变量来实现锁的操作。
ThreadLocal
mybatis,JDBC类型框架底层的事务使用ThreadLocal存储connection连接对象
ThreadLocal实现原理:
- 每个Thread中都存储这一个成员变量,ThreadLocalMap
- ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
- ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
- 每个线程都有独立的ThreadLocalMap,
在基于ThreadLocal对象本身作为Key
,对value进行存取 ThreadLocal的key是一个弱引用
,特点是,在GC时,必须被回收。如果ThreadLocal对象是去引用后,如果key是强引用,会导致ThreadLocal对象无法被回收
ThreadLocal内存泄露问题:
- 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没被回收,就会导致内存泄露,内存中的value无法被回收。同时也无法被获取到。
- 只需要在内存使用完ThreadLocal对象之后,及时的调用remove方法,移除Entry即可
可见性
可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。
这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。
MESI协议
解决可见性的方式
volatile
volatile是一个关键字,用来修饰成员变量。
如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主内存操作
volatile的内存语义:
- volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
- volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量
其实加了volatile就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile修饰的属性,会在转为汇编之后后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:
- 将当前处理器缓存行的数据写回到主内存
- 这个写回的数据,在其他的CPU内核的缓存中,直接无效。
总结:volatile就是让CPU每次操作这个数据时,必须立即同步到主内存中,以及从主内存读取数据
synchronized
synchronized也是可以解决可见性问题的,synchronized的内存语义。
如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。
Lock
Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。
Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。
如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉取。
有序性
在Java中,.java文件中的内容会被编译,在执行前需要再次转为CPU可以识别的指令,CPU在执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满足一些要求),会对指令进行重排
。
指令乱序执行的原因,是为了尽可能的发挥CPU的性能。
Java中的程序是乱序执行的。
Java程序验证乱序执行效果:
as-if-serial
不论指令如何重排序
,需要保证单线程的执行结果是不变的
。
如果存在依赖关系
,那么也不能做指令重排
happens-before
指令重排序后的执行结果和指令重排序之前的结果是一致的话,那么该排序就是合法的
两个操作,第一个操作的结果对于第二个操作来说是可见的
具体规则:
1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。
4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
JMM只有在不出现上述8中情况时,才不会触发指令重排效果。
volatile
如果需要让程序对某一个属性的操作不出现指令重排,除了满足happens-before原则之外,还可以基于volatile修饰属性,从而对这个属性的操作,就不会出现指令重排的问题了。
volatile如何实现的禁止指令重排?
内存屏障概念。将内存屏障看成一条指令。
会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。
锁
锁的分类
可重入锁,不可重入锁
java中提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是可重入锁。
重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的。(内部锁和外部锁,锁的是同一把锁,锁的是同一个对象)
不可重入:当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到的,因为A锁被当前线程占用着,需要等待自己释放锁再获取锁。
乐观锁、悲观锁
java中提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是悲观锁。
Java中提供的CAS操作,就是乐观锁的一种实现。
悲观锁:获取不到锁资源时,会将当前线程挂起
(进入Blocked、waiting),线程挂起会涉及到用户态和内核态的切换,而且这种切换是比较消耗资源的。
- 用户态:JVM可以自行执行的指令,不需要借助操作系统执行
- 内核态:JVM不可用自行执行,需要操作系统才可执行
乐观锁:获取不到锁资源,可以再次调度CPU,重新尝试获取锁资源。
Atomic原子类,就是基于CAS乐观锁实现的。
公平锁、非公平锁
java中提供的synchronized是非公平锁
java中提供的ReentrantLock,ReentrantReadWriteLock既可以是公平锁,也可以是非公平锁,默认是非公平锁
公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,锁被A持有,同时线程B在排队。直接排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。
非公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,先尝试竞争一波
- 拿到锁资源:开心,插队成功。
- 没有拿到锁资源:依然要排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。(具体看AQS)
互斥锁、贡献锁
java中提供的synchronized是互斥锁
java中提供的ReentrantLock,ReentrantReadWriteLock既可以是互斥锁,也可以是共享锁
**互斥锁:**同一时间点,只有一个线程持有这把锁
共享锁::同一时间点,可以有多个线程持有这把锁
Synchronized
类锁、对象锁
synchronized的使用一般是同步方法和同步代码块
synchronized的锁是基于对象实现的
如果使用同步方法
- static:此时使用的是当前类 .class作为锁(类锁)
- 非static:此时使用的是当前对象作为锁(对象锁)
synchronized的优化
锁消除: 在synchronized修饰的代码中,如果不存在操作临界资源的情况
,会触发锁消除,你即便写了synchronized,他也不会触发。(锁的是当前对象,不存在竞争,不存在操作临界资源的情况)
public synchronized void method(){
// 没有操作临界资源
// 此时这个方法的synchronized你可以认为木有~~
}
锁膨胀(锁粗化): 如果在一个循环中,频繁的获取和释放锁资源,这样会带来很大的消耗,锁膨胀就是将锁的范围扩大,避免
public void method(){
for(int i = 0;i < 999999;i++){
synchronized(对象){
}
}
// 这是上面的代码会触发锁膨胀
synchronized(对象){
for(int i = 0;i < 999999;i++){
}
}
}
锁升级:
ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差。
synchronized就在JDK1.6做了锁升级的优化
-
无锁、匿名偏向 :当前对象没有作为锁存在。
-
偏向锁: 如果当前所资源,只有一个线程在频繁的获取个释放,那么这个线程 过来,只需要判断,当前指向的线程是否是当前线程。
- 如果是,直接拿走锁资源
- 如果不是,基于CAS方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁(偏向锁出现锁竞争的情况)
-
轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
-
重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程(用户态&内核态)
synchronized实现原理
MarkWord
MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、
synchronized的锁升级
锁默认情况下,开启了偏向锁延迟。
偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启
因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作
如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向
整个锁升级状态的转变:
ReentrantLock
ReentrantLock和synchronized的区别
核心区别:
ReentrantLock是个类,synchronized是个关键字,导入都是jvm层面互斥锁的实现方式
效率方面:
如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级的概念。而synchronized是存在锁升级的概念
底层实现区别:
ReentrantLock是基于AQS实现的,synchronized是基于ObjectMonitor
功能向的区别:
ReentrantLock既可以是公平锁,也可以是非公平锁,默认非公平锁
synchronized是非公平锁
AQS概述
AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。
首先AQS中提供了一个由volatile修饰,并且采用CAS方式修饰的int类型的state变量。
其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
AQS内部结构和属性
加锁流程源码剖析
1.线程A先执行CAS,将state从0修改成1,线程A就获取到了锁资源,去执行业务代码即可
2.线程B再执行CAS,发现state已经是1了,无法获取到锁资源
3.线程B需要去排队,将自己封装成Node对象
4.需要将当前线程B的Node放入到双向duilie中保存,排队
4.1 但是双向链表中,必须有一个伪结点作为头节点,并且放入到双向队列中
4.2 将线程B的Node挂在tail后面,并且将上一个节点的状态修改为-1,再挂起线程B
加锁源码分析
- lock方法
// 非公平锁
final void lock() {
// 上来就先基于CAS的方式,尝试将state从0改为1
if (compareAndSetState(0, 1))
// 获取锁资源成功,会将当前线程设置到exclusiveOwnerThread属性,代表是当前线程持有着锁资源
setExclusiveOwnerThread(Thread.currentThread());
else
// 执行acquire,尝试获取锁资源
acquire(1);
}
// 公平锁
final void lock() {
// 执行acquire,尝试获取锁资源
acquire(1);
}
public final void acquire(int arg) {
// tryAcquire:再次查看,当前线程是否可以尝试获取锁资源
if (!tryAcquire(arg) &&
// 没有拿到锁资源
// addWaiter(Node.EXCLUSIVE):将当前线程封装为Node节点,插入到AQS的双向链表的结尾
// acquireQueued:查看我是否是第一个排队的节点,如果是可以再次尝试获取锁资源,如果长时间拿不到,挂起线程
// 如果不是第一个排队的额节点,就尝试挂起线程即可
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 中断线程的操作
selfInterrupt();
}
/*
* tryAcquire方法竞争锁最资源的逻辑,分为公平锁和非公平锁
*/
// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取了state熟属性
int c = getState();
// 判断state当前是否为0,之前持有锁的线程释放了锁资源
if (c == 0) {
// 再次抢一波锁资源
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
// 拿锁成功返回true
return true;
}
}
// 不是0,有线程持有着锁资源,如果是,证明是锁重入操作
else if (current == getExclusiveOwnerThread()) {
// 将state + 1
int nextc = c + acquires;
if (nextc < 0) // 说明对重入次数+1后,超过了int正数的取值范围
// 01111111 11111111 11111111 11111111
// 10000000 00000000 00000000 00000000
// 说明重入的次数超过界限了。
throw new Error("Maximum lock count exceeded");
// 正常的将计算结果,复制给state
setState(nextc);
// 锁重入成功
return true;
}
// 返回false
return false;
}
// 公平锁实现
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// ....
int c = getState();
if (c == 0) {
// 查看AQS中是否有排队的Node
// 没人排队抢一手 。有人排队,如果我是第一个,也抢一手
if (!hasQueuedPredecessors() &&
// 抢一手~
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 锁重入~~~
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 查看是否有线程在AQS的双向队列中排队
// 返回false,代表没人排队
public final boolean hasQueuedPredecessors() {
// 头尾节点
Node t = tail;
Node h = head;
// s为头结点的next节点
Node s;
// 如果fail指针指向head节点,证明当前队列中没有节点,没有线程排队,直接去抢占锁资源
return h != t &&
// s节点不为null,并且s节点的线程为当前线程
(s == null || s.thread != Thread.currentThread());
}
/**
* addWaite方法,将没有拿到锁资源的线程扔到AQS队列中去排队
*/
// 没有拿到锁资源,过来排队, mode:代表互斥锁
private Node addWaiter(Node mode) {
// 将当前线程封装为Node,
Node node = new Node(Thread.currentThread(), mode);
// 拿到尾结点
Node pred = tail;
// 如果尾结点不为null
if (pred != null) {
// 当前节点的prev指向尾结点
node.prev = pred;
// 以CAS的方式,将当前线程设置为tail节点
if (compareAndSetTail(pred, node)) {
// 将之前的尾结点的next指向当前节点
pred.next = node;
return node;
}
}
// 如果CAS失败,以死循环的方式,保证当前线程的Node一定可以放到AQS队列的末尾
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
// 拿到尾结点
Node t = tail;
// 如果尾结点为空,AQS中一个节点都没有,构建一个伪节点,作为head和tail
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 比较熟悉了,以CAS的方式,在AQS中有节点后,插入到AQS队列的末尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
setHead 设置头结点
线程被唤醒强到锁后,该节点自动变为头结点
/**
* acquireQueued方法,判断当前线程是否还能再次尝试获取锁资源,如果不能再次获取锁资源,或者又没获取到,尝试将当前线程挂起
*/
// 当前没有拿到锁资源后,并且到AQS排队了之后触发的方法。 中断操作这里不用考虑
final boolean acquireQueued(final Node node, int arg) {
// 不考虑中断
// failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly)
boolean failed = true;
try {
boolean interrupted = false;
// 死循环…………
for (;;) {
// 拿到当前节点的前继节点
final Node p = node.predecessor();
// 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。
if (p == head && tryAcquire(arg)) {
// 获取锁资源成功
// 设置头结点为当前获取锁资源成功Node,并且取消thread信息
setHead(node);
// help GC
p.next = null;
// 获取锁失败标识为false
failed = false;
return interrupted;
}
// 没拿到锁资源……
// shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true,
// 如果不能,就返回false,继续下次循环
if (shouldParkAfterFailedAcquire(p, node) &&
// 这里基于Unsafe类的park方法,将当前线程挂起
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 在lock方法中,基本不会执行。
cancelAcquire(node);
}
}
// 获取锁资源成功后,先执行setHead
private void setHead(Node node) {
// 当前节点作为头结点 伪
head = node;
// 头结点不需要线程信息
node.thread = null;
node.prev = null;
}
// 当前Node没有拿到锁资源,或者没有资格竞争锁资源,看一下能否挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// -1,SIGNAL状态:代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点
// 1,CANCELLED状态:代表当前节点以及取消了
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 上一个节点为-1之后,当前节点才可以安心的挂起线程
return true;
if (ws > 0) {
// 如果当前节点的上一个节点是取消状态,我需要往前找到一个状态不为1的Node,作为他的next节点
// 找到状态不为1的节点后,设置一下next和prev
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 上一个节点的状态不是1或者-1,那就代表节点状态正常,将上一个节点的状态改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- tryLock方法
tryLock();
// tryLock方法,无论公平锁还有非公平锁。都会走非公平锁抢占锁资源的操作
// 就是拿到state的值, 如果是0,直接CAS浅尝一下
// state 不是0,那就看下是不是锁重入操作
// 如果没抢到,或者不是锁重入操作,告辞,返回false
public boolean tryLock() {
// 非公平锁的竞争锁操作
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
tryLock(time,unit);
// tryLock(time,unit)执行的方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {
// 线程的中断标记位,是不是从false,别改为了true,如果是,直接抛异常
if (Thread.interrupted())
throw new InterruptedException();
// tryAcquire分为公平和非公平锁两种执行方式,如果拿锁成功, 直接告辞,
return tryAcquire(arg) ||
// 如果拿锁失败,在这要等待指定时间
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果等待时间是0秒,直接告辞,拿锁失败
if (nanosTimeout <= 0L)
return false;
// 设置结束时间。
final long deadline = System.nanoTime() + nanosTimeout;
// 先扔到AQS队列
final Node node = addWaiter(Node.EXCLUSIVE);
// 拿锁失败,默认true
boolean failed = true;
try {
for (;;) {
// 如果在AQS中,当前node是head的next,直接抢锁
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 结算剩余的可用时间
nanosTimeout = deadline - System.nanoTime();
// 判断是否是否用尽的位置
if (nanosTimeout <= 0L)
return false;
// shouldParkAfterFailedAcquire:根据上一个节点来确定现在是否可以挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
// 避免剩余时间太少,如果剩余时间少就不用挂起线程
nanosTimeout > spinForTimeoutThreshold)
// 如果剩余时间足够,将线程挂起剩余时间
LockSupport.parkNanos(this, nanosTimeout);
// 如果线程醒了,查看是中断唤醒的,还是时间到了唤醒的。
if (Thread.interrupted())
// 是中断唤醒的!
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
取消节点分析:
1.线程置为null
2.往期找到有效节点作为当前节点的prev
3.将waitStatus设置为1,代表取消
4.脱离整个AQS队列:
4.1当前Node是tail
4.2当前节点是head的后续节点
4.3不是tail节点,也不是head的后续节点
// 取消在AQS中排队的Node
private void cancelAcquire(Node node) {
// 如果当前节点为null,直接忽略。
if (node == null)
return;
//1. 线程设置为null
node.thread = null;
//2. 往前跳过被取消的节点,找到一个有效节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//3. 拿到了上一个节点之前的next
Node predNext = pred.next;
//4. 当前节点状态设置为1,代表节点取消
node.waitStatus = Node.CANCELLED;
// 脱离AQS队列的操作
// 当前Node是尾结点,将tail从当前节点替换为上一个节点
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 到这,上面的操作CAS操作失败
int ws = pred.waitStatus;
// 不是head的后继节点
if (pred != head &&
// 拿到上一个节点的状态,只要上一个节点的状态不是取消状态,就改为-1
(ws == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
&& pred.thread != null) {
// 上面的判断都是为了避免后面节点无法被唤醒。
// 前继节点是有效节点,可以唤醒后面的节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 当前节点是head的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
- lockinterruptibly方法
// 这个是lockInterruptibly和tryLock(time,unit)唯一的区别
// lockInterruptibly,拿不到锁资源,就死等,等到锁资源释放后,被唤醒,或者是被中断唤醒
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 中断唤醒抛异常!
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
// 这个方法可以确认,当前挂起的线程,是被中断唤醒的,还是被正常唤醒的。
// 中断唤醒,返回true,如果是正常唤醒,返回false
return Thread.interrupted();
}
释放锁流程源码剖析
线程A持有当前锁,重入了一次,state = 2
线程B和C获取锁资源失败,在AQS中排队
线程A释放资源调用unlock方法,就是执行了tryRelease方法
首先判断是否是线程A持有锁资源,如果不是就抛异常。
如果线程A持有锁资源,对state - 1
-1成功后,会判断state是否为0,如果不是方法结束,
为0,证明锁资源释放干净了。
查看头节点的状态
如果不为0,后续链表中有挂起的线程,需要唤醒。
在唤醒线程时,需要先将当前的-1,改成0,找到有效节点唤醒。找到之后,唤醒线程
AQS中常见的问题
(head,fail可以理解成指针;节点中的线程唤醒后head节点设置为该节点;tail节点的状态为0,直到新的节点入队tail指针下移,置为-1)
AQS中为什么要有一个虚拟head节点
可以实现,但是操作起来会麻烦点,如果没有head节点,next节点就是不可用的,需要prev指针通过fail从后往前遍历
AQS可以没有head,设计之初指定head只是为了更方便的操作。
如果ReentrantLock中锁释放资源时,会考虑释放需要唤醒后继节点。如果头结点的状态不是-1.就不需要唤醒后继节点。唤醒后继节点时,需要找到head.next节点,如果head.next为null,或者是取消了,此时需要遍历整个双链;从后往前遍历,找到离head最近的Node。规避了一些不必要的唤醒操作。
如果不用虚拟节点(哨兵节点),当前节点挂起,当前节点的状态设置为-1.可行,AQS本身就是使用了哨兵节点做双向链表的一些操作
AQS中为什么使用双向链表
AQS的双向链表就为了更方便的操作Node节点。
在执行tryLock,lockInterruptibly方法时,如果在阻塞线程时,中断了线程,此时回执行cancelAcquire取消当前节点,不在AQS中排队。如果是单链,此时会导致哦取消节点,无法直接将当前节点的prev节点的next指针,指向当前节点的next节点,需要从头遍历。
ConditionObject
ConditionObject的介绍&应用
像synchronized提供了wait和notify的方法实现线程在持有锁时,可以实现挂起,已经唤醒的操作。
ReentrantLock也拥有这个功能。
ReentrantLock提供了await和signal方法去实现类似wait和notify的功能。
想执行await或者是signal就必须先持有lock锁的资源。
public static void main(String[] args) throws InterruptedException, IOException {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
System.out.println("子线程获取锁资源并await挂起线程");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
condition.await();
System.out.println("子线程挂起后被唤醒!持有锁资源");
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}).start();
Thread.sleep(100);
// =================main======================
lock.lock();
System.out.println("主线程等待5s拿到锁资源,子线程执行了await方法");
condition.signal();
System.out.println("主线程唤醒了await挂起的子线程");
lock.unlock();
}
condition相当于synchronized的waitset
aqs的双链阻塞队列相当于synchronized的cxq
Condition的await方法分析(前)
持有锁的线程在执行await方法后会做几个操作:
判断线程是否中断,加入到condition单链中,释放锁资源,LockSupport.park(this)挂起线程
- 判断线程是否中断,如果中断了,什么都不做。
- 没有中断,就讲当前线程封装为Node添加到Condition的单向链表中
- 一次性释放掉锁资源。
- 如果当前线程没有在AQS队列,就正常执行LockSupport.park(this)挂起线程。
// await方法的前置分析,只分析到线程挂起
public final void await() throws InterruptedException {
// 先判断线程的中断标记位是否是true
if (Thread.interrupted())
// 如果是true,就没必要执行后续操作挂起了。
throw new InterruptedException();
// 在线程挂起之前,先将当前线程封装为Node,并且添加到Condition队列中
Node node = addConditionWaiter();
// fullyRelease在释放锁资源,一次性将锁资源全部释放,并且保留重入的次数
int savedState = fullyRelease(node);
// 省略一行代码……
// 当前Node是否在AQS队列中?
// 执行fullyRelease方法后,线程就释放锁资源了,如果线程刚刚释放锁资源,其他线程就立即执行了signal方法,
// 此时当前线程就被放到了AQS的队列中,这样一来线程就不需要执行LockSupport.park(this);去挂起线程了
while (!isOnSyncQueue(node)) {
// 如果没有在AQS队列中,正常在Condition单向链表里,正常挂起线程。
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 线程挂起先,添加到Condition单向链表的业务~~
private Node addConditionWaiter() {
// 拿到尾节点。
Node t = lastWaiter;
// 如果尾节点有值,并且尾节点的状态不正常,不是-2,尾节点可能要拜拜了~
if (t != null && t.waitStatus != Node.CONDITION) {
// 如果尾节点已经取消了,需要干掉取消的尾节点~
unlinkCancelledWaiters();
// 重新获取lastWaiter
t = lastWaiter;
}
// 构建当前线程的Node,并且状态设置为-2
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 如果last节点为null。直接将当前节点设置为firstWaiter
if (t == null)
firstWaiter = node;
else
// 如果last节点不为null,说明有值,就排在lastWaiter的后面
t.nextWaiter = node;
// 把当前节点设置为最后一个节点
lastWaiter = node;
// 返回当前节点
return node;
}
// 干掉取消的尾节点。
private void unlinkCancelledWaiters() {
// 拿到头节点
Node t = firstWaiter;
// 声明一个节点,爱啥啥~~~
Node trail = null;
// 如果t不为null,就正常执行~~
while (t != null) {
// 拿到t的next节点
Node next = t.nextWaiter;
// 如果t的状态不为-2,说明有问题
if (t.waitStatus != Node.CONDITION) {
// t节点的next为null
t.nextWaiter = null;
// 如果trail为null,代表头结点状态就是1,
if (trail == null)
// 将头结点指向next节点
firstWaiter = next;
else
// 如果trail有值,说明不是头结点位置
trail.nextWaiter = next;
// 如果next为null,说明单向链表遍历到最后了,直接结束
if (next == null)
lastWaiter = trail;
}
// 如果t的状态是-2,一切正常
else {
// 临时存储t
trail = t;
}
// t指向之前的next
t = next;
}
}
// 一次性释放锁资源
final int fullyRelease(Node node) {
// 标记位,释放锁资源默认失败!
boolean failed = true;
try {
// 拿到现在state的值
int savedState = getState();
// 一次性释放干净全部锁资源
if (release(savedState)) {
// 释放锁资源失败了么? 没有!
failed = false;
// 返回对应的锁资源信息
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
// 如果释放锁资源失败,将节点状态设置为取消
node.waitStatus = Node.CANCELLED;
}
}
Condition的signal方法分析
确保signal的是持锁线程,脱离condition队列,重新加入到aqs队列中
分为了几个部分:
- 确保执行signal方法的是持有锁的线程
- 脱离Condition的队列
- 将Node状态从-2改为0
- 将Node添加到AQS队列
- 为了避免当前Node无法在AQS队列正常唤醒做了一些判断和操作
// 线程挂起后,可以基于signal唤醒~
public final void signal() {
// 在ReentrantLock中,如果执行signal的线程没有持有锁资源,直接扔异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 拿到排在Condition首位的Node
Node first = firstWaiter;
// 有Node在排队,才需要唤醒,如果没有,直接告辞~~
if (first != null)
doSignal(first);
}
// 开始唤醒Condition中的Node中的线程
private void doSignal(Node first) {
// 先一波do-while走你~~~
do {
// 获取到第二个节点,并且将第二个节点设置为firstWaiter
if ( (firstWaiter = first.nextWaiter) == null)
// 说明就一个节点在Condition队列中,那么直接将firstWaiter和lastWaiter置位null
lastWaiter = null;
// 如果还有nextWaiter节点,因为当前节点要被唤醒了,脱离整个Condition队列。将nextWaiter置位null
first.nextWaiter = null;
// 如果transferForSignal返回true,一切正常,退出while循环
} while (!transferForSignal(first) &&
// 如果后续节点还有,往后面继续唤醒,如果没有,退出while循环
(first = firstWaiter) != null);
}
// 准备开始唤醒在Condition中排队的Node
final boolean transferForSignal(Node node) {
// 将在Condition队列中的Node的状态从-2,改为0,代表要扔到AQS队列了。
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
// 如果失败了,说明在signal之前应当是线程被中断了,从而被唤醒了。
return false;
// 如果正常的将Node的状态从-2改为0,这是就要将Condition中的这个Node扔到AQS的队列。
// 将当前Node扔到AQS队列,返回的p是当前Node的prev
Node p = enq(node);
// 获取上一个Node的状态
int ws = p.waitStatus;
// 如果ws > 0 ,说明这个Node已经被取消了。
// 如果ws状态不是取消,将prev节点的状态改为-1,。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果prev节点已经取消了,可能会导致当前节点永远无法被唤醒。立即唤醒当前节点,基于acquireQueued方法,
// 让当前节点找到一个正常的prev节点,并挂起线程
// 如果prev节点正常,但是CAS修改prev节点失败了。证明prev节点因为并发原因导致状态改变。还是为了避免当前
// 节点无法被正常唤醒,提前唤醒当前线程,基于acquireQueued方法,让当前节点找到一个正常的prev节点,并挂起线程
LockSupport.unpark(node.thread);
// 返回true
return true;
}
Condition的await方法分析(后)
分为了几个部分:
- 唤醒之后,要先确认是中断唤醒还是signal唤醒,还是signal唤醒后被中断
- 确保当前线程的Node已经在AQS队列中
- 执行acquireQueued方法,等待锁资源。
- 在获取锁资源后,要确认是否在获取锁资源的阶段被中断过,如果被中断过,并且不是THROW_IE,那就确保interruptMode是REINTERRUPT。
- 确认当前Node已经不在Condition队列中了
- 最终根据interruptMode来决定具体做的事情
- 0:嘛也不做。
- THROW_IE:抛出异常
- REINTERRUPT:执行线程的interrupt方法
// 现在分析await方法的后半部分
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
// 中断模式~
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 如果线程执行到这,说明现在被唤醒了。
// 线程可以被signal唤醒。(如果是signal唤醒,可以确认线程已经在AQS队列中)
// 线程可以被interrupt唤醒,线程被唤醒后,没有在AQS队列中。
// 如果线程先被signal唤醒,然后线程中断了。。。。(做一些额外处理)
// checkInterruptWhileWaiting可以确认当前中如何唤醒的。
// 返回的值,有三种
// 0:正常signal唤醒,没别的事(不知道Node是否在AQS队列)
// THROW_IE(-1):中断唤醒,并且可以确保在AQS队列
// REINTERRUPT(1):signal唤醒,但是线程被中断了,并且可以确保在AQS队列
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// Node一定在AQS队列
// 执行acquireQueued,尝试在ReentrantLock中获取锁资源。
// acquireQueued方法返回true:代表线程在AQS队列中挂起时,被中断过
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
// 如果线程在AQS队列排队时,被中断了,并且不是THROW_IE状态,确保线程的interruptMode是REINTERRUPT
// REINTERRUPT:await不是中断唤醒,但是后续被中断过!!!
interruptMode = REINTERRUPT;
// 如果当前Node还在condition的单向链表中,脱离Condition的单向链表
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 如果interruptMode是0,说明线程在signal后以及持有锁的过程中,没被中断过,什么事都不做!
if (interruptMode != 0)
// 如果不是0~
reportInterruptAfterWait(interruptMode);
}
// 判断当前线程被唤醒的模式,确认interruptMode的值。
private int checkInterruptWhileWaiting(Node node) {
// 判断线程是否中断了。
return Thread.interrupted() ?
// THROW_IE:代表线程是被interrupt唤醒的,需要向上排除异常
// REINTERRUPT:代表线程是signal唤醒的,但是在唤醒之后,被中断了。
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
// 线程是正常的被signal唤醒,并且线程没有中断过。
0;
}
// 判断线程到底是中断唤醒的,还是signal唤醒的!
final boolean transferAfterCancelledWait(Node node) {
// 基于CAS将Node的状态从-2改为0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 说明是中断唤醒的线程。因为CAS成功了。
// 将Node添加到AQS队列中~(如果是中断唤醒的,当前线程同时存在Condition的单向链表以及AQS的队列中)
enq(node);
// 返回true
return true;
}
// 判断当前的Node是否在AQS队列(signal唤醒的,但是可能线程还没放到AQS队列)
// 等到signal方法将线程的Node扔到AQS队列后,再做后续操作
while (!isOnSyncQueue(node))
// 如果没在AQS队列上,那就线程让步,稍等一会,Node放到AQS队列再处理(看CPU)
Thread.yield();
// signal唤醒的,返回false
return false;
}
// 确认Node是否在AQS队列上
final boolean isOnSyncQueue(Node node) {
// 如果线程状态为-2,肯定没在AQS队列
// 如果prev节点的值为null,肯定没在AQS队列
if (node.waitStatus == Node.CONDITION || node.prev == null)
// 返回false
return false;
// 如果节点的next不为null。说明已经在AQS队列上。、
if (node.next != null)
// 确定AQS队列上有!
return true;
// 如果上述判断都没有确认节点在AQS队列上,在AQS队列中寻找一波
return findNodeFromTail(node);
}
// 在AQS队列中找当前节点
private boolean findNodeFromTail(Node node) {
// 拿到尾节点
Node t = tail;
for (;;) {
// tail是否是当前节点,如果是,说明在AQS队列
if (t == node)
// 可以跳出while循环
return true;
// 如果节点为null,AQS队列中没有当前节点
if (t == null)
// 进入while,让步一手
return false;
// t向前引用
t = t.prev;
}
}
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
// 如果是中断唤醒的await,直接抛出异常!
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 如果是REINTERRUPT,signal后被中断过
else if (interruptMode == REINTERRUPT)
// 确认线程的中断标记位是true
// Thread.currentThread().interrupt();
selfInterrupt();
}
ReentrantReadWriteLock
读写锁的实现原理
ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,如果没有拿到,依然去AQS队列中排队。
读锁操作:基于state的高16位进行操作。
写锁操作:基于state的低16位进行操作。
ReentrantReadWriteLock依然是可重入锁。
写锁重入:读写锁中的写锁的重入方式,基本和ReentrantLock一致,没有什么区别,依然是对state进行+1操作即可,只要确认持有锁资源的线程,是当前写锁线程即可。只不过之前ReentrantLock的重入次数是state的正数取值范围,但是读写锁中写锁范围就变小了。
读锁重入:因为读锁是共享锁。读锁在获取锁资源操作时,是要对state的高16位进行 + 1操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁资源。这样一来,多个读操作在持有读锁时,无法确认每个线程读锁重入的次数。为了去记录读锁重入的次数,每个读操作的线程,都会有一个ThreadLocal记录锁重入的次数。
写锁的饥饿问题:读锁是共享锁,当有线程持有读锁资源时,再来一个线程想要获取读锁,直接对state修改即可。在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求锁资源,如果可以绕过写锁,直接拿资源,会造成写锁长时间无法获取到写锁资源。
读锁在拿到锁资源后,如果再有读线程需要获取读锁资源,需要去AQS队列排队。如果队列的前面需要写锁资源的线程,那么后续读线程是无法拿到锁资源的。持有读锁的线程,只会让写锁线程之前的读线程拿到锁资源
写锁
加锁流程
写锁加锁流程:
1.写线程来竞争写锁资源
2.直接通过tryAcquire来获取写锁资源(公平锁&非公平锁)
3.获取state值,并且拿到低16位的值。
4.如果state值不为0,判断是否锁重入操作(判断当前持有写锁的线程是否是当前线程)
5.如果state值为0
是公平锁,查看队列是否有排队的,有的话入队阻塞,反之抢锁资源
是非公平锁,直接抢锁资源
6.如果加锁成功执行代码,反之入队阻塞
写锁释放锁流程:
释放的流程和ReentrantLock一致,只是在判断释放是否干净时,判断低16位的值
读锁
读锁加锁流程:
1.读操作线程竞争读锁资源
2.拿到state
3.判断state的低16位是否为0(不为0代表着写锁占用资源)如果占用锁的线程不是当前线程,则拿到state的高16位的值,接着入队阻塞
公平锁:如果队列为空抢锁
非公平锁:抢锁,没抢到排队
4.cas对state的高16位 +1
读锁的释放锁流程:
1、处理重入以及state的值
2、唤醒后续排队的Node
阻塞队列
生产者
生产者消费者是设计模式的一种。让生产者和消费者基于一个容器来解决强耦合问题。
生产者 消费者彼此之间不会直接通讯的,而是通过一个容器(队列)进行通讯。
所以生产者生产完数据后扔到容器中,不通用等待消费者来处理。
消费者不需要去找生产者要数据,直接从容器中获取即可。
而这种容器最常用的结构就是队列。
JUC阻塞队列的存取方法
生产者存储方法
add(E) // 添加数据到队列,如果队列满了,无法存储,抛出异常
offer(E)// 添加数据到队列,如果队列满了,返回false
offer(E,timeout,unit) // 添加数据到队列,如果队列满了,阻塞timeout时间,如果阻塞一段时间,依然没添加进入,返回false
put(E) // 添加数据到队列,如果队列满了,挂起线程,等到队列中有位置,再扔数据进去,死等!
消费者取数据方法
remove() // 从队列中移除数据,如果队列为空,抛出异常
poll() // 从队列中移除数据,如果队列为空,返回null
poll(timeout,unit)// 从队列中移除数据,如果队列为空,挂起线程timeout时间,等生产者扔数据,再获取
take() // 从队列中移除数据,如果队列为空,线程挂起,一直等到生产者扔数据,再获取
ArrayBlockingQueue
ArrayBlockingQueue在初始化的时候,必须指定当前队列的长度。
因为ArrayBlockingQueue是基于数组实现的队列结构,数组长度不可变,必须提前设置数组长度信息。
public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
// 必须设置队列的长度
ArrayBlockingQueue queue = new ArrayBlockingQueue(4);
// 生产者扔数据
queue.add("1");
queue.offer("2");
queue.offer("3",2,TimeUnit.SECONDS);
queue.put("2");
// 消费者取数据
System.out.println(queue.remove());
System.out.println(queue.poll());
System.out.println(queue.poll(2,TimeUnit.SECONDS));
System.out.println(queue.take());
}
LinkedBlockingQueue
LinkedBlockingQueue的底层实现是通过链表,最大长度是int的最大值2的32次方减1
PriorityBlockingQueue
首先PriorityBlockingQueue是一个优先级队列,不满足先进先出的概念。
会将查询的数据进行排序,排序的方式就是基于插入数据值的本身。
如果是自定义对象必须要实现comparable接口
才可以添加到优先级队列
排序的方式是基于二叉堆
实现的。底层采用的数据结构是二叉堆
二叉堆
优先级队列priorityblockqueue基于二叉堆实现的
什么是二叉堆?
- 二叉堆就是一颗完整的二叉树
- 任意一个节点大于父节点或小于父节点
- 基于同步的方式,可以定义出小顶堆和大顶堆
二叉堆完全基于数组实现
DelayQueue
DelayQueue就是一个延迟队列
,生产者写入一个消息,这个消息还有直接被消费的延迟时间。需要让消息具有延迟的特性。
DelayQueue也是基于二叉堆结构实现的,甚至本事就是基于PriorityQueue实现的功能。二叉堆结构每次获取的是栈顶的数据,需要让DelayQueue中的数据,在比较时,跟根据延迟时间做比较,剩余时间最短的要放在栈顶。
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {
// 发现DelayQueue中的元素,需要继承Delayed接口。
}
// ==========================================
// 接口继承了Comparable,这样就具备了比较的能力。
public interface Delayed extends Comparable<Delayed> {
// 抽象方法,就是咱们需要设置的延迟时间
long getDelay(TimeUnit unit);
// Comparable接口提供的:public int compareTo(T o);
}
案例实现:
public class Task implements Delayed {
/** 任务的名称 */
private String name;
/** 什么时间点执行 */
private Long time;
/**
*
* @param name
* @param delay 单位毫秒。
*/
public Task(String name, Long delay) {
// 任务名称
this.name = name;
this.time = System.currentTimeMillis() + delay;
}
/**
* 设置任务什么时候可以出延迟队列
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
// 单位是毫秒,视频里写错了,写成了纳秒,
return unit.convert(time - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}
/**
* 两个任务在插入到延迟队列时的比较方式
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
return (int) (this.time - ((Task)o).getTime());
}
}
public static void main(String[] args) throws InterruptedException {
// 声明元素
Task task1 = new Task("A",1000L);
Task task2 = new Task("B",5000L);
Task task3 = new Task("C",3000L);
Task task4 = new Task("D",2000L);
// 声明阻塞队列
DelayQueue<Task> queue = new DelayQueue<>();
// 将元素添加到延迟队列中
queue.put(task1);
queue.put(task2);
queue.put(task3);
queue.put(task4);
// 获取元素
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
// A,D,C,B
}
SynchronousQueue
SynchronousQueue这个阻塞队列和其他的阻塞队列有很大的区别
在咱们的概念中,队列肯定是要存储数据的,但是SynchronousQueue不会存储数据的
SynchronousQueue队列中,他不存储数据,存储生产者或者是消费者
当存储一个生产者到SynchronousQueue队列中之后,生产者会阻塞(看你调用的方法)
生产者最终会有几种结果:
- 如果在阻塞期间有消费者来匹配,生产者就会将绑定的消息交给消费者
- 生产者得等阻塞结果,或者不允许阻塞,那么就直接失败
- 生产者在阻塞期间,如果线程中断,直接告辞。
同理,消费者和生产者的效果是一样。
生产者和消费者的数据是直接传递的,不会经过SynchronousQueue。
SynchronousQueue是不会存储数据的。
public static void main(String[] args) throws InterruptedException {
// 因为当前队列不存在数据,没有长度的概念。
SynchronousQueue queue = new SynchronousQueue();
String msg = "消息!";
/*new Thread(() -> {
// b = false:代表没有消费者来拿
boolean b = false;
try {
b = queue.offer(msg,1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(b);
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println(queue.poll());
}).start();*/
new Thread(() -> {
try {
System.out.println(queue.poll(1, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(100);
new Thread(() -> {
queue.offer(msg);
}).start();
}
线程池
为什么要使用线程池
在开发中,为了提升效率的操作,我们需要将一些业务采用多线程的方式去执行。
比如有一个比较大的任务,可以将任务分成几块,分别交给几个线程去执行,最终做一个汇总就可以了。
比如做业务操作时,需要发送短信或者是发送邮件,这种操作也可以基于异步的方式完成,这种异步的方式,其实就是再构建一个线程去执行。
但是,如果每次异步操作或者多线程操作都需要新创建一个线程,使用完毕后,线程再被销毁,这样的话,对系统造成一些额外的开销。在处理过程中到底由多线程处理了多少个任务,以及每个线程的开销无法统计和管理。
所以咱们需要一个线程池机制来管理这些内容。线程池的概念和连接池类似,都是在一个Java的集合中存储大量的线程对象,每次需要执行异步操作或者多线程操作时,不需要重新创建线程,直接从集合中拿到线程对象直接执行方法就可以了。
JDK中就提供了线程池的类。
在线程池构建初期,可以将任务提交到线程池中。会根据一定的机制来异步执行这个任务。
- 可能任务直接被执行
- 任务可以暂时被存储起来了。等到有空闲线程再来处理。
- 任务也可能被拒绝,无法被执行。
JDK提供的线程池中记录了每个线程处理了多少个任务,以及整个线程池处理了多少个任务。同时还可以针对任务执行前后做一些勾子函数的实现。可以在任务执行前后做一些日志信息,这样可以多记录信息方便后面统计线程池执行任务时的一些内容参数等等……
如果是局部变量的线程池,用完要shutdown,处理的方式还是执行execute或者submit方法。
submit是异步执行的
JDK自带的构建线程池的方式
newFixedThreadPool
这个线程池的特别是线程数是固定的。
在Executors中第一个方法就是构建newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
构建时,需要给newFixedThreadPool方法提供一个nThreads的属性,而这个属性其实就是当前线程池中线程的个数。当前线程池的本质其实就是使用ThreadPoolExecutor。
构建好当前线程池后,线程个数已经固定好**(线程是懒加载,在构建之初,线程并没有构建出来,而是随着人任务的提交才会将线程在线程池中国构建出来)**。如果线程没构建,线程会待着任务执行被创建和执行。如果线程都已经构建好了,此时任务会被放到LinkedBlockingQueue无界队列中存放,等待线程从LinkedBlockingQueue中去take出任务,然后执行。
ExecutorService threadPool = Executors.newFixedThreadPool(3);
newSingleThreadExecutor
单例线程池,线程池中只有一个工作线程在处理任务
如果业务涉及到顺序消费
,可以采用newSingleThreadExecutor
ExecutorService threadPool = Executors.newSingleThreadExecutor();
单例线程池使用完毕后,不执行shutdown的后果:
如果是局部变量仅限当前线程池使用的线程池,在使用完毕之后要记得执行shutdown,避免线程无法结束
如果是全局的线程池,很多业务都会到,使用完毕后不要shutdown,因为其他业务也要执行当前线程池
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
当第一次提交任务到线程池时,会直接构建一个工作线程
这个工作线程带执行完人后,60秒没有任务可以执行后,会结束
如果在等待60秒期间有任务进来,他会再次拿到这个任务去执行
如果后续提升任务时,没有线程是空闲的,那么就构建工作线程去执行。
最大的一个特点,任务只要提交到当前的newCachedThreadPool中,就必然有工作线程可以处理
newScheduleThreadPool
当前线程池是一个定时任务的线程池,而这个线程池就是可以以一定周期去执行一个任务,或者是延迟多久执行一个任务一次
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
使用
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
// 正常执行
// pool.execute(() -> {
// System.out.println(Thread.currentThread().getName() + ":1");
// });
// 延迟执行,执行当前任务延迟5s后再执行
// pool.schedule(() -> {
// System.out.println(Thread.currentThread().getName() + ":2");
// },5,TimeUnit.SECONDS);
// 周期执行,当前任务第一次延迟5s执行,然后没3s执行一次
// 这个方法在计算下次执行时间时,是从任务刚刚开始时就计算。
// pool.scheduleAtFixedRate(() -> {
// try {
// Thread.sleep(3000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(System.currentTimeMillis() + ":3");
// },2,1,TimeUnit.SECONDS);
// 周期执行,当前任务第一次延迟5s执行,然后没3s执行一次
// 这个方法在计算下次执行时间时,会等待任务结束后,再计算时间
pool.scheduleWithFixedDelay(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + ":3");
},2,1,TimeUnit.SECONDS);
自定义线程池
首先ThreadPoolExecutor中,一共提供了7个参数,每个参数都是非常核心的属性,在线程池去执行任务时,每个参数都有决定性的作用。
但是如果直接采用JDK提供的方式去构建,可以设置的核心参数最多就两个,这样就会导致对线程池的控制粒度很粗。所以在阿里规范中也推荐自己去自定义线程池。手动的去new ThreadPoolExecutor设置他的一些核心属性。
自定义构建线程池,可以细粒度的控制线程池,去管理内存的属性,并且针对一些参数的设置可能更好的在后期排查问题。
ThreadPoolExecutor提供的七个核心参数
public ThreadPoolExecutor(
int corePoolSize, // 核心工作线程(当前任务执行结束后,不会被销毁)
int maximumPoolSize, // 最大工作线程(代表当前线程池中,一共可以有多少个工作线程)
long keepAliveTime, // 非核心工作线程在阻塞队列位置等待的时间
TimeUnit unit, // 非核心工作线程在阻塞队列位置等待时间的单位
BlockingQueue<Runnable> workQueue, // 任务在没有核心工作线程处理时,任务先扔到阻塞队列中
ThreadFactory threadFactory, // 构建线程的线程工作,可以设置thread的一些信息
RejectedExecutionHandler handler) { // 当线程池无法处理投递过来的任务时,执行当前的拒绝策略
// 初始化线程池的操作
}
JDK提供的几种拒绝策略:
-
AbortPolicy:当前拒绝策略会在无法处理任务时,直接抛出一个异常
-
CallerRunsPolicy:当前拒绝策略会在线程池无法处理任务时,将任务交给调用者处理
-
DiscardPolicy:当前拒绝策略会在线程池无法处理任务时,直接将任务丢弃掉
-
DiscardOldestPolicy:当前拒绝策略会在线程池无法处理任务时,将队列中最早的任务丢弃掉,将当前任务再次尝试交给线程池处理
-
自定义Policy:根据自己的业务,可以将任务扔到数据库,也可以做其他操作。
private static class MyRejectedExecution implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("根据自己的业务情况,决定编写的代码!");
}
}
线程池状态
// 111:只有runnable状态可以处理任务
private static final int RUNNING = -1 << COUNT_BITS;
// 000:不会接受新任务,正在处理的任务正常运行,阻塞队列中的任务也会执行
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 001:不会接受新任务,正在运行的任务会中断,也不会处理阻塞队列里的任务
private static final int STOP = 1 << COUNT_BITS;
// 010:代表线程池马上关闭的过渡状态
private static final int TIDYING = 2 << COUNT_BITS;
//011:代表TERMINATED状态,这个状态是TIDYING状态转换过来的,转换过来只需要执行一个terminated方法。
private static final int TERMINATED = 3 << COUNT_BITS;
JUC并发集合
ConcurrentHashMap
concurrentHashMap是线程安全的HashMap
concurrentHashMap在jdk1.8中是以cas+synchronize的方式实现线程安全的,锁的是数组下标
concurrentHashMap在jdk1.7中是以segment实现线程安全的
HashMap可以允许k或v为null,concurrenthashmap中不允许k或v为空
存储操作
1.算出hashcode
2.散列算法找出数组下标
JUC并发工具
CountdownLatch
CountDownLatch就是JUC包下的一个工具,整个工具最核心的功能就是计数器。
如果有三个业务需要并行处理,并且需要知道三个业务全部都处理完毕了。
需要一个并发安全的计数器来操作。
CountDownLatch就可以实现。
给CountDownLatch设置一个数值。可以设置3。
每个业务处理完毕之后,执行一次countDown方法,指定的3每次在执行countDown方法时,对3进行-1。
主线程可以在业务处理时,执行await,主线程会阻塞等待任务处理完毕。
当设置的3基于countDown方法减为0之后,主线程就会被唤醒,继续处理后续业务。
当咱们的业务中,出现2个以上允许并行处理的任务,并且需要在任务都处理完毕后,再做其他处理时,可以采用CountDownLatch去实现这个功能。
CountDownLatch应用
模拟有三个任务需要并行处理,在三个任务全部处理完毕后,再执行后续操作
CountDownLatch中,执行countDown方法,代表一个任务结束,对计数器 - 1
执行await方法,代表等待计数器变为0时,再继续执行
执行await(time,unit)方法,代表等待time时长,如果计数器不为0,返回false,如果在等待期间,计数器为0,方法就返回true
static ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);
static CountDownLatch countDownLatch = new CountDownLatch(3);
public static void main(String[] args) throws InterruptedException {
System.out.println("主业务开始执行");
sleep(1000);
executor.execute(CompanyTest::a);
executor.execute(CompanyTest::b);
executor.execute(CompanyTest::c);
System.out.println("三个任务并行执行,主业务线程等待");
// 死等任务结束
// countDownLatch.await();
// 如果在规定时间内,任务没有结束,返回false
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
System.out.println("三个任务处理完毕,主业务线程继续执行");
}else{
System.out.println("三个任务没有全部处理完毕,执行其他的操作");
}
}
private static void a() {
System.out.println("A任务开始");
sleep(1000);
System.out.println("A任务结束");
countDownLatch.countDown();
}
private static void b() {
System.out.println("B任务开始");
sleep(1500);
System.out.println("B任务结束");
countDownLatch.countDown();
}
private static void c() {
System.out.println("C任务开始");
sleep(2000);
System.out.println("C任务结束");
countDownLatch.countDown();
}
private static void sleep(long timeout){
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
CyclicBarrier
循环屏障
Barrier屏障:让一个或多个线程达到一个屏障点,会被阻塞。屏障点会有一个数值,当达到一个线程阻塞在屏障点时,就会对屏障点的数值进行-1操作,当屏障点数值减为0时,屏障就会打开,唤醒所有阻塞在屏障点的线程。在释放屏障点之后,可以先执行一个任务,再让所有阻塞被唤醒的线程继续之后后续任务。
Cyclic循环:所有线程被释放后,屏障点的数值可以再次被重置。
CyclicBarrier一般被称为栅栏
。
CyclicBarrier是一种同步机制,允许一组线程互相等待。现成的达到屏障点其实是基于await方法在屏障点阻塞。
CyclicBarrier并没有基于AQS实现,他是基于ReentrantLock锁的机制去实现了对屏障点–,以及线程挂起的操作。(CountDownLatch本身是基于AQS,对state进行release操作后,可以-1)
CyclicBarrier没来一个线程执行await,都会对屏障数值进行-1操作,每次-1后,立即查看数值是否为0,如果为0,直接唤醒所有的互相等待线程。
CyclicBarrier对比CountDownLatch区别
- 底层实现不同。CyclicBarrier基于ReentrantLock做的。CountDownLatch直接基于AQS做的。
- 应用场景不同。CountDownLatch的计数器只能使用一次。
而CyclicBarrier在计数器达到0之后,可以重置计数器
(可以复用)。CyclicBarrier可以实现相比CountDownLatch更复杂的业务,执行业务时出现了错误,可以重置CyclicBarrier计数器,再次执行一次
。 - CyclicBarrier还提供了很多其他的功能:
- 可以获取到阻塞的现成有多少
- 在线程互相等待时,如果有等待的线程中断,可以抛出异常,避免无限等待的问题。
- CountDownLatch一般是让主线程等待,让子线程对计数器–。CyclicBarrier更多的让子线程也一起计数和等待,等待的线程达到数值后,再统一唤醒
CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再一次执行。
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(3,() -> {
System.out.println("等到各位大佬都到位之后,分发护照和签证等内容!");
});
new Thread(() -> {
System.out.println("Tom到位!!!");
try {
barrier.await();
} catch (Exception e) {
System.out.println("悲剧,人没到齐!");
return;
}
System.out.println("Tom出发!!!");
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("Jack到位!!!");
try {
barrier.await();
} catch (Exception e) {
System.out.println("悲剧,人没到齐!");
return;
}
System.out.println("Jack出发!!!");
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("Rose到位!!!");
try {
barrier.await();
} catch (Exception e) {
System.out.println("悲剧,人没到齐!");
return;
}
System.out.println("Rose出发!!!");
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("张三到位!!!");
try {
barrier.await();
} catch (Exception e) {
System.out.println("悲剧,人没到齐!");
return;
}
System.out.println("张三出发!!!");
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("李四到位!!!");
try {
barrier.await();
} catch (Exception e) {
System.out.println("悲剧,人没到齐!");
return;
}
System.out.println("李四出发!!!");
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("王五到位!!!");
try {
barrier.await();
} catch (Exception e) {
System.out.println("悲剧,人没到齐!");
return;
}
System.out.println("王五出发!!!");
}).start();
Thread.sleep(100);
}
/*
Tom到位!!!
Jack到位!!!
Rose到位!!!
等到各位大佬都到位之后,分发护照和签证等内容!
Rose出发!!!
Tom出发!!!
Jack出发!!!
张三到位!!!
李四到位!!!
王五到位!!!
等到各位大佬都到位之后,分发护照和签证等内容!
王五出发!!!
张三出发!!!
李四出发!!!
*/
Semaphone
sync,ReentrantLock是互斥锁,保证一个资源同一时间只允许被一个线程访问
Semaphore(信号量)保证1个或多个资源可以被指定数量的线程同时访问
底层实现是基于AQS去做的。
Semaphore底层也是基于AQS的state属性做一个计数器的维护。state的值就代表当前共享资源的个数。如果一个线程需要获取的1或多个资源,直接查看state的标识的资源个数是否足够,如果足够的,直接对state - 1拿到当前资源。如果资源不够,当前线程就需要挂起等待。知道持有资源的线程释放资源后,会归还给Semaphore中的state属性,挂起的线程就可以被唤醒。
Semaphore也分为公平和非公平的概念。
使用场景:连接池对象就可以基础信号量去实现管理。在一些流量控制上,也可以采用信号量去实现。再比如去迪士尼或者是环球影城,每天接受的人流量是固定的,指定一个具体的人流量,可能接受10000人,每有一个人购票后,就对信号量进行–操作,如果信号量已经达到了0,或者是资源不足,此时就不能买票。
应用
其实Semaphore整体就是对构建Semaphore时,指定的资源数的获取和释放操作
获取资源方式:
- acquire():获取一个资源,没有资源就挂起等待,如果中断,直接抛异常
- acquire(int):获取指定个数资源,资源不够,或者没有资源就挂起等待,如果中断,直接抛异常
- tryAcquire():获取一个资源,没有资源返回false,有资源返回true
- tryAcquire(int):获取指定个数资源,没有资源返回false,有资源返回true
- tryAcquire(time,unit):获取一个资源,如果没有资源,等待time.unit,如果还没有,就返回false
- tryAcquire(int,time,unit):获取指定个数资源,如果没有资源,等待time.unit,如果还没有,就返回false
- acquireUninterruptibly():获取一个资源,没有资源就挂起等待,中断线程不结束,继续等
- acquireUninterruptibly(int):获取指定个数资源,没有资源就挂起等待,中断线程不结束,继续等
归还资源方式:
- release():归还一个资源
- release(int):归还指定个数资源
public static void main(String[] args) throws InterruptedException {
// 今天环球影城还有人个人流量
Semaphore semaphore = new Semaphore(10);
new Thread(() -> {
System.out.println("一家三口要去~~");
try {
semaphore.acquire(3);
System.out.println("一家三口进去了~~~");
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("一家三口走了~~~");
semaphore.release(3);
}
}).start();
for (int i = 0; i < 7; i++) {
int j = i;
new Thread(() -> {
System.out.println(j + "大哥来了。");
try {
semaphore.acquire();
System.out.println(j + "大哥进去了~~~");
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(j + "大哥走了~~~");
semaphore.release();
}
}).start();
}
Thread.sleep(10);
System.out.println("main大哥来了。");
if (semaphore.tryAcquire()) {
System.out.println("main大哥进来了。");
}else{
System.out.println("资源不够,main大哥进来了。");
}
Thread.sleep(10000);
System.out.println("main大哥又来了。");
if (semaphore.tryAcquire()) {
System.out.println("main大哥进来了。");
semaphore.release();
}else{
System.out.println("资源不够,main大哥进来了。");
}
}
异步编程
FutureTask
FutureTask是一个可以取消异步任务的类。FutureTask对Future做的一个基本实现。可以调用方法区开始和取消一个任务。
一般是配合Callable去使用。
异步任务启动之后,可以获取一个绑定当前异步任务的FutureTask。
可以基于FutureTask的方法去取消任务,查看任务是否结果,以及获取任务的返回结果。
FutureTask内部的整体结构中,实现了RunnableFuture的接口,这个接口又继承了Runnable, Future这个两个接口。所以FutureTask也可以作为任务直接交给线程池去处理。
使用
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask(()->{
System.out.println("任务开始执行……");
Thread.sleep(2000);
System.out.println("任务执行完毕……");
return "OK";
});
// futureTask提供了run方法,一般不会自己去调用run方法,让线程池去执行任务,由线程池去执行run方法
// run方法在执行时,是有任务状态的。任务已经执行了,再次调用run方法无效的。
// 如果希望任务可以反复被执行,需要去调用runAndReset方法
futureTask.run();
// ExecutorService service = Executors.newFixedThreadPool(10);
// service.submit(futureTask);
// service.shutdown();
Thread.sleep(2000);
// 对任务状态的控制
System.out.println("futureTask.isDone() = " + futureTask.isDone());
// 对返回结果的获取,类似阻塞队列的take方法,死等结果
String str = futureTask.get();
// 对返回结果的获取,类似阻塞队列的poll方法
// 如果在指定时间内,没有拿到方法的返回结果,直接扔TimeoutException
String str2 = futureTask.get(3000, TimeUnit.MILLISECONDS);
System.out.println("str = " + str);
System.out.println("str2 = " + str2);
}