Something About Lock
锁是实现同步原语的基础,在Java层面,jdk为我们提供隐式锁(synchronize),以及基于volatile,final的显式锁(Unsafe->LockSupport->AQS),并在此基础上建立起了各种并发工具(java.util.concurrent).
在开始介绍Java中的锁机制之前,有必要先梳理一下并发领域的一些重要的概念,诸如:并行与并发,同步与异步,程序与进程,进程与线程,在真正理解这些概念之后,我们开始今天的主题:Lock In Java.
一 异常控制流
ECF(Exception Controller Flow),我们知道,系统中最终在干活的只有CPU,从系统启动开始,CPU就孜孜不倦的从内存中读取指令和数据并执行,对于CPU而言,程序就是一串串控制流.当然在系统中也存在一些异常控制流.
系统中的异常的类型:
类型 | 原因 | 返回行为 | 对应程序 |
---|---|---|---|
中断(interrupt) | 来自I/O设备的信号(键盘,网卡,磁盘,定时器芯片等) | 返回到下一条指令 | 中断处理程序由内核中的硬件驱动程序提供 (I/O) |
陷阱(trap) | 有意的异常(例如:系统调用) | 返回到下一条指令 | 陷阱处理程序(系统调用程序) |
故障(fault) | 潜在可恢复的错误(例如:缺页) | 可能返回当前指令 | 故障处理程序(虚拟内存) |
终止(abort) | 不可恢复的错误(例如:除零,内存访问违例) | 不会返回 | abort例程 |
异常在系统中各个层面的作用:
层次 | 异常在此层次的用途 |
---|---|
应用层 | 非本地跳转(try catch throw) |
OS与应用程序 | 陷阱异常即系统调用 |
OS内 | 通过时钟中断信号可以进行进程上下文切换(进程) |
硬件与OS | 通过中断来沟通 |
从这里我们可以看到,ECF是操作系统实现I/O,进程和虚拟内存的基本机制,下面是x86-64中关于以上概念的表述
二 逻辑控制流
在介绍进程前,我们先梳理下什么是程序,程序直观来讲就是一些可执行文件和数据的集合,例如Java程序的jar包,以及C程序的.so(动态库)或.a (静态库).
程序
下面列举一些我们遇见过的程序:
Java程序:HBase,Elasticsearch,Zookeeper,RocketMQ,JMeter,Log4j,Maven等
C/C++程序:Nginx,Redis,ZeroMQ,MongoDB,MySQL,JVM,Bash Shell,Vim,make指令,gcc等
平台 | JVM的Java语言开发平台 | |
---|---|---|
开发环境 | vim,gcc,glibc,binutils,make,gibc头文件,内核头文件 | idea,jvm,maven |
源文件 | .c | .java |
可执行文件 | .o | .class |
可执行文件的集合 | [.o] = .so动态库,.a 静态库 链接阶段使用 | jar |
软件包 | .rpm 软件包 | pom (Java似乎并没有rpm对应的软件安装包的概念) |
软件包存储位置 | usr/lib or usr/lib64 or /lib | maven的本地仓库 |
软件包管理 | apt-get and yum | maven |
发布 | 发布应用需要同时发布可执行程序以及依赖的函数库(分静态,动态) |
关于Java的实现
JSR(Java Specification Requests)(定义规范) -> RI (Reference Implementations)SourceCode -> compiler -> jdk(which include jre and jvm) -> JavaSE
进程
即运行的程序,是操作系统提供的一个抽象.系统中每个程序都运行在某个进程的上下文(context)中,上下文由程序正确运行所需的状态组成.例如:程序的代码和数据(放堆里),调用它的栈,PC(程序计数器),内核数据结构(进程表,页表,文件表).
上下文切换是一个重要的概念,与之相关联的又会引入调度(scheduling),权限指令(privileged instruction),内核模式与用户模式,系统调用(system call,由%rax保存调用号)与函数调用的概念.
这里我们需要理解什么是上下文,上下文如何切换,切换时机,谁来切换(内核中的调度器)等问题.
切换时机:
- 时间片用完(中断ECF)
- 进程阻塞(读文件时,wait调用)
- sleep系统调用(显式)
我们来看一个例子,read系统调用过程:
Linux中常用的系统调用
至此,我们终于可以开始介绍并发了
三 关于并发
计算机系统中的逻辑流有不同的形式,例如:异常处理程序(中断,系统调用),进程,信号处理程序,线程,应用程序(JVM进程),并发的web请求等.
一个逻辑流的执行在时间上与另一个流重叠,称为并发流(concurrent flow),这两个流称为并发地执行(并发描述的是现象,是真实存在的).
来看下面的例子,思考哪些是并发执行的?单核环境会有并发现象吗?
并发带来的问题
思考,如果两个线程并发执行,先后访问同一块内存区域,会有问题吗? (可见性和临界区问题) 接下来引入一些重要的概念
可见性问题
内存可见性,顺序一致性,数据一致性问题似乎都是描述的同一个问题,即线程通信问题,如何设计线程通信的协议以解决这些问题? 这里我们以Java中的JMM来说明,首先我们来回顾下现代计算机的硬件结构:
下面这张是JMM内存模型:
从中可以看到,Java线程的本地内存其实包含: CPU缓存,寄存器,写缓冲区和其他硬件与编译器的优化.
Java中的共享变量放在堆里,这里的共享变量指: 实例域(即对象的字段),静态域(Class对象的字段),数组元素.
JMM(Java Memory Model)可以理解为Java线程的通信协议,在Java中通过抽象出JMM内存模型,并给出happen-before 协议 只要我们遵守这些协议,就可以避免内存可见性问题,就像在顺序一致性模型中运行一样(JMM给我们的保证).
线程在JMM模型上通信
这里还有一个问题,即JMM内存模型是如何实现的? 在回答这个问题前需要了解一个事实,即在计算机中,软件技术和硬件技术有一个共同的目标,在不改变程序执行结果前提下(as_if_serial),尽可能提高并行度(指令流水线,编译器优化,内存系统优化).
首先我们需要考虑的问题是:
为什么需要重排序
1) 指令流水线与编译器优化
b = a * 5
v = *b
c = a + 3
由于1与3可并发执行,而2之b无法随即获得(有数据依赖),因此可以先计算乘法1与加法3,再执行2
乱序执行使用其他“可以执行”的指令来填补了时间的空隙,然后再在结束时重新排序运算结果来实现指令的顺序执行中的运行结果。指令在原始计算机代码中的顺序被称为程序顺序,在处理器中他们被按照数据顺序中被处理,这种顺序中,数据,运算符,在计算机寄存器中变得可以获取。一般来说乱序执行需要复杂的电路来实现转换一种顺序到另一种顺序并且维护在输出时的逻辑顺序;而处理器本身就好像是随机执行的样子。OoOE(out of order execution)Wiki
2) 内存系统优化
内存系统利用程序执行时的时间局部性,空间局部性的特点,增加缓存导致的虚假重排序
点击查看代码
static class MemoryReorderExample {
private int a,b,x,y;
public void processA() {
a = 1; //A1
x= b; //A2
}
public void processB() {
b = 2; //B1
y = a; //B2
}
假设有这么一个执行顺序,线程A执行processA,之后线程B执行processB,可能有一种情况是x=0,y=0,因为 a=1可以只写到本地缓存(写缓冲,cpu寄存器,cpu缓存等), b=2也是
这样就像是执行 A2 A1 , B2 B1一样,即指令被重排了(虚假的重排)
重排序对并发程序正确性的影响
点击查看代码
static class ReorderExample{
private boolean flag = false;
int a = 0;
public void write(){
a = 1; // Step1
flag = true;// Step2
}
public void read(){
if(flag){
int i = a * a;
}
}
}
假设有这么一个执行顺序,线程一执行write,之后线程二执行read,线程二不一定能看到i=1,因为 Step1 和Step2没有数据依赖,系统可以进行重排序
Java如何解决可见性问题
CPU为上层应用提供了内存屏障指令来禁止重排序,JMM通过封装这些指令并适配不同的平台来实现happen-before协议(关于实现的具体细节可以参考<<Java并发编程艺术>>的第三章)
不同CPU提供的内存屏障指令:
happen-before协议
在JSR-133中,happens-before关系定义如下:
如果一个操作happens-before另一个操作,那么意味着第一个操作的结果对第二个操作可见,而且第一个操作的执行顺序将排在第二个操作的前面。
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的结果,与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)
点击查看代码
1 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
2 监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3 volatile规则:对一个volatile变量的写,happens-before于任意后续对一个volatile变量的读。
4 传递性:若果A happens-before B,B happens-before C,那么A happens-before C。
5 线程启动规则:Thread对象的start()方法,happens-before于这个线程的任意后续操作。
6 线程终止规则:线程中的任意操作,happens-before于该线程的终止监测。我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
7 线程中断操作:对线程interrupt()方法的调用,happens-before于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到线程是否有中断发生。
8 对象终结规则:一个对象的初始化完成,happens-before于这个对象的finalize()方法的开始。
happens-before与JMM的关系:
临界区问题
在介绍临界区问题时需要引入下面几个重要的概念
竞争冒险(race hazard)现象:程序的正确性依赖于特定的执行顺序.
(注:这给系统带来了不确定性,根据墨菲定律,一件事情有可能发生的话它就会发生,可能是现在或以后,不确定性对于系统来讲其实是很危险的,线程的调度我们是无法控制的(程序员不该假设并发执行的线程以某个特定的顺序调度),但是调度 后是否执行,需不需要等待我们是可以控制的,这个就是同步(synchronization),如何实现就是利用同步原语(mutex,cond),这是前人在实践的基础上总结出来的,因为我们工作其实很少会涉及到同步问题,因为很容易使系统陷入不确定性的状态,对于系统来讲可能是致命的,这个问题不好处理,需要封装,出问题也很难复现,从而很难修复,所以写多线程应用程序对于我们来说是一个挑战)
互斥访问(mutual exclusion):任意时刻仅允许一个线程访问.
临界区(critical section): 保证互斥访问共享资源(Java中的共享资源有哪些?)的代码区域,在临界区内,线程可以安全的对共享资源操作,线程安全.
所谓临界区问题: 指多个线程并发的执行临界区代码,会引起竞争冒险现象,我们需要针对它们设计一套满足以下条件的协议(或算法)来保证程序的正确性.
操作系统中基于锁提供了一些同步原语(synchronization primitive),比如(基于原子CAS的自旋锁,基于原子FAA的排号锁(ticket lock),条件变量(condition varibale),信号量(semaphore),读写锁等,对应的系统调pthread_mutex,pthread_cond(信号量基于这两个实现),封装在gclib包下.
Java中提供了synchronze,final,volatile等同步原语,本次不介绍其具体的实现,只介绍操作系统的同步原语的实现,然后使用Java封装的方法来实现一份Java版的同步原语.
OS同步原语伪代码
// CAS
int atomic_CAS(int *addr,int expected, int new_value) {
int tmp = *addr;
if(*addr == expected) {
*addr = new_value;
}
return tmp;
}
// FAA
int atomic_FAA(volatile int *addr, int add_value) {
int tmp = *addr;
*addr = tmp + add_value;
return tmp;
}
// SpinLock
void lock_init(int *lock) {
*lock = 0;
}
void lock(int *lock) {
while(atomic_CAS(lock,0,1) != 0)
; //忙等
}
void unlock(int *lock) {
*lock = 0;
}
// 互斥锁其实是不公平的,接下来介绍排号锁
// 排号自旋锁
struct lock {
volatile int owner;
volatile int next;
};
void lock_init(struct lock *lock) {
// 初始化排号锁
lock->owner = 0;
lock->next = 0;
}
void lock(struct lock *lock) {
// 拿到自己的序号
volatile int my_ticket = atomic_FAA(&lock->next,1);
while(lock->owner != my_ticket) {
; // 忙等
}
}
void unlock(struct lock*lock) {
//传递给下一位竞争者
lock->owner++;
}
// 循环等待是无意义的,不仅染费CPU资源,还增加系统能耗,由于OS提供了挂起阻塞原语,
// 我们可以利用此机制来解决这个问题-条件变量
// 条件变量提供两个接口,cond_wait,cond_signal,分别用于挂起当前线程和唤醒的呢带在该条件变量的线程,
// 必须搭配一个互斥锁使用,用于保护对条件的判断和修改.
struct cond
{
struct thread *wait_list;
};
void cond_wait(struct cond*cond, struct lock*mutex) {
list_append(cond->wait_list,thread_self());
atomic_block_unlock(mutex); //原子挂起并放锁
lock(mutex);
}
void cond_sign(struct cond* cond) {
if(!list_empty(cond->wait_list)) {
wakeup(list_remove(cond->wait_list));
}
}
void cond_broadcast(struct cond*cond) {
while(!list_empty(cond->wait_list)) {
wakeup(list_remove(cond->wait_list));
}
}
// 信号量的定义
void wait(int *s) {
while(*s <= 0) {
*s = *s -1;
}
}
void signal(int *s) {
*s = *s+1;
}
// 信号量的实现
struct sem
{
/**
* 当没有线程等待时,value为正数或零,表示剩余的资源数
* 当有线程等待时,为负数,其绝对值表示正在等待获取资源的线程数量
*
*/
int value;
/**
* 有线程等待时的可用资源数,只能为正数或零,即应该被唤醒的线程数
*
*/
int wakeup;
struct lock sem_lock;
struct cond sem_cond;
};
void wait(struct sem*s) {
lock(&s->sem_lock);
s->value--;
if(s->value < 0) {
do {
cond_wait(&s->sem_cond,&s->sem_lock);
}while(s->wakeup == 0);
s->wakeup--;
}
unlock(&s->sem_lock);
}
void signal(struct sem * s) {
lock(&s->sem_lock);
s->value++;
if(s->value <=0) {
s->wakeup++;
cond_sign(&s->sem_cond);
}
unlock(&s->sem_lock);
}
// 利用信号量(PV原语)解决生产者消费者问题
struct sem empty_slot;
struct sem filled_slot;
void producer(void) {
int new_msg;
while(true) {
new_msg = produce_new();
wait(&empty_slot);// P操作(Proberen) 检验
buffer_add_safe(new_msg);
signal(&filled_slot); //V操作 Verhogen 自增
}
}
void consumer(void) {
int cur_msg;
while(true) {
wait(&filled_slot); //P
cur_msg = buffer_remove_safe();
signal(&empty_slot);// V
consume_msg(cur_msg);
}
}
四 Java中的锁机制
在学习和理解了以上这些概念后,我们可以使用Java提供的Unsafe类,LockSupport类实现一些同步原语(当然只是玩具).
根据伪代码实现同步原语
1 Java互斥锁的一种简单实现
Unsafe操作
@Test
public void UnSafeTest() throws Exception{
// 1 获取Unsafe对象
Unsafe unsafe = UnsafeFactory.getUnsafe();
// 2.获取aOffset
SimpleInteger simpleInteger = new SimpleInteger(1);
Field filed = simpleInteger.getClass().getDeclaredField("value");
long aOffset = unsafe.objectFieldOffset(filed);
// 3. 操作内存
unsafe.putInt(simpleInteger,aOffset,100);
Assert.assertEquals(simpleInteger.getValue(),100);
}
UnsafeFactory.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @author YaXonZen
* @since 2022/3/27 20:12
**/
public class UnsafeFactory {
private volatile static Unsafe unsafe;
public static Unsafe getUnsafe() throws Exception{
if(unsafe == null) {
synchronized (UnsafeFactory.class) {
if(unsafe == null) {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe)theUnsafe.get(null);
}
}
}
return unsafe;
}
}
UnFairMutex.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent;
import sun.misc.Unsafe;
/** 共享资源 注意内存可见性
* 不公平的互斥锁实现
* @author YaXonZen
* @since 2022/3/27 20:21
**/
public class UnFairMutex {
/**
* 0表示该锁未被获取 1表示该锁已被获取
*/
private volatile int value;
/**
* 用于直接处理内存
*/
private static final Unsafe UNSAFE;
/**
* value字段的内存编译量 UNSAFE入参要用
*/
private static final long VALUE_OFFSET;
static {
try {
UNSAFE = UnsafeFactory.getUnsafe();
VALUE_OFFSET = UNSAFE.objectFieldOffset
(UnFairMutex.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public UnFairMutex() {
this.value = 0;
}
/**
* 自旋锁: while + CAS 忙等
* 思考: 是否正确处理临界区问题:
* 互斥访问 ?
* 空闲让进 ?
* 有限等待 ?
* 不能保证有限等待,即有的线程可能会饿死,存在这种情况:异构环境,大核频率高于小核,
* 小核上运行的线程可能饿死,即自旋锁是不公平的 但由于实现简单,在竞争程度低的情况下很有效
*/
public void lock() {
// 尝试获取锁(将value设置为1)
while (UNSAFE.getAndSetInt(this, VALUE_OFFSET,1) != 0) {
// 忙等,实现简单,在竞争程度低时很有用,
// 但效率低:循环等待毫无意义,不仅浪费CPU资源还增加了系统的能耗
// 思考: how to solve?
}
}
public void unlock() {
value = 0;
}
@Override
public String toString() {
return "Mutex{" +
"value=" + value +
'}';
}
}
FairMutex.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent;
import sun.misc.Unsafe;
/** 共享资源 注意内存可见性 思考:如何验证这是公平的?
* 公平锁 by FAA(Fetch And Add)
* @author YaXonZen
* @since 2022/3/27 21:17
**/
public class FairMutex {
/**
* 当前号码
*/
private volatile int owner;
/**
* 下一个号码
*/
private volatile int next;
/**
* 用于直接处理内存
*/
private static final Unsafe UNSAFE;
/**
* next字段的内存编译量 UNSAFE入参要用
*/
private static final long NEXT_OFFSET;
static {
try {
UNSAFE = UnsafeFactory.getUnsafe();
NEXT_OFFSET = UNSAFE.objectFieldOffset
(FairMutex.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
public FairMutex() {
// 初始号码为0
this.owner = 0;
this.next = 0;
}
public void lock() {
// 取号 同时原子分配下一个号码
int myNumber = UNSAFE.getAndAddInt(this, NEXT_OFFSET, 1);
// 排不到号就等待
while(myNumber != owner) {
// 忙等
}
}
public void unlock() {
// 通知其他线程 即 下一个号 这里由于是排队,不会有并发调用unlock的情况,所以可以直接++
owner++;
}
@Override
public String toString() {
return "MutexFair{" +
"owner=" + owner +
", next=" + next +
'}';
}
}
ReentrantMutex.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent;
import sun.misc.Unsafe;
/**
* 共享资源 注意内存可见性
*
* @author YaXonZen
* @since 2022/3/27 21:55
**/
public class ReentrantMutex {
/**
* 0表示该锁为被获取 否则表示获取锁的次数
*/
private volatile int value;
/**
* 该锁的持有者
*/
private volatile Thread owner;
/**
* 用于直接处理内存
*/
private static final Unsafe UNSAFE;
/**
* value字段在ReenterMutex对象上的内存偏移量 UNSAFE入参要用
*/
private static final long VALUE_OFFSET;
/**
* 用于原子操作
*/
private final Object obj1 = new Object();
private final Object obj2 = new Object();
private final Object obj3 = new Object();
static {
try {
UNSAFE = UnsafeFactory.getUnsafe();
VALUE_OFFSET = UNSAFE.objectFieldOffset
(UnFairMutex.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
public ReentrantMutex() {
this.value = 0;
}
public void lock() {
Thread currentThread = Thread.currentThread();
// 第一次进入锁
if (owner == null) {
// 可能会有多个线程进入 占有锁及设置owner需原子操作
synchronized (obj1) {
owner = currentThread;
this.value++;
}
// 非第一次进入
} else if (owner == currentThread) {
// 可以直接++ 无需锁保护,因为这里不会有两个线程进来执行
this.value++;
// 已有其他线程获得锁
} else {
// 获取锁并设置owner需要原子操作
synchronized (obj2) {
// 按照之前的加锁逻辑枷锁
while (UNSAFE.getAndSetInt(this, VALUE_OFFSET, 1) != 0) {
}
// 此时获取到锁 设置owner
owner = currentThread;
}
}
}
public void unlock() throws IllegalAccessException {
if (owner == null) {
throw new IllegalStateException("请先执行lock再执行unlock");
} else if (owner != Thread.currentThread()) {
throw new IllegalAccessException("无权放锁");
}
// 放锁和设置owner需要原子操作
synchronized (obj3) {
this.value--;
// 如果为0 表示该锁已被释放
if (this.value == 0) {
this.owner = null;
}
}
}
}
AtomicInteger.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent;
/**
* @author YaXonZen
* @since 2022/3/27 20:33
**/
public class AtomicInteger {
private int value;
private final UnFairMutex unFairMutex;
public AtomicInteger(int i) {
this.value = i;
this.unFairMutex = new UnFairMutex();
}
public void increment() {
unFairMutex.lock();
value++;
unFairMutex.unlock();
}
public int getValue() {
return value;
}
public void deadLock() {
unFairMutex.lock();
value++;
// 再次尝试拿锁,会直接死锁,思考为什么?
unFairMutex.lock();
value++;
unFairMutex.unlock();
}
}
AtomicLong
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent;
/**
* @author YaXonZen
* @since 2022/3/27 21:30
**/
public class AtomicLong {
private long value;
private final FairMutex fairMutex;
public AtomicLong(long i) {
this.value = i;
this.fairMutex = new FairMutex();
}
public void increment() {
fairMutex.lock();
value++;
fairMutex.unlock();
}
public long getValue() {
return value;
}
}
AtomicInt.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent;
/**
* @author YaXonZen
* @since 2022/3/27 22:39
**/
public class AtomicInt {
private int value;
private final ReentrantMutex reentrantMutex;
public AtomicInt(int i) {
this.value = i;
this.reentrantMutex = new ReentrantMutex();
}
public void increment() {
reentrantMutex.lock();
value++;
try {
reentrantMutex.unlock();
}catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public int getValue() {
return value;
}
public void testIfDeadLock() {
reentrantMutex.lock();
value++;
// 再次尝试拿锁,会直接死锁,思考为什么?
reentrantMutex.lock();
value++;
try {
reentrantMutex.unlock();
value--;
reentrantMutex.unlock();
}catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
2 使用互斥锁以及LockSupport实现 Conditon
Condition.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent.cond;
import concurrent.lock.FairMutex;
import java.util.LinkedList;
import java.util.concurrent.locks.LockSupport;
/**
* 条件变量需要与一个互斥锁配合,该互斥锁用于保护对条件的判断和修改
* 使用条件变量时注意识别:
* 1.变量
* 2.保护变量的锁
* 3.条件变量(条件即 什么条件下把线程放到等待队列)
* <p>
* 这三者会一起出现
* 等待队列: 条件变量为变量提供的一个 waitThreadList
* <p>
* Condition本质就是一个等待队列
*
* @author: YaXonZen
* @description: 条件变量
* @date: 23:21 2022/3/28
**/
public class Condition {
/**
* 用于原子放锁和挂起
*/
private final Object obj1 = new Object();
/**
* 等待队列,等待在该条件变量的进程集合
*/
private final LinkedList<Thread> waitThreadList = new LinkedList<>();
/**
* 挂起当前线程
*
* @param fairMutex 互斥锁 用于保护对条件的判断和修改
*/
public void condWait(FairMutex fairMutex) {
waitThreadList.add(Thread.currentThread());
/*
* 这里主要为了保证 挂起和放锁的原子操作,由于没找到 Java的实现所以用了一个obj.
* 如果先放锁再挂起当前线程,则有可能存在其他线程在间隙调用cond_signal操作.
* 由于当前线程还未挂起,cond_signal操作无法唤醒该线程,也无法阻止该线程挂起,
* 最终导致该线程挂起后无法被唤醒.如果我们先挂起后放锁,被挂起的线程无法成功放锁.
* 伪码是使用 atomic_block_unlock操作的,这个操作要由os辅助完成
*/
// 思考: 这里Java线程明明不会并发的进来执行,为什么要加锁,为什么要保证原子性?
/* 这个问题很有意思,正常来说我们加锁主要为了原子操作,所以就想着这块地方在Java层面并不会有多个线程并发执行的情况为什么要加锁,
* 但其实我们忽略了 这段代码的内容是挂起操作,挂起与其他os线程的通知操作有关,就相当于还有其他os线程执行会受这部分代码的影响
*/
// 原子挂起该线程并放锁 (linux 中的futex实现)
synchronized (obj1) {
fairMutex.unlock();
LockSupport.park();
}
//唤醒和正在执行fairMutex之间可能不会马上被调度,可以其他生产者线程消费 emptySlot,所以外层需要使用while (emptySlot == 0)
// fairMutex.unlock();
// 消费 emptySlot;
// fairMutex.lock();
// 我们该竞争锁实现简单,仅是自旋,正常来说唤醒后需要放到同步队列,再去竞争锁
fairMutex.lock();
}
public void condSignal() {
// 唤醒等待队列中的一个线程
if (!waitThreadList.isEmpty()) {
Thread remove = waitThreadList.remove();
LockSupport.unpark(remove);
}
}
public void condSignalAll() {
while (!waitThreadList.isEmpty()) {
Thread remove = waitThreadList.remove();
LockSupport.unpark(remove);
}
}
@Override
public String toString() {
return "Condition{" +
"waitThreadList=" + waitThreadList +
", obj1=" + obj1 +
'}';
}
}
3 使用AQS实现互斥锁
Mutex.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent.lock;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
/**
* 使用jdk提供的AQS实现
*
* @author YaXonZen
* @since 2022/4/1 15:16
**/
public class Mutex {
private Mutex() {
}
private static final Sync REAL_MUTEX = new Sync();
/**
* 互斥锁, status=1 表示已占有锁, status=0 表示已释放
* AQS提供一个status用于线程通信
* 提供一个同步队列用于管理被阻塞的线程
*/
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean isHeldExclusively() {
return super.getState() == 1;
}
/**
* 尝试获取锁,与ReentrantMutex比较
*
* @param arg unknown
* @return op结果
*/
@Override
protected boolean tryAcquire(int arg) {
// 希望oldValue=0,则set value=1
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new UnsupportedOperationException();
}
// 更新 owner和statu
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
public static void lock() {
REAL_MUTEX.acquire(1);
}
public static void unlock() {
REAL_MUTEX.release(1);
}
}
4 生产者消费者场景模拟
Food.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent.cond;
import concurrent.util.ObjectWithAttr;
/**
* @author YaXonZen
* @since 2022/3/31 8:27
**/
public class Food extends ObjectWithAttr {
public Food(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static Food newInstance(long millis) {
if (millis < 0) {
millis = 200;
}
return new Food(millis);
}
@Override
public String getName() {
return "Food";
}
@Override
public String toString() {
return "Food{" +
"name='" + name + '\'' +
'}';
}
}
FoodContainer.java
/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent.cond;
import concurrent.lock.UnFairMutex;
import org.apache.log4j.Logger;
/**
* @author YaXonZen
* @since 2022/3/31 8:35
**/
public class FoodContainer {
/**
* 固定容器的大小
*/
private final int size;
/**
* 食物放在这个下标
*/
private int writeCnt = 0;
/**
* 从这个下标拿食物
*/
private int readCnt = 0;
/**
* 用于保护 共享资源: writeCnt,readCnt的 ++操作
*/
private final UnFairMutex unFairMutex = new UnFairMutex();
private final Food[] foods;
Logger logger = Logger.getLogger(FoodContainer.class);
public FoodContainer(int size) {
this.size = size;
foods = new Food[size];
}
public void add(Food food) {
// 使用锁保护 共享资源(由多个生产者共享) writeCnt
unFairMutex.lock();
foods[writeCnt] = food;
logger.info("生产food:" + food);
// 循环数组
writeCnt = (writeCnt + 1) % size;
unFairMutex.unlock();
}
public Food get(long millis) {
// 使用锁保护 共享资源(由多个消费者共享) readCnt
unFairMutex.lock();
Food food = foods[readCnt];
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 循环数组
readCnt = (readCnt + 1) % size;
unFairMutex.unlock();
return food;
}
public int getSize() {
return size;
}
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < size; i++) {
buffer.append(foods[i]).append(",");
}
return buffer.substring(0, buffer.capacity() - 1);
}
}
Restaurant.java
**/*
* Copyright (c) 2022. this file belong to アオギリの树
*/
package concurrent.cond;
import concurrent.lock.FairMutex;
import concurrent.util.ExecutorUtil;
import concurrent.util.ThreadUtil;
import org.apache.log4j.Logger;
import java.util.Random;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author YaXonZen
* @since 2022/3/31 9:11
**/
public class Restaurant {
/**
* 厨师团队 负责做饭
*/
private final ThreadPoolExecutor cookTeam;
/**
* 食客团队 负责干饭
*/
private final ThreadPoolExecutor consumerTeam;
/**
* 老板 负责监督
*/
private final ThreadPoolExecutor boss;
/**
* 饭店营业时长
*/
private final long workMillis;
Logger logger = Logger.getLogger(Restaurant.class);
private static volatile Restaurant instance;
private Restaurant(int cookNum, int consumerNum, int foodsSize, long workMillis) {
this.workMillis = workMillis;
foodContainer = new FoodContainer(foodsSize);
emptySlot = foodContainer.getSize();
// 初始化 cookNum 个 厨师
cookTeam = ExecutorUtil.getExecutor("cookTeam", cookNum);
// 初始化 consumerNum 个食客
consumerTeam = ExecutorUtil.getExecutor("consumerTeam", consumerNum);
boss = ExecutorUtil.getExecutor("boss", 1);
}
/**
* 食物容器
*/
private final FoodContainer foodContainer;
/**
* 容器空槽个数
*/
private int emptySlot;
/**
* 公平锁,用于保护 emptySlot的判断和修改
*/
private final FairMutex emptySlotLock = new FairMutex();
/**
* 空槽条件变量, 提供一个等待队列给emptySlot
*/
private final Condition emptyCond = new Condition();
/**
* 满槽个数
*/
private int filledSlot = 0;
/**
* 公平锁,用于保护 filledSlot的判断和修改
*/
private final FairMutex filledSlotLock = new FairMutex();
/**
* 满槽条件变量, 提供一个等待队列给filledSlot
*/
private final Condition filledCond = new Condition();
public static Restaurant getInstance(int cookNum, int consumerNum,
int foodsSize, long workMillis) {
if (instance == null) {
synchronized (Restaurant.class) {
if (instance == null) {
instance = new Restaurant(cookNum, consumerNum, foodsSize, workMillis);
}
}
}
return instance;
}
public void open(long consumerMillis, long cookMillis) {
// 老板开始监督任务情况 workMillis后发送关闭信号
SuperviseJob superviseJob = new SuperviseJob(Thread.currentThread());
boss.execute(superviseJob);
// 天气多云转晴,适合做饭
try {
do {
// 厨师 做饭
cookTeam.submit(new CookJob(cookMillis));
// 间隔0~200ms过来吃饭 防止 任务过多
ThreadUtil.sleep(new Random(20).nextInt(200));
// 食客 干饭
consumerTeam.submit(new ConsumerJob(consumerMillis));
} while (!Thread.currentThread().isInterrupted());
} finally {
cookTeam.shutdown();
consumerTeam.shutdown();
// 注意boss也要 shutdown 否则主线程不会关闭
boss.shutdown();
}
logger.info("cookCompletedTaskNum: " + cookTeam.getCompletedTaskCount());
logger.info("consumerCompletedTaskNum: " + consumerTeam.getCompletedTaskCount());
}
/**
* 下饭任务 生产者线程
*/
class CookJob implements Runnable {
private final long cookMillis;
public CookJob(long cookMillis) {
this.cookMillis = cookMillis;
}
@Override
public void run() {
Food food = Food.newInstance(cookMillis);
// emptySlotLock 保护 共享资源emptySlot(被ProdConsumer线程共享) 小思考:为什么Java规定,wait操作要放到 synchronize代码块中?
emptySlotLock.lock();
// 如果没有空位置,就等待 这里使用while而不是if,与 double check 思想类似 见UnsafeFactory
while (emptySlot == 0) {
// 一个条件变量需要配合一个锁来使用
// 注意识别: 变量(emptySlot),保护变量的锁(emptySlotLock),条件变量(条件即 什么条件下把线程放到等待队列) 这三者会一起出现
emptyCond.condWait(emptySlotLock);
}
emptySlot--;
emptySlotLock.unlock();
foodContainer.add(food);
// filledSlotLock 保护filledSlot共享资源(被ProdConsumer线程共享)
filledSlotLock.lock();
filledSlot++;
// 新增一个满槽, 唤醒 消费者线程
filledCond.condSignal();
filledSlotLock.unlock();
}
}
/**
* 干饭任务 消费者线程
*/
class ConsumerJob implements Runnable {
private final long consumerMillis;
public ConsumerJob(long consumerMillis) {
this.consumerMillis = consumerMillis;
}
@Override
public void run() {
filledSlotLock.lock();
while (filledSlot == 0) {
filledCond.condWait(filledSlotLock);
}
filledSlot--;
filledSlotLock.unlock();
Food food = foodContainer.get(consumerMillis);
logger.info("消费food:" + food);
emptySlotLock.lock();
emptySlot++;
emptyCond.condSignal();
emptySlotLock.unlock();
}
}
/**
* 监督任务, 在workMillis 后给 被监督者manageThread 发送 中断信号
*/
class SuperviseJob implements Runnable {
/**
* 正在打工的线程
*/
private final Thread workingThread;
public SuperviseJob(Thread workingThread) {
this.workingThread = workingThread;
}
@Override
public void run() {
ThreadUtil.sleep(workMillis);
workingThread.interrupt();
}
}
@Override
public String toString() {
return "Restaurant{" +
", workMillis=" + workMillis +
'}';
}
public static void main(String[] args) {
Restaurant.getInstance(1, 1, 10, 5000)
.open(10, 20);
}
}
Java中显式锁与隐式锁
我们通过互斥锁解决了临界区问题,通过条件变量优化了忙等问题,但其实还有可优化的地方,例如我们调用 StringBuffer.append时,它是线程安全的类,每个方法都用synchronize封装了,在jdk1.6前使用每次都要加互斥锁(即重锁,mutex+cond),效率较低.
为了优化在后面的版本中实现了锁升级,无锁->偏向锁(reentantLock)->轻量级锁(while+CAS)→重量级锁(mutex+cond),可以参照前面的实现来理解(具体细节可以参考<<Java并发编程艺术>>第二章)
我们这里使用的是Unsafe去操作的,其实里面调用的是由JVM封装的接口,里面实际用到的就是操作系统提供的mutex+cond同步原语(ParkEvent are used for Java-level "monitor"synchronization).
五 参考资料
1 <<深入理解计算机系统>>
2 <<现代操作系统原理与实现>> (银杏封面)
3 <<Java并发编程艺术>>
4 Thread.interrupt()相关源码分析
5 LockSupport原理分析
6 好大学慕课
六 总结
本篇主要梳理了并发领域的一些重要概念的出场顺序,关于Java中的同步原语(synchronize,final,volatile),信号量,AQS,读写锁等的具体实现并没有深入探讨,相信在学完本篇文章后再开始后续的学习会顺畅许多:-)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器