并行和并发的区别:
- 从操作系统角度看,线程是CPU分配的最小单位。比较重要的是:并发通常是需要共享资源,通常会发生竞态条件或死锁
- 并行:同一时刻多个线程都在执行,需要多个cpu核心。超线程还是会共享寄存器,缓存这些
- 并发:同一时刻,只有一个线程在执行,一段时间内有多个线程在执行,依赖于cpu的线程上下文切换,如果是不同进程则是进程上下文切换,线程为线程上下文切换。
什么是进程?什么是线程?什么时候用进程?什么时候用线程?为什么多进程?为什么多线程?
- 进程就是在电脑上的一个应用,是系统进行资源分配和调度的基本单位,比如:jvm就是一个进程,拥有自己独立的地址和资源,运行时数据区;
- 线程是CPU调度和分配资源的基本单位,就像是一个java进程里面的main线程,同时也可以有很多处理其他任务的线程,线程之间共享进程间的资源,
- 多进程适用于需要更严格的隔离和更高的稳定性的场景,而多线程适用于需要更轻量级的并发处理和更高的资源利用率的场景(频繁切换、频繁销毁删除)。进程创建需要分配地址而线程只需要创建一个栈和程序计数器。
- 多进程多线程可以发挥多核处理器性能,多线程可以将耗时的放在后台执行,以确保流畅性。
讲讲协程?
- 对于kotlin中的协程,当线程执行类似于delay的挂起函数的时候,该函数会立即执行并挂起线程的执行,保存协程的状态(状态机),协程调度器选择其它准备好的协程,将当前线程让出给其它协程执行,是非阻塞的。
- 虚拟线程主要是减少了线程的创建和上下文的切换开销,类似于挂起,但是从开发者视角看和传统线程模型无异,就称为阻塞。
重点!!什么是阻塞什么是挂起?
阻塞:当线程在等待某个资源的时候,比如像没有锁,就会自己挂起(但是当前线程就被挂起了),然后一旦有资源了就能执行,有线程的切换。
挂起:当线程再执行某个任务的时候,需要获取某个资源,但是没有,这个任务就会被挂起,而不存在线程的切换。
JDK21虚拟线程:
Tomcat11支持JDK21虚拟线程,用户态线程
在原本的线程模型里,为了提高并发量,就是采用了异步非阻塞的方式,但是不方便调试。
创建和销毁通过线程池解决了,但是线程多了频繁的切换线程怎么解决呢?
通过虚拟线程解决,无限放大了并发量。
当一个线程阻塞的时候就会被调度到其它平台线程。
主要是IO密集型任务阻塞的时候,不适合CPU密集型,只是增大了规模。不需要池化,用完即抛。
虚拟线程相关问题:
-
为什么虚拟线程不能用synchronized?
- 虚拟线程在两种情况下会pinned
- 系统调用,因为这些操作本身和操作系统资源相关,需要与特定的操作系统相关联,因为synchronized获取和释放涉及到操作系统级别。
- 本地方法调用,可能依赖于线程上下文环境或者是特定的线程局部存储,固定线程可以确保执行环境与预期一致
- 虚拟线程在两种情况下会pinned
- 为什么不要和线程池一起使用?
- 线程池的目的是避免频繁的创建和销毁,而虚拟线程的目的是避免切换,虚拟线程代价并不高,不需要池化
- 为什么避免使用ThreadLocal?
- 虚拟线程数量巨大,可能导致内存泄漏等问题,需要可以考虑scopedvalues,作用域绑定到特定的执行范围或者上下文
- 在CPU密集不建议使用虚拟线程!
线程创建方式?
- 继承Thread
- 实现Runnable
- 实现Callable接口配合FutureTask(实现了runnable和future)
- 线程池
- 线程工厂
为什么要调用start方法,不直接使用run方法?
- 直接调用run方法就是在当前线程中串行执行,而start会创建出一个新的线程进行任务处理。
线程的状态?
操作系统层面:
java层面:
常用调度方法?
什么是线程上下文切换?什么是进程上下文切换?
线程:
分为相同进程和不同进程:不同进程就和进程切换一样,同一个进程CPU资源的分配采用了时间片轮换,当当前线程的时间片结束后,就会从运行状态切换到就绪状态,让给其他线程。信息保留在程序计数器中,切换到另一个栈帧。包括寄存器里的私有数据。TCB
进程:
CPU资源从一个进程切换到另一个进程,需要保留内存空间的指针,运行指令等等,再读入下个进程。
PCB条目:进程标识符(PID唯一)、进程状态、程序计数器(下一条指令的地址)、CPU寄存器(当前进程的所有寄存器值,上下文切换的时候用到)、CPU调度信息(进程优先级和其它参数)、内存管理信息(进程地址空间的指针等)、IO状态等等。
守护线程?
线程分为守护线程和用户线程,当最后一个非守护线程结束的时后,jvm就退出。
线程之间的通信方式?
- 悲观锁、乐观锁、信号量、共享变量、消息队列,条件变量
MESI
- 缓存一致性:各个处理器缓存中存储的内存地址的数据保持一致
- Modified:缓存行的数据已经被修改,且只在当前处理器的缓存中存在,与主内存中的数据不同。
- Exclusive:缓存行的数据与主内存一致,但只有当前处理器缓存中有这个数据,其他处理器没有。
- Shared:缓存行的数据与主内存一致,且可能在多个处理器的缓存中存在。
- Invalid:缓存行的数据是无效的,即另一个处理器已经修改了该数据,当前缓存行中的数据不能再被
原子性、可见性、有序性及解决办法?
-
原子性:
- 多线程操作共享资源,预期结果与最终结果一致,即一个线程操作,另一个线程不会影响到。联想i++这个操作,获取i,i+1,返回i三个操作,获取到i之后当前线程阻塞了,但是还有其他线程进行了操作。
- 解决:synchronized拿锁串行、CAS会导致ABA问题,引入版本号、Lock类似与synchronized串行,volatile不行,可能在拿了一个数据之后写回时候无效,或者其他线程已经修改了,重复了。
-
可见性:
-
JMM获取共享变量会加载到线程的共享内存,如果其他线程更改了,当前并不知道
-
根本原因在于cpu寄存器和指令重排(按顺序来说,a已经是被修改了),如果没有MESI cpu缓存也是会导致的
-
因为线程将共享变量放在了工作内存TCB,用的时候加载到cpu寄存器,但是并不知道已经被修改了
- 解决:volatile,转换为汇编会加一个lock,(volatile 写操作会把之前的共享变量更新一并发布出去,而不只是 volatile 变量本身。)缓存行的数据立即写回主内存,这个写回的数据在其他cpu缓存中标志无效。synchronized将同步代码块里的变量从CPU缓存行中移除,必须去拿新的数据,释放后也会立即更新。lock有volatile修饰的state字段,缓存行的数据也会和上面一样。final不可以修改,kafka生产者这样做的。
-
int a = 0; volatile boolean ready = false; // 线程1 a = 1; ready = true; // 线程2 if (ready) { System.out.println(a); }
线程2可能会看到
ready
为true
,但a
仍然是0
,这就导致了可见性问题,即线程2无法看到线程1对a
的修改。
-
-
有序性:指令重排
- 解决:volatile有内存屏障,阻止屏障之前的写操作排到屏障之后,组织屏障之后的读操作放到屏障之前。synchronized和Reentrantlock也可以
- volatile重点!!!
- 重排序可以分为JIT编译器重排序和处理器重排序,volatile 保证有序性,就是通过分别限制这两种类型的重排序。
- as-if-serial,指在单线程环境下无论如何重排都不会影响结果,要满足
-
int num = 0; boolean ready = false; // 线程1 执行此方法 public void actor1(I_Result r) { if (ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 线程2 执行此方法 public void actor2(I_Result r) { num = 2; ready = true; }
保证了可见性,但无法决定竞态条件,所以需要线程同步
- happens-before:规定了对共享变量的写操作对其它线程的读操作可见
- 线程的:前面的发生在后面线程的之前
- start的
- 中断的
- 锁的:lock发生在unlock
- 对象的:初始化在finalize之前
- 线程的:前面的发生在后面线程的之前
- 在每个 volatile 写操作的前面插入一个
StoreStore
屏障 - 在每个 volatile 写操作的后面插入一个
StoreLoad
屏障 - 在每个 volatile 读操作的后面插入一个
LoadLoad
屏障 - 在每个 volatile 读操作的后面插入一个
LoadStore
屏障
ThreadLocal是什么?
每个线程都有一个ThreadLocalMap(堆中,线程对象本身在堆中,这个是线程对象里的,也在堆中),key为ThreadLocal,value为值,弱引用。
有内存泄漏,尤其在线程池中ThreadLocalMap生命周期太长,没有引用了但是这个线程还在运行,使用后调用remove();垃圾回收将弱引用回收,所引用value的强引用又回收不了,又拿不到。在清理机制之前都泄露了一下
那么key为什么要用弱引用呢?
通过判断Entry不为null,但是key为null判断当前对象应该被清理了。如果是强引用,在线程池中,无法被清理。减少内存泄漏,ThreadLocalMap内部有清理机制弱引用和显式remove()!!!
ThreadLocal结构?
虽然是map但是并没有实现map接口,结构和HashMap还是类似的。存储entry的数组,散列算法hash位运算取余,每次创建一个ThreadLocal对象都会新增一个黄金分割数。带来的就是hash分布很均匀。
怎么解决hash冲突的?
开放地址法,被占了就去找下一个位置。
扩容机制?
上面已经提过ThreadLocalMap内部有清理机制(set、get、remove),当在调用set执行完清理之后,且entry数量也到了2/3的扩容阈值,就开始rehash(),rehash()会执行清理,还要根据条件判断是否扩容?扩容为两倍,通过重新计算hash,开放地址法迁移。
父子线程怎么共享数据?
InheritableThreadLocal,同样也是Thread里的一个变量,当这个变量不为空的时候,就会给子线程。
也可以创建一个成员变量赋值
对java内存模型的理解?不熟!
主要用来定义多线程中变量的访问规则,解决变量的可见性、有序性和原子性问题。
那为什么要有自己的内存呢?不熟!
直接操作共享变量的话,会引发很多竞争,增加了线程安全问题的复杂度。
CPU为了执行效率,可能会对指令进行乱序执行(指令重排),在不影响最终执行结果的前提下,使得CPU有更大的自由度。
什么是指令重排?
为了提高性能,编译器和处理器通常会常常对指令做出重排序。
编译器重排序-指令集并行重排序-内存系统重排序
在单例模式中常常出现。
2、3步骤重排后导致返回未初始化。
线程同步?
为了安全,按照预定的次序访问共享资源,避免造成混乱。
sy和lock
synchronized专题:
- 实现方式:
- synchronized基于对象实现的
- 不操作共享变量的时候触发锁消除
- 在循环中如果频繁的获取和释放,就会触发锁膨胀
- 锁升级
- 无锁,匿名偏向
- 偏向锁:假设是一个线程在访问,通过CAS(JDK15已经废除)
- 轻量级锁:通过CAS,一定时间获取不到就是重量级锁,通过将对象头中的MarkWord复制到线程栈中,CAS进行修改
- 重量级锁:类似Reentrantlock的等待队列等等
- synchronized底层:
通过ObjectMonitor实现的,依赖于JVM的Monitor机制,每个对象都有一个monitor
大概结构类似于reentrantlock -
ObjectMonitor { Object _owner; // 当前拥有 `Monitor` 的线程 int _count; // 记录重入的次数 std::deque<Thread*> _EntryList; // 等待获取 `Monitor` 的线程队列 std::deque<Thread*> _WaitSet; // 调用 `wait()` 后等待唤醒的线程队列 // 其他状态信息和辅助方法 }
- 如何保证原子性、可见性、有序性的
- 原子性单线程访问
- 有序性:实际还是有指令重排,但是单线程下没什么影响
- 可见性:对变量解锁的时候,必须把此变量同步回主存
-
自适应自旋锁
为了减少线程在获取锁时的阻塞开销,Java 引入了自旋锁机制。自适应自旋锁是自旋锁的一种优化策略。
-
自旋锁:
- 线程在尝试获取锁时,如果锁已经被其他线程持有,线程会“自旋”一段时间(即不断循环检查锁是否被释放),而不是立即阻塞挂起。
- 如果在自旋期间锁被释放,线程可以直接获取锁,避免了线程阻塞和上下文切换带来的开销。
-
自适应自旋:
- 自适应自旋是根据线程前一次自旋的成功或失败次数来动态调整自旋时间的机制。
- 如果线程在前一次自旋中成功获得锁,那么在下一次遇到锁竞争时,线程会自旋更长时间。
- 反之,如果自旋失败,系统可能会减少自旋时间或直接挂起线程。
-
优点:
- 自适应自旋锁通过动态调整自旋时间,提高了锁竞争时的性能,减少了线程阻塞的频率。
-
sy和lock的区别?
- sy是关键字,lock是对象
- sy不用主动释放(通过monitor enter和monitor exit实现),lock要主动释放
- lock没有锁升级,同时可以中断等待,有非公平锁,有读写锁,condition比wait那些功能强。
- reentrantlock支持公平锁非公平锁
重磅!ReetrantLock源码分析!下面是1.8的自己看了下和新版差别还是有一些的!
首先是最重要的AQS!
JUC包下面的一个基类,JUC下很多比如RL、ThreadPoolExcutor,阻塞队列、CountDownLatch、CyclicBarrier都是基于AQS实现的!
有一个volatile修饰,并且采用CAS方式的int state的变量
AQS维护了一个双向链表,有head和tail,head属于是个虚拟节点,不存储Thread;
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;
//表示线程已经被移动到Condition队列中了
static final int CONDITION = -2;
//用于表示当一个线程在某些同步器中成功释放资源或锁时,应该将信号传播给其他等待的线程。
//信号量用到了
static final int PROPAGATE = -3;
//0表示当前节点在阻塞队列中,等待着获取锁,把上面几个写到这里面
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
公平锁流程:
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);
}
acquire:
public final void acquire(int arg) {
// tryAcquire:再次查看,当前线程是否可以尝试获取锁资源
if (!tryAcquire(arg) &&
// 没有拿到锁资源
// acquireQueued:查看我是否是第一个排队的节点,如果是可以再次尝试获取锁资源,如果长时间拿不到,挂起线程
// 如果不是第一个排队的额节点,就尝试挂起线程即可
acquireQueued(
// addWaiter(Node.EXCLUSIVE):将当前线程封装为Node节点,插入到AQS的双向链表的结尾
addWaiter(Node.EXCLUSIVE), arg))
// 中断线程的操作
selfInterrupt();
}
tryacquire就不一样了,非公平锁是判断state是否为0然后直接抢锁,后面有一些重入锁的逻辑,而公平锁需要在state=0的情况下去判断等待队列里面有没有排队的,没有或者自己是第一个就去拿一下,后面也是锁重入。
// 非公平锁实现
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;
// 如果头尾节点相等,证明没有线程排队,直接去抢占锁资源
return h != t &&
// s节点不为null,并且s节点的线程为当前线程(排在第一名的是不是我)
(s == null || s.thread != Thread.currentThread());
}
try完了就是进入下一个判断addWaiter,公平不公平都得走这里,封装成节点,加到尾巴上
// 没有拿到锁资源,过来排队, 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;
}
}
}
}
最后就是acquireQueued方法,一直在循环里面,此时已经是尾巴了,看看前面那个节点是不是头,是就再获取一下资源。不是就尝试挂起,挂起要判断前面节点的状态是不是正常的,>0都不正常!
// 当前没有拿到锁资源后,并且到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(time),可以中断,分为公平锁和非公平锁逻辑,拿不到就看时间是否挂起,没时间就算了,有时间就挂起这么久,但是唤醒的时候要看是中断、到时间了、被唤醒,前两个要取消节点。
取消节点分析:
这时候得看是哪个位置?尾巴?头后面?还是中间的?不同的处理逻辑,
// 取消在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方法,拿不到锁就死等,要么被唤醒,要么被中断,中断唤醒抛异常,取消节点。
释放锁:
流程就是释放state,看head的waitstatus去唤醒后面有效的节点,从后往前找最近的节点,因为在取消节点那里存在前面找不到后面,但是后面指着前面。
public void unlock() {
// 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
sync.release(1);
}
// 释放锁的核心流程
public final boolean release(int arg) {
// 核心释放锁资源的操作之一
if (tryRelease(arg)) {
// 如果锁已经释放掉了,走这个逻辑
Node h = head;
// h不为null,说明有排队的(录课时估计脑袋蒙圈圈。)
// 如果h的状态不为0(为-1),说明后面有排队的Node,并且线程已经挂起了。
if (h != null && h.waitStatus != 0)
// 唤醒排队的线程
unparkSuccessor(h);
return true;
}
return false;
}
// ReentrantLock释放锁资源操作
protected final boolean tryRelease(int releases) {
// 拿到state - 1(并没有赋值给state)
int c = getState() - releases;
// 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// free,代表当前锁资源是否释放干净了。
boolean free = false;
if (c == 0) {
// 如果state - 1后的值为0,代表释放干净了。
free = true;
// 将持有锁的线程置位null
setExclusiveOwnerThread(null);
}
// 将c设置给state
setState(c);
// 锁资源释放干净返回true,否则返回false
return free;
}
// 唤醒后面排队的Node
private void unparkSuccessor(Node node) {
// 拿到头节点状态
int ws = node.waitStatus;
if (ws < 0)
// 先基于CAS,将节点状态从-1,改为0
compareAndSetWaitStatus(node, ws, 0);
// 拿到头节点的后续节点。
Node s = node.next;
// 如果后续节点为null或者,后续节点的状态为1,代表节点取消了。
if (s == null || s.waitStatus > 0) {
s = null;
// 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点环境
for (Node t = tail; t != null && t != node; t = t.prev)
// 从后往前找到状态小于等于0的节点
// 找到离head最新的有效节点,并赋值给s
if (t.waitStatus <= 0)
s = t;
}
// 只要找到了这个需要被唤醒的节点,执行unpark唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
ConditionObject
为了实现wait和notify的方法
await:
单向链表!同样是用AQS的Node实现的,但是会判断中断,没中断就会加入单向链表里面,同时也会干掉取消了的Node,状态设置为-2,然后挂起,释放state
当这个线程重新启动的时候,看是中断唤醒还是被唤醒,或者是唤醒后被中断了,根据中断标志位和状态去判断去操作,因为正常唤醒的都在AQS队列,后面就需要去移除condition这些之类的
signal:
只有持有锁资源的线程才能signal,脱离队列,改状态0,加入AQS队列
ReetrantReadWriteLock
这里是共享模式的AQS
写互斥、读共享,读基于state的高16位置,写基于低16位
阻塞队列-只写我总结的点,源码不写了
ArrayBlockingQueue:
基于ReentrantLock实现的,一把锁两个condition,同时是循环数组,生产者消费者有各自的Condition,简化并发操作但是性能不行,因为通过条件变量去控制,两把锁无法保证并发安全,例如count;
线程挂起被唤醒的时候采用的while防止虚假唤醒,适合CPU密集
LinkedBlockingQueue:
有哨兵节点,一把锁,两个Condition互相唤醒,两把锁为了性能,适合IO密集
PriorityBlockingQueue:
数组实现的无界二叉堆(可自定义比较器),最大容量为Integer.MAX_VALUE - 8,分虚拟机,一把锁一个Condition,通过一个volatile变量CAS实现扩容,此时会释放锁,小于64会2倍,大于64会1.5倍,长度超过最大就会失败,其他的线程就等待新数组生成写新数组,会调整结构所以一把锁一个condition,叫notEmpty。
DelayQueue:
一把锁,一个Condition,同理上面,基于优先级队列实现的,通过condition的awaitnanos去等
SynchronousQueue:
存消费者和生产者的,进行匹配,主要是内部类Transfer,两种实现方式TransferStack、TransferQueue前者非公平,后者公平,比较少,就不看了。。。
线程池!
五种线程池
固定线程池newFixedThreadPool固定线程数,LinkedBlockingQueue;
单例线程池newSingleThreadPool一个线程,LinkedBlockingQueue;
缓存线程池newCachedThreadPool,核心线程0,最大线程无限,使用的SynchronousQueue,只要提交工作就有工作线程能处理,使用SynchronousQueue的原因是理念相似,快速任务处理,避免累积任务,减少资源占用,同时动态调整;
定时任务线程池newScheduleThreadPool,基于DelayQueue
newWorkStealingPool,是基于ForkJoinPool构造出来的,有多个阻塞队列,分而治之
线程池源码!
七个参数:
public ThreadPoolExecutor(
int corePoolSize, // 核心工作线程(当前任务执行结束后,不会被销毁)
int maximumPoolSize, // 最大工作线程(代表当前线程池中,一共可以有多少个工作线程)
long keepAliveTime, // 非核心工作线程在阻塞队列位置等待的时间
TimeUnit unit, // 非核心工作线程在阻塞队列位置等待时间的单位
BlockingQueue<Runnable> workQueue, // 任务在没有核心工作线程处理时,任务先扔到阻塞队列中
ThreadFactory threadFactory, // 构建线程的线程工作,可以设置thread的一些信息
RejectedExecutionHandler handler) { // 当线程池无法处理投递过来的任务时,执行当前的拒绝策略
// 初始化线程池的操作
}
四种拒绝策略:
AbortPolicy:当前拒绝策略会在无法处理任务时,直接抛出一个异常
CallerRunsPolicy:当前拒绝策略会在线程池无法处理任务时,将任务交给调用者处理
DiscardPolicy:当前拒绝策略会在线程池无法处理任务时,直接将任务丢弃掉
DiscardOldestPolicy:当前拒绝策略会在线程池无法处理任务时,将队列中最早的任务丢弃掉,将当前任务再次尝试交给线程池处理
自定义实现接口。
参数设置:
- 核心线程数看cpu密集还是io密集
- 非核心线程,同上
- 等待时间:要求响应好就多等一会儿,创建也要时间
- 阻塞队列:像LinkedList一把锁两个condition效率高
- 拒绝策略:不允许丢弃就callruns
- 核心属性ctl的AtomicInteger是volatile修饰的
-
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates 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;
-
// 当前是线程池的核心属性 // 当前的ctl其实就是一个int类型的数值,内部是基于AtomicInteger套了一层,进行运算时,是原子性的。 // ctl表示着线程池中的2个核心状态: // 线程池的状态:ctl的高3位,表示线程池状态 // 工作线程的数量:ctl的低29位,表示工作线程的个数 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // Integer.SIZE:在获取Integer的bit位个数 // 声明了一个常量:COUNT_BITS = 29 private static final int COUNT_BITS = Integer.SIZE - 3; 00000000 00000000 00000000 00000001 00100000 00000000 00000000 00000000 00011111 11111111 11111111 11111111 // CAPACITY就是当前工作线程能记录的工作线程的最大个数 private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 线程池状态的表示 // 当前五个状态中,只有RUNNING状态代表线程池没问题,可以正常接收任务处理 // 111:代表RUNNING状态,RUNNING可以处理任务,并且处理阻塞队列中的任务。 private static final int RUNNING = -1 << COUNT_BITS; // 000:代表SHUTDOWN状态,不会接收新任务,正在处理的任务正常进行,阻塞队列的任务也会做完。 private static final int SHUTDOWN = 0 << COUNT_BITS; // 001:代表STOP状态,不会接收新任务,正在处理任务的线程会被中断,阻塞队列的任务一个不管。 private static final int STOP = 1 << COUNT_BITS; // 010:代表TIDYING状态,这个状态是否SHUTDOWN或者STOP转换过来的,代表当前线程池马上关闭,就是过渡状态。 private static final int TIDYING = 2 << COUNT_BITS; // 011:代表TERMINATED状态,这个状态是TIDYING状态转换过来的,转换过来只需要执行一个terminated方法。 private static final int TERMINATED = 3 << COUNT_BITS; // 在使用下面这几个方法时,需要传递ctl进来 // 基于&运算的特点,保证只会拿到ctl高三位的值。 private static int runStateOf(int c) { return c & ~CAPACITY; } // 基于&运算的特点,保证只会拿到ctl低29位的值。 private static int workerCountOf(int c) { return c & CAPACITY; }
高三位:线程池状态,低29位:最大线程数,-1到3五个状态,-1为running
-
有参构造主要是健壮性校验,核心线程是可以为0的哦
// 有参构造。无论调用哪个有参构造,最终都会执行当前的有参构造 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // 健壮性校验 // 核心线程个数是允许为0个的。 // 最大线程数必须大于0,最大线程数要大于等于核心线程数 // 非核心线程的最大空闲时间,可以等于0 if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) // 不满足要求就抛出参数异常 throw new IllegalArgumentException(); // 阻塞队列,线程工厂,拒绝策略都不允许为null,为null就扔空指针异常 if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); // 不要关注当前内容,系统资源访问决策,和线程池核心业务关系不大。 this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); // 各种赋值,JUC包下,几乎所有涉及到线程挂起的操作,单位都用纳秒。 // 有参构造的值,都赋值给成员变量。 // Doug Lea的习惯就是将成员变量作为局部变量单独操作。 this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
-
excute方法,如果当前线程数量小于核心线程,就会增加工作线程,addWorker是可以通过参数控制创建核心线程还是普通线程的
// 提交任务到线程池的核心方法 // command就是提交过来的任务 public void execute(Runnable command) { // 提交的任务不能为null if (command == null) throw new NullPointerException(); // 获取核心属性ctl,用于后面的判断 int c = ctl.get(); // 如果工作线程个数,小于核心线程数。 // 满足要求,添加核心工作线程 if (workerCountOf(c) < corePoolSize) { // addWorker(任务,是核心线程吗) // addWorker返回true:代表添加工作线程成功 // addWorker返回false:代表添加工作线程失败 // addWorker中会基于线程池状态,以及工作线程个数做判断,查看能否添加工作线程 if (addWorker(command, true)) // 工作线程构建出来了,任务也交给command去处理了。 return; // 说明线程池状态或者是工作线程个数发生了变化,导致添加失败,重新获取一次ctl c = ctl.get(); } // 添加核心工作线程失败,往这走 // 判断线程池状态是否是RUNNING,如果是,正常基于阻塞队列的offer方法,将任务添加到阻塞队列 if (isRunning(c) && workQueue.offer(command)) { // 如果任务添加到阻塞队列成功,走if内部 // 如果任务在扔到阻塞队列之前,线程池状态突然改变了。 // 重新获取ctl int recheck = ctl.get(); // 如果线程池的状态不是RUNNING,将任务从阻塞队列移除, if (!isRunning(recheck) && remove(command)) // 并且直接拒绝策略 reject(command); // 在这,说明阻塞队列有我刚刚放进去的任务 // 查看一下工作线程数是不是0个 // 如果工作线程为0个,需要添加一个非核心工作线程去处理阻塞队列中的任务 // 发生这种情况有两种: // 1. 构建线程池时,核心线程数是0个。 // 2. 即便有核心线程,可以设置核心线程也允许超时,设置allowCoreThreadTimeOut为true,代表核心线程也可以超时 else if (workerCountOf(recheck) == 0) // 为了避免阻塞队列中的任务饥饿,添加一个非核心工作线程去处理 addWorker(null, false); } // 任务添加到阻塞队列失败 // 构建一个非核心工作线程 // 如果添加非核心工作线程成功,直接完事,告辞 else if (!addWorker(command, false)) // 添加失败,执行决绝策略 reject(command); }
通过上面可以看到,当添加核心线程失败的时候,可能会出现多种情况,第一种是ctl变化,可能是状态变化,具体看下面的addWorker。然后回去判断状态以及添加进阻塞队列,并不是直接就去创建工作线程!如果状态变化就会执行拒绝策略和移除任务!如果是running或者任务移除失败,也要确保有线程可以去执行任务!如果添加阻塞队列失败就会添加线程,添加失败就执行拒绝策略!就是说当阻塞队列满了或者是运行状态为shutdown才会去创建新线程的。
-
Worker:
// Worker继承了AQS,目的就是为了控制工作线程的中断。 // Worker实现了Runnable,内部的Thread对象,在执行start时,必然要执行Worker中断额一些操作 private final class Worker extends AbstractQueuedSynchronizer implements Runnable{ // =======================Worker管理任务================================ // 线程工厂构建的线程 final Thread thread; // 当前Worker要执行的任务 Runnable firstTask; // 记录当前工作线程处理了多少个任务。 volatile long completedTasks; // 有参构造 Worker(Runnable firstTask) { // 将State设置为-1,代表当前不允许中断线程 setState(-1); // 任务赋值 this.firstTask = firstTask; // 基于线程工作构建Thread,并且传入的Runnable是Worker this.thread = getThreadFactory().newThread(this); } // 当thread执行start方法时,调用的是Worker的run方法, public void run() { // 任务执行时,执行的是runWorker方法 runWorker(this); } // =======================Worker管理中断================================ // 当前方法是中断工作线程时,执行的方法 void interruptIfStarted() { Thread t; // 只有Worker中的state >= 0的时候,可以中断工作线程 if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { try { // 如果状态正常,并且线程未中断,这边就中断线程 t.interrupt(); } catch (SecurityException ignore) { } } } protected boolean isHeldExclusively() { return getState() != 0; } protected boolean tryAcquire(int unused) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } protected boolean tryRelease(int unused) { setExclusiveOwnerThread(null); setState(0); return true; } public void lock() { acquire(1); } public boolean tryLock() { return tryAcquire(1); } public void unlock() { release(1); } public boolean isLocked() { return isHeldExclusively(); } }
runworker:总而言之就是在while里面不断循环执行任务,其中有很多状态判断以及超时检验,拿不到任务就会销毁,销毁的可能是核心线程,可能是非核心线程,看哪个线程没任务了
// 工作线程启动后执行的任务。 final void runWorker(Worker w) { // 拿到当前线程 Thread wt = Thread.currentThread(); // 从worker对象中拿到任务 Runnable task = w.firstTask; // 将Worker中的firstTask置位空 w.firstTask = null; // 将Worker中的state置位0,代表当前线程可以中断的 w.unlock(); // allow interrupts // 判断工作线程是否是异常结束,默认就是异常结束 boolean completedAbruptly = true; try { // 获取任务 // 直接拿到第一个任务去执行 // 如果第一个任务为null,去阻塞队列中获取任务 while (task != null || (task = getTask()) != null) { // 执行了Worker的lock方法,当前在lock时,shutdown操作不能中断当前线程,因为当前线程正在处理任务 w.lock(); // 比较ctl >= STOP,如果满足找个状态,说明线程池已经到了STOP状态甚至已经要凉凉了 // 线程池到STOP状态,并且当前线程还没有中断,确保线程是中断的,进到if内部执行中断方法 // if(runStateAtLeast(ctl.get(), STOP) && !wt.isInterrupted()) {中断线程} // 如果线程池状态不是STOP,确保线程不是中断的。 // 如果发现线程中断标记位是true了,再次查看线程池状态是大于STOP了,再次中断线程 // 这里其实就是做了一个事情,如果线程池状态 >= STOP,确保线程中断了。 if ( ( runStateAtLeast(ctl.get(), STOP) || ( Thread.interrupted() && runStateAtLeast(ctl.get(), STOP) ) ) && !wt.isInterrupted()) wt.interrupt(); 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; // 当前工作线程处理的任务数+1 w.completedTasks++; // 执行unlock方法,此时shutdown方法才可以中断当前线程 w.unlock(); } } // 如果while循环结束,正常走到这,说明是正常结束 // 正常结束的话,在getTask中就会做一个额外的处理,将ctl - 1,代表工作线程没一个。 completedAbruptly = false; } finally { // 考虑干掉工作线程 processWorkerExit(w, completedAbruptly); } } // 工作线程结束前,要执行当前方法 private void processWorkerExit(Worker w, boolean completedAbruptly) { // 如果是异常结束 if (completedAbruptly) // 将ctl - 1,扣掉一个工作线程 decrementWorkerCount(); // 操作Worker,为了线程安全,加锁 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 当前工作线程处理的任务个数累加到线程池处理任务的个数属性中 completedTaskCount += w.completedTasks; // 将工作线程从hashSet中移除 workers.remove(w); } finally { // 释放锁 mainLock.unlock(); } // 只要工作线程凉了,查看是不是线程池状态改变了。 tryTerminate(); // 获取ctl int c = ctl.get(); // 判断线程池状态,当前线程池要么是RUNNING,要么是SHUTDOWN if (runStateLessThan(c, STOP)) { // 如果正常结束工作线程 if (!completedAbruptly) { // 如果核心线程允许超时,min = 0,否则就是核心线程个数 int min = allowCoreThreadTimeOut ? 0 : corePoolSize; // 如果min == 0,可能会出现没有工作线程,并且阻塞队列有任务没有线程处理 if (min == 0 && ! workQueue.isEmpty()) // 至少要有一个工作线程处理阻塞队列任务 min = 1; // 如果工作线程个数 大于等于1,不怕没线程处理,正常return if (workerCountOf(c) >= min) return; } // 异常结束,为了避免出现问题,添加一个空任务的非核心线程来填补上刚刚异常结束的工作线程 addWorker(null, false); } }
jstack pid,可视化工具查看死锁
LongAdder和AtomicLong?
- 主要为了解决AtmoicLong下多线程竞争激烈导致性能不高,通过分段+CAS的形式实现的,也是通过Unsafe
并发集合?
线程安全的集合该怎么选择?
List、Set线程安全的有Vector、Collections.synchronizedList、CopyOnWrite系列(lock)。
数据量大怎么考虑?
不考虑CopyOnWrite,因为在写操作的时候会复制一套副本,在副本中进行操作,读操作读原生数据,因为要复制。
不在业务代码实现的话,就选择Collections.synchronizedList
也可以仿照Collections.synchronizedList,直接做一个装饰着模式,套一层
多列集合就用ConCurrent系列;
ConcurrentHashMap存储数据结构是什么样子呢?
数组+链表+红黑树
链表到红黑树是8,红黑树到链表是6,红黑树上也维持着双向链表!
可以用跳表吗?
可以,但是不支持修改结构,同时跳表空间占用率较高,但是写效率不像红黑树这么复杂。
ConcurrentHashMap的负载因子可以重新指定吗?
final修饰,不能改,同时没有基于负载因子计算阈值,是直接位运算的,改不改没区别,HashMap可以改,小了导致频繁扩容,大了导致冲突多,同时也要重新计算泊松比。
ConcurrentHashMap的散列算法?相比HashMap有什么区别?
都是对key进行hashCode进行取模高16位和低16位进行异或,但是高位还是保留着的!!!,就是对长度-1进行与运算。
源码:
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ConcurrentHashMap不允许key或者value出现为null的值,跟HashMap的区别
if (key == null || value == null) throw new NullPointerException();
// 根据key的hashCode计算出一个hash值,后期得出当前key-value要存储在哪个数组索引位置
int hash = spread(key.hashCode());
// 一个标识,在后面有用!
int binCount = 0;
// 省略大量的代码……
}
// 计算当前Node的hash值的方法
static final int spread(int h) {
// 将key的hashCode值的高低16位进行^运算,最终又与HASH_BITS进行了&运算
// 将高位的hash也参与到计算索引位置的运算当中
// 为什么HashMap、ConcurrentHashMap,都要求数组长度为2^n
// HASH_BITS让hash值的最高位符号位肯定为0,代表当前hash值默认情况下一定是正数,因为hash值为负数时,有特殊的含义
// static final int MOVED = -1; // 代表当前hash位置的数据正在扩容!
// static final int TREEBIN = -2; // 代表当前hash位置下挂载的是一个红黑树
// static final int RESERVED = -3; // 预留当前索引位置……
return (h ^ (h >>> 16)) & HASH_BITS;
// 计算数组放到哪个索引位置的方法 (f = tabAt(tab, i = (n - 1) & hash)
// n:是数组的长度
}
sizeCtl作用?
初始化和扩容的一个控制作用。-1代表正在初始化,-n代表正在扩容并代表代表有n-1个线程正在扩容。=0啥事没有,>0两个意思要么没初始化代表初始化数组的长度,初始化了代表下次扩容的阈值。
扩容整体流程?
计算扩容标识戳,包括sizeCtl、transferIndex、nextTable等,一起协调扩容过程,到达sizeCtl阈值开始扩容。
第一个扩容的线程--给sizeCtl赋值,计算自己的步长,初始化为两倍开始迁移,一直循环领迁移任务,直到结束。
后面有线程进来就将sizeCtl+1,计算步长、领取任务、循环扩容结束。
扩容标识戳?
基于oldTable长度计算出来的,第十七位一定是1,然后左移15位,第一个进来+2,后面进来+1,然后赋值给sizeCtl。
那为啥基于长度计算?
确保扩容的线程,要和正在扩容的线程要扩成一样的。
如何统计个数?
为了确保安全以及效率,选择的是CounterCells数组,CAS修改,借鉴了LongAdder,多位置记录,但是sum可能不准确
ConcurrentHashMap在JDK1.7和1.8如何保证写数据线程安全?
1.7基于ReentrantLock实现的,一把锁锁多个格子;
1.8基于CAS和Synchronized,CAS填格子,Syn锁格子上的Node;
扩容期间可以查询数据吗?
直接在老数组上查,扩容完成会有一个ForwardingNode,代表去新数组了。
为何保留一套双向链表?
写操作时,为了平衡,会左旋和右旋,指针会变化,但是有双向链表(并不是实际上的双向链表,而是指向父节点,有点像双向)就方便读了;
迁移的时候,以及退化的时候也很方便。
三个工具类:
协调多个线程之间的执行和同步
CountDownLatch:
简化理解旅游团,大家自己去玩,司机在等待,最后来一个countdown一个,然后==0了一起回家。基于AQS
就是state,await就是插入双向链表挂起线程,如果countdown完之后state为0就去唤醒等待的节点。同时也要看状态-1唤醒,然后改成0避免重复唤醒。
一个线程等待多个线程
- 应用:原本在项目里是比如写入数据库后需要去写redis、上传minio之类的,下面这两个死循环同步完成,但是有一个挂了蛮浪费系统资源的,还出不去了,不适合这种分布式事务,还是得mq那些比较好。
- 用的话就是在测试中模拟并发,同步蛮复杂,用的少
CyclicBarrier
简化理解为旅游团,大家全部集合完了一起出发,基于ReentrantLock和condition,用来保证count,可以重复使用,如果有等待线程中断,可以抛出异常(打上一个broken的标志位),避免无限等待。线程中断后唤醒所有线程,然后使用reset()就能重复使用了。
Semaphore
基于AQS的State做一个维护,保证一个资源可以被多个线程访问。做一些流量控制,-3表示是否应该唤醒多个线程同时获得资源。
如何减少线程的上下文切换?
- 减少线程数:太多切换频繁
- 无锁并发编程:避免进入阻塞而减少切换次数
- 乐观锁:非阻塞
- 虚拟线程,虚拟线程也有自己的栈
UnSafe类?
- 提供硬件级别的原子操作
- 主要有:
- 分配内存释放内存
- 线程挂起
- CAS操作
实现三个线程顺序执行的方式?
使用reentrantlock需要同步措施+同步标识符+虚假唤醒
可以通过join抢占,或者countlatch这种实现,一个执行完执行另一个
//使用CompletableFuture
void fun3(){
CompletableFuture.runAsync(() -> System.out.println("t1")).thenRun(() -> System.out.println("t2")).thenRun(() -> System.out.println("t3"));
}
//使用join,join会等待线程执行完
public class ThreadOrderExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("Thread 1 is running");
});
Thread t2 = new Thread(() -> {
System.out.println("Thread 2 is running");
});
Thread t3 = new Thread(() -> {
System.out.println("Thread 3 is running");
});
try {
t1.start(); // Start the first thread
t1.join(); // Wait for the first thread to finish
t2.start(); // Start the second thread
t2.join(); // Wait for the second thread to finish
t3.start(); // Start the third thread
t3.join(); // Wait for the third thread to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//使用countdownlatch
public static void main(String[] args) {
CountDownLatch latch1 = new CountDownLatch(1); // Latch for thread 2
CountDownLatch latch2 = new CountDownLatch(1); // Latch for thread 3
Thread t1 = new Thread(() -> {
System.out.println("Thread 1 is running");
latch1.countDown(); // Decrease latch1 to 0, allowing thread 2 to run
});
Thread t2 = new Thread(() -> {
try {
latch1.await(); // Wait for latch1 to reach 0
System.out.println("Thread 2 is running");
latch2.countDown(); // Decrease latch2 to 0, allowing thread 3 to run
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t3 = new Thread(() -> {
try {
latch2.await(); // Wait for latch2 to reach 0
System.out.println("Thread 3 is running");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t3.start();
}
进阶:实现多个线程顺序打印0-999
通过synchronized和reentrantlock实现
public static void main(String[] args) {
fun1();
}
//通过Synchronized实现
static final Object Lock=new Object();
static AtomicInteger count=new AtomicInteger(0);
static final int MAXN=100;
static void fun1(){
new Thread(new Ree(0)).start();
new Thread(new Ree(1)).start();
new Thread(new Ree(2)).start();
}
static class Syn implements Runnable{
private int i;
public Syn(int i) {
this.i = i;
}
@Override
public void run() {
while (count.get()<=MAXN){
synchronized (Lock){
try{
while (count.get()%3!=i){
Lock.wait();
}
//醒之后要再判断,有点像单例模式
if (count.get()<=MAXN){
System.out.println("Thread-"+i+"---"+count.get());
}
count.getAndIncrement();
Lock.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//通过ReentrantLock实现
static final ReentrantLock lock=new ReentrantLock();
static final Condition c0=lock.newCondition();
static final Condition c1=lock.newCondition();
static final Condition c2=lock.newCondition();
static final HashMap<String,Condition> map=new HashMap<>(3);
static class Ree implements Runnable{
private int i;
public Ree(int i) {
this.i = i;
map.put("c0",c0);
map.put("c1",c1);
map.put("c2",c2);
}
@Override
public void run() {
while (count.get()<=MAXN){
lock.lock();
try {
while (count.get()%3!=i){
map.get("c"+i).await();
}
if (count.get()<=MAXN){
System.out.println("Thread-"+i+"---"+count.get());
count.getAndIncrement();
map.get("c"+((i+1)%3)).signal();
}else {
map.get("c"+((i+1)%3)).signal();
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}