前言
随着科技的进步,计算机硬件和算力也迎来跨越式的发展,从单核 CPU 串行执行计算任务到如今多核 CPU 并行执行任务。
为了充分利用多核资源,并发编程技术应时代出现。
并发编程在提高程序执行效率的同时,也带来了技术上的挑战,比如多线程之间如何通信,常见的解决方案是通过管道或者共享内存。Java 作为比较流行的编程语言,自然也是要顺应并发的潮流,Java 采用共享内存方式保障多线程之间的通信。
使用共享内存通信,也带来了并发的相关问题:可见性、原子性、有序性。
其中原子性则是由线程切换导致的并发问题,线程如何调度也是一门精细活,本文则从线程阻塞和唤醒的调度角度,来探讨和揭开一部分 Unsafe 魔法类的面纱。
阻塞和唤醒
艺术来源于生活,一些技术上的解决方案也可以体现在日常生活中。
如社区医院的就诊过程,患者取号->医生叫号->诊断->检查室检查->拿到结果复诊->诊断结束-叫下一位,把医生和患者看做线程和任务,则等待患者和诊断则是阻塞和唤醒的场景。
基于该场景,做一下代码实现:
public class test07 {
public static void main(String[] args) {
List<Patient> list = Arrays.asList(
new Patient(Check.BRAIN.getType()),
new Patient(Check.EYES.getType()),
new Patient(Check.OTHER.getType())
);
ExecutorService executorService = Executors.newCachedThreadPool();
for (Patient patient : list) {
executorService.submit(new Doctor(patient));
executorService.submit(new SpecialExamination(patient));
}
executorService.shutdown();
System.out.println("ok");
}
}
class Doctor implements Runnable {
private Patient patient;
public Doctor(Patient patient) {
this.patient = patient;
}
public void run() {
/**
* 1.检查是否挂号
* 2.是否需要检查原因
* 3.处理其他患者
* */
synchronized (this.patient) {
while (null == patient.getReason()) {
System.out.println("医生" + Thread.currentThread().getName() + "单号" + patient.getNumber() + "需要进行检查");
try {
patient.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("医生" + Thread.currentThread().getName() + "开始治疗单号" + patient.getNumber() + "类型" + patient.getType() + "原因" + patient.getReason());
return;
}
}
}
class SpecialExamination implements Runnable {
private Patient patient;
public SpecialExamination(Patient patient) {
this.patient = patient;
}
public void run() {
synchronized (patient) {
if (patient.getType() == Check.BRAIN.getType()) {
patient.setReason("脑子坏了");
} else if (patient.getType() == Check.EYES.getType()) {
patient.setReason("头发没了");
} else if (patient.getType() == Check.OTHER.getType()) {
patient.setReason("其他原因");
}
System.out.println("通知单号" + patient.getNumber() + "可以开始会诊");
patient.notifyAll();
}
}
}
enum Check {
BRAIN(1),
EYES(2),
OTHER(3);
public int getType() {
return type;
}
private int type;
Check(int type) {
this.type = type;
}
}
class Patient {
private String number;
private int type;
private String reason;
public Patient(int type) {
this.number = UUID.randomUUID().toString();
this.type = type;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
}
运行结果:
医生pool-1-thread-1单号284a4fe2-4692-47b7-8466-44b36a1d6265需要进行检查
通知单号284a4fe2-4692-47b7-8466-44b36a1d6265已经拿到检查结果可以开始会诊
医生pool-1-thread-1开始治疗单号284a4fe2-4692-47b7-8466-44b36a1d6265类型1原因脑子坏了
医生pool-1-thread-3单号76afe44b-2bb2-40e9-9201-ef4d24a95619需要进行检查
通知单号76afe44b-2bb2-40e9-9201-ef4d24a95619已经拿到检查结果可以开始会诊
医生pool-1-thread-3开始治疗单号76afe44b-2bb2-40e9-9201-ef4d24a95619类型2原因眼镜坏了
医生pool-1-thread-5单号c98b41de-6ab8-41a7-8c11-9838eeacd73e需要进行检查
通知单号c98b41de-6ab8-41a7-8c11-9838eeacd73e已经拿到检查结果可以开始会诊
分诊结束
医生pool-1-thread-5开始治疗单号c98b41de-6ab8-41a7-8c11-9838eeacd73e类型3原因其他原因
由上代码输出可以看到每个 "医生" 都让 "患者" 去做检查,检查完毕后再由当前 "医生" 进行会诊。
"医生" 线程接待任务,然后任务流转到检查线程,"医生" 线程阻塞等待检查线程执行完毕后被唤醒执行剩下的事情。
上面的实现借助 wait & notify 来实现阻塞和唤醒,如果唤醒线程先执行,阻塞线程后执行,那么被挂起的线程将永远无法被唤醒,无法达到预期结果,所以需要引入锁来保障执行顺序。
如果不借助 wait & notify 和锁能实现阻塞和唤醒吗?当然可以,下面看一下使用 LockSupport 如何达成效果吧。
public class test07 {
public static void main(String[] args) {
List<Patient> list = Arrays.asList(
new Patient(Check.BRAIN.getType()),
new Patient(Check.EYES.getType()),
new Patient(Check.OTHER.getType())
);
ExecutorService executorService = Executors.newCachedThreadPool();
for (Patient patient : list) {
executorService.submit(new Doctor(patient));
executorService.submit(new SpecialExamination(patient));
}
executorService.shutdown();
System.out.println("分诊结束");
}
}
class Doctor implements Runnable {
private Patient patient;
public Doctor(Patient patient) {
this.patient = patient;
}
@Override
public void run() {
/**
* 1.检查是否挂号
* 2.是否需要检查原因
* 3.处理其他患者
* */
while (null == patient.getReason()) {
System.out.println("医生" + Thread.currentThread().getName() + "单号" + patient.getNumber() + "需要进行检查");
patient.setCurThread(Thread.currentThread());
LockSupport.park(patient);
}
System.out.println("医生" + Thread.currentThread().getName() + "开始治疗单号" + patient.getNumber() + "类型" + patient.getType() + "原因" + patient.getReason());
return;
}
}
class SpecialExamination implements Runnable {
private Patient patient;
public SpecialExamination(Patient patient) {
this.patient = patient;
}
@Override
public void run() {
if (patient.getType() == Check.BRAIN.getType()) {
patient.setReason("脑子坏了");
} else if (patient.getType() == Check.EYES.getType()) {
patient.setReason("眼镜坏了");
} else if (patient.getType() == Check.OTHER.getType()) {
patient.setReason("其他原因");
}
System.out.println("通知单号" + patient.getNumber() + "已经拿到检查结果可以开始会诊");
LockSupport.unpark(patient.getCurThread());
}
}
enum Check {
BRAIN(1),
EYES(2),
OTHER(3);
public int getType() {
return type;
}
private int type;
Check(int type) {
this.type = type;
}
}
class Patient {
private String number;
private int type;
private String reason;
public Thread getCurThread() {
return curThread;
}
public void setCurThread(Thread curThread) {
this.curThread = curThread;
}
private Thread curThread;
public Patient(int type) {
this.number = UUID.randomUUID().toString();
this.type = type;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
}
运行结果:
医生pool-1-thread-1单号f1cd75b6-3b08-4d91-86c8-b2840d586b42需要进行检查
通知单号f1cd75b6-3b08-4d91-86c8-b2840d586b42已经拿到检查结果可以开始会诊
医生pool-1-thread-1开始治疗单号f1cd75b6-3b08-4d91-86c8-b2840d586b42类型1原因脑子坏了
医生pool-1-thread-3单号0a1dad26-9569-494f-8c87-6185aa453d76需要进行检查
通知单号0a1dad26-9569-494f-8c87-6185aa453d76已经拿到检查结果可以开始会诊
医生pool-1-thread-1单号56202b98-789f-4bd3-ba5d-c0809559b3e2需要进行检查
医生pool-1-thread-3开始治疗单号0a1dad26-9569-494f-8c87-6185aa453d76类型2原因眼镜坏了
分诊结束
通知单号56202b98-789f-4bd3-ba5d-c0809559b3e2已经拿到检查结果可以开始会诊
医生pool-1-thread-1开始治疗单号56202b98-789f-4bd3-ba5d-c0809559b3e2类型3原因其他原因
上代码块做了一些改造将 "医生" 线程对象与 "患者" 任务进行了绑定,使用 LockSupport 进行阻塞和唤醒,由结果可得,两个 "医生" 线程都有预期的表现。
LockSupport 在不使用锁的情况下完成了线程的预期调度。它是如何做到的呢?下文尝试从源码角度探讨背后的隐秘。
public class LockSupport {
private LockSupport() {} // Cannot be instantiated.
private static void setBlocker(Thread t, Object arg) {
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}
private static final sun.misc.Unsafe UNSAFE;
private static final long parkBlockerOffset;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) { throw new Error(ex); }
}
}
上代码块是精简过后的相关源码,该类相关功能 API 如下:
粗略阅读了这四个方法,一头雾水,LockSupport 内部定义了大量的变量,多处使用了 Unsafe 类,想要读懂它, 要先打破它的铠甲,通读全类发现多处依赖了 Unsafe 这个静态类,这个类是什么作用呢,来探一探究竟。
Unsafe
什么是 Unsafe 类?
Java 是一种高级抽象语言,直接针对于系统底层操作调用方式较少,有些情况需要直接访问系统内存资源并自主管理资源,为了较高的安全性舍弃了指针概念,所以当某些情况需要提升Java运行效率增强计算机底层资源访问能力,所以产生了 Unsafe 类,用来替代部分指针角色。
由于 UNSAFE 可以直接操作计算机底层资源引起资源管理泄露问题,所以使用 Unsafe 时要严谨<对于 Unsafe 类的调用方式,本文一笔带过,可凭借兴趣自行查阅>。
Unsafe 类可以做什么呢?
Unsafe 类中提供的 API 大致作用分类:
内存操作、CAS、对象操作、系统相关、线程调度、数组操作、Class相关、内存屏障。
初步了解 Unsafe 这个魔法类后,发现它竟然提供了这么丰富的内容,那么再回头读懂 LockSupport 源码对一些变量的位移获取和设置就不会一头雾水了,可以做出如下解读:
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
// 获取当前线程对象parkBlocker字段的位移偏移量
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) { throw new Error(ex); }
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
// 通过unsafe设置互斥变量
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
// 清空互斥变量
setBlocker(t, null);
}
private static void setBlocker(Thread t, Object arg) {
// 当前线程对象的parkBlocker属性设置arg
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
park 和 unpark 中调用了 Unsafe 的 park 和 unpark,这两个方法被 native 修饰,说明调用了C实现的原生函数,不过核心在这两个方法内,他们内部做了什么呢,只能继续挖掘源码,去官网扒拉出原生函数源码。如下:
park源码逻辑:
void Parker::park(bool isAbsolute, jlong time) {
// 原子交换操作将计数器的值改为0,同时检查计数器的原值是否大于 0,即_counter>0,如果大于0<执行unpark将_counter改为1>则表示unpark先于park执行,本次park直接返回,否则往下执行
if (Atomic::xchg(0, &_counter) > 0) return;
Thread* thread = Thread::current();
assert(thread->is_Java_thread(), "Must be JavaThread");
JavaThread *jt = (JavaThread *)thread;
// 判断当前线程是否标记了中断,如果有中断则直接返回
if (Thread::is_interrupted(thread, false)) {
return;
}
timespec absTime;
if (time < 0 || (isAbsolute && time == 0) ) { // don't wait at all
return;
}
if (time > 0) {
unpackTime(&absTime, isAbsolute, time);
}
ThreadBlockInVM tbivm(jt);
// // 再次判断当前线程是否标记了中断,如果有中断则直接返回,否则通过pthread_mutex_trylock增加互斥锁<mutex>
if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
return;
}
int status ;
// 以上都不命中时,再次判断计数器是否执行过unpark
if (_counter > 0) { // no wait needed
// 如果执行过unpark,则将计数器重置
_counter = 0;
// 计数器重置后完成互斥锁释放
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
// Paranoia to ensure our locked and lock-free paths interact
// correctly with each other and Java-level accesses.
OrderAccess::fence();
return;
}
sigset_t oldsigs;
sigset_t* allowdebug_blocked = os::Linux::allowdebug_blocked_signals();
pthread_sigmask(SIG_BLOCK, allowdebug_blocked, &oldsigs);
OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);
jt->set_suspend_equivalent();
assert(_cur_index == -1, "invariant");
// 当无等待时长时,且未执行过unpark,调用pthread_cond_wait函数,查阅得该函数它会使当前线程加入操作系统的条件等待队列,同时释放 mutex 锁并使当前线程挂起
// 等待其他线程执行pthread_cond_signal唤醒当前
if (time == 0) {
_cur_index = REL_INDEX; // arbitrary choice when not timed
status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
} else {
_cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ;
if (status != 0 && WorkAroundNPTLTimedWaitHang) {
pthread_cond_destroy (&_cond[_cur_index]) ;
pthread_cond_init (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr());
}
}
_cur_index = -1;
assert_status(status == 0 || status == EINTR ||
status == ETIME || status == ETIMEDOUT,
status, "cond_timedwait");
#ifdef ASSERT
pthread_sigmask(SIG_SETMASK, &oldsigs, NULL);
#endif
_counter = 0 ;
status = pthread_mutex_unlock(_mutex) ;
assert_status(status == 0, status, "invariant") ;
OrderAccess::fence();
if (jt->handle_special_suspend_equivalent_condition()) {
jt->java_suspend_self();
}
}
unpark源码逻辑:
void Parker::unpark() {
int s, status ;
// 调用pthread_mutex_lock函数,给该线程加互斥锁,并通过下赋值操作将计数器赋值为1
status = pthread_mutex_lock(_mutex);
assert (status == 0, "invariant") ;
s = _counter;
_counter = 1;
if (s < 1) {
// 计数器小于1则当前线程被执行过park
if (_cur_index != -1) {
// 判断 Parker 对象所关联的线程是否被 park,如果是,则通过 pthread_mutex_signal 函数唤醒该线程,最后释放锁。
// pthread_cond_signal和pthread_cond_wait配套使用,当前线程调用unpark的pthread_cond_signal后,则唤醒操作系统中某个执行pthread_cond_wait的等待线程并设置_cur_index=-1
if (WorkAroundNPTLTimedWaitHang) {
status = pthread_cond_signal (&_cond[_cur_index]);
assert (status == 0, "invariant");
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant");
} else {
// must capture correct index before unlocking
int index = _cur_index;
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant");
status = pthread_cond_signal (&_cond[index]);
assert (status == 0, "invariant");
}
} else {
pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
}
} else {
pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
}
}
梳理一下park和unpark执行逻辑:
Unsafe 的 park 和 unpark 主要依靠系统的 pthread_cond_signal 和 pthread_cond_wait 对线程进行挂起和恢复,来达成调度中的阻塞和唤醒效果,值得一提的是,wait 和 notify 底层也是依附这两个函数进行线程挂起和恢复。
经过以上内容对源码的分析,已然对 LockSupport 有一个简单的认识,能够不通过 Java 锁来协调线程挂起和唤醒,还有神奇的 Unsafe 魔法类,此刻估计已经按奈不住跃跃欲试的心,那就快快把脑袋里的想法变成代码吧。
推荐阅读
招贤纳士
政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注