Java与C语言中的锁
C
嵌入式汇编的语法格式是:
asm(code
: output operand list
: input operand list
: clobber list)
__asm__是GCC关键字asm的宏定义
寄存器其添加%,例如%0,就是0号寄存器
b,w,l分别表示字节,字,双字
output operand list 和 input operand list是c代码和嵌入式汇编代码的接口,clobber list描述了汇编代码对寄存器的修改情况
volatile关键字
易变性
被volatile修饰的变量被认为是易变的,CPU在读取该类型的变量时不能直接使用寄存器中的值,而是需要重新从主内存中读取。在对该类型的变量做出修改后,也需要立即同步会主内存。
其原理是在编译后的汇编代码中,会多出两条内存读写的指令。
不可优化性
被volatile修饰的变量,在编译阶段不会被编译器优化掉。例如替换成常量等。
顺序性
被volatile修饰的变量,在编译阶段保证不会被执行指令重排序。
指令重排序的含义是指,在保证函数输出不变的前提下,编译器会调整指令的前后顺序,已达到最优的执行速度。
这里的顺序性是多个volatile类型的变量之间保证的,在一个普通变量和一个volatile变量之间的编译顺序是不被保证的。
原子操作
原子操作就是保证指令以原子的方式执行(执行过程不被打断)。
那么操作系统是怎么保证原子操作的呢?
- 大部分原子操作的实现就是将变量的读取和修改的行为包含在一个单步执行中,更简单的说就是CPU提供的指令集中支持单指令的变量修改。
- 如果不支持单独修改的话,就为单步执行提供了锁内存总线的指令。
内核提供了两组原子操作接口— 一组针对整数操作,另一组针对单独的位操作。
整数操作
atomic_t结构体定义如下:
// goldfish/include/linux/types.h
typedef struct {
int counter;
} atomic_t;
atomic_t使用很简单:
atomic_t v;//声明
atomic_t u = ATOMIC_INIT(0);//定义u,并初始化为0
atomic_set(&v, 4);//设置v的值为4
atomic_add(2, &v);//v=v+2=6
atomic_inc(&v);//v=v+1=7
int ret = atomic_read(&v);//转换为整型
int atomic_dec_and_test(atomic_t *v);//执行v减一并检查结果是否为0。如果结果为0就返回真,否则返回假
原子操作的实现依赖不同架构的指令集,在x86上atomic_add的实现为:
//通过内联汇编指令实现
#define LOCK_PREFIX "\n\tlock; "
static __always_inline void atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i));
}
在多处理器环境下,通过再addl指令前添加lock指令,锁住对应内存,防止在多处理系统或多线程竞争的环境下互斥使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。
位操作
原子位操作是对普通指针进行的操作,不想原子整型对应atomic_t,这里没有特殊的数据类型。
unsigned long word = 0;
set_bit(0, &word);//第0位被设置为1
set_bit(1, &word);//第1位被设置为1
printk("%ul\n", word);//因为第0位和第1位都是1,所以打印出3
clear_bit(1, &word);//清空第1位,即变成0
change_bit(0, &word);//反转第0位,及变为0
//设置第0位,并返回设置之前的值
if(test_and_set_bit(0, &word)) {
//因为之前第0位为0,所以if条件永远为假
}
自旋锁 pin lock
自旋锁,顾名思义,当获取不到锁时,会一直在原地打转,不断尝试去获得锁,直到成功。
自旋锁的使用方法:
//定义一个自旋锁
spinlock_t lock;
spin_lock_init(&lock);
//获取自旋锁,保护临界区
spin_lock(&lock);
...
//释放锁
spin_unlock(&lock);
自旋锁结构体定义如下:
// goldfish/include/linux/spinlock_types.h
typedef struct spinlock {
union {
struct raw_spinlock rlock;
};
} spinlock_t;
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
} raw_spinlock_t;
typedef struct {
volatile unsigned int lock;
} arch_spinlock_t;
获取自旋锁spin_lock函数的实现如下:
// goldfish/include/linux/spinlock.h
static __always_inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
经过一系列调用最终调用函数为:
// goldfish/include/linux/spinlock_api_smp.h
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
// 禁止CPU抢占,同时添加内存栅栏,防止指令重排序
//CPU抢占控制由thread_info结构体中的preempt_count整型变量控制,0可抢占,大于0不可抢占
preempt_disable();
//debug时执行
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
//真正的加锁操作do_raw_spin_lock
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
//goldfish/include/linux/spinlock.h
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
__acquire(lock);
//该函数实现要区分不同的CPU架构
arch_spin_lock(&lock->raw_lock);
}
在ia64架构上的实现如下:
//arch/ia64/include/asm/spinlock.h
static __always_inline void arch_spin_lock(arch_spinlock_t *lock)
{
__ticket_spin_lock(lock);
}
static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)
{
int *p = (int *)&lock->lock, ticket, serve;
ticket = ia64_fetchadd(1, p, acq);
if (!(((ticket >> TICKET_SHIFT) ^ ticket) & TICKET_MASK))
return;
ia64_invala();
for (;;) {
asm volatile ("ld4.c.nc %0=[%1]" : "=r"(serve) : "r"(p) : "memory");
if (!(((serve >> TICKET_SHIFT) ^ ticket) & TICKET_MASK))
return;
cpu_relax();
}
}
arm64上的实现为:
//arch/arm64/include/asm/spinlock_types.h
typedef struct {
#ifdef __AARCH64EB__ /* 大端字节序(高位存放在低地址) */
u16 next;
u16 owner;
#else /* 小端字节序(低位存放在低地址) */
u16 owner;
u16 next;
#endif
} __aligned(4) arch_spinlock_t;
//arch/arm64/include/asm/spinlock.h
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned int tmp;
arch_spinlock_t lockval, newval;
asm volatile(
/* Atomically increment the next ticket. */
ARM64_LSE_ATOMIC_INSN(
/* prefetch memory,将参数*lock代表的32位数据写到3号寄存器中 */
" prfm pstl1strm, %3\n"
/* 独占加载3号寄存器,到w0寄存器中*/
"1: ldaxr %w0, %3\n"
/* 将w0寄存器的值(next值)加上(1 << 16)后存储到w1存储器*/
" add %w1, %w0, %w5\n"
/* 尝试将加1后的值保存到为3号寄存器中,如果写入失败,向w2写入非0,如果写入成功向w2写入0。*/
" stxr %w2, %w1, %3\n"
/*compare and branch on Non-Zero,判断是w2是不是非0,如果是非0的则跳转到1号执行分支,直到加1成功。*/
" cbnz %w2, 1b\n",
/* 将w5中的数据写到w2寄存器中 */
" mov %w2, %w5\n"
/* 0号寄存器与3号寄存器的原子相加存到2号寄存器中*/
" ldadda %w2, %w0, %3\n"
__nops(3)
)
/* 判断是否能获取到锁 */
" eor %w1, %w0, %w0, ror #16\n"
/*判断w1号寄存器是不是0,如果是0,则跳转到3号执行分支处*/
" cbz %w1, 3f\n"
/*
* 到这里说明没有拿到锁,sevl(send local event)发送一个本地事件,避免错过
* 其他处理器释放自旋锁时发出的事件
*/
" sevl\n"
/* wfe(wait for event) 使处理器进入低功耗状态,等待事件*/
"2: wfe\n"
/* 判断是否拿到锁,没有的话重复执行2号逻辑分支*/
" ldaxrh %w2, %4\n"
" eor %w1, %w2, %w0, lsr #16\n"
" cbnz %w1, 2b\n"
/* We got the lock. Critical section starts here. */
"3:"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
: "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
: "memory");
}
// arch/arm/include/asm/spinlock.h
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;
prefetchw(&lock->slock);
__asm__ __volatile__(
"1: ldrex %0, [%3]\n"
" add %1, %0, %4\n"
" strex %2, %1, [%3]\n"
" teq %2, #0\n"
" bne 1b"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");
while (lockval.tickets.next != lockval.tickets.owner) {
wfe();
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
}
smp_mb();
}
自旋锁的原理是:通过循环不停的尝试获取锁,直到成功或超时。
读写锁 write read pin lock
读写锁也是自旋锁,是更细粒度的锁。它允许多个读操作同时执行。同时只可以存在一个写操作,读和写不能同时进行。
因为读写锁是互斥的,当读锁没有被释放时,写操作是无法获取到锁的,但是读操作是继续执行。所以在读操作比较多的时候,读锁一直被占用,导致写锁无法获取,导致写操作处于饥饿状态。
下面是使用方法:
rwlock mr_rwlock = RW_LOCK_UNLOCKED;
read_lock(&mr_rwlock);//获取读锁
//... 临界区
read_unlock(&mr_rwlock);//释放读锁
write_lock(&mr_rwlock);//获取写锁
//... 临界区
write_unlock(&mr_rwlock);释放写锁
其源码实现如下:
读写锁结构体如下:
typedef struct {
volatile unsigned int lock;
} arch_rwlock_t;
获取读锁:
//glodfish/arch/arm/include/asm/spinlock.h
static inline void arch_read_lock(arch_rwlock_t *rw)
{
unsigned long tmp, tmp2;
prefetchw(&rw->lock);
__asm__ __volatile__(
"1: ldrex %0, [%2]\n"
" adds %0, %0, #1\n"
" strexpl %1, %0, [%2]\n"
WFE("mi")
" rsbpls %0, %1, #0\n"
" bmi 1b"
: "=&r" (tmp), "=&r" (tmp2)
: "r" (&rw->lock)
: "cc");
//添加内存屏障barrier()
smp_mb();
}
获取写锁:
static inline void arch_write_lock(arch_rwlock_t *rw)
{
unsigned long tmp;
prefetchw(&rw->lock);
__asm__ __volatile__(
"1: ldrex %0, [%1]\n"
" teq %0, #0\n"
WFE("ne")
" strexeq %0, %2, [%1]\n"
" teq %0, #0\n"
" bne 1b"
: "=&r" (tmp)
: "r" (&rw->lock), "r" (0x80000000)
: "cc");
//添加内存屏障barrier()
smp_mb();
}
顺序锁 seq pin lock
读写锁的缺点是读写互斥,读读不互斥,当存在大量读操作时会使写操作饥饿。
顺序锁是读写锁的优化。相对于读写锁,顺序锁中的读写不再互斥,当在读操作期间,写操作完成了,那么读操作必须重新执行。另外,顺序锁中的写写仍是互斥的。
信号量 semphore
信号量与自旋锁不同的是,当获取不到信号量时,进程不会原地打转,而是进入休眠等待状态。
信号量的使用场景为:
1、锁定时间比较长,线程的切换、维护等待队列以及唤醒的开销相对于CPU自旋较小
2、超过一个以上的锁持有者。因为信号量可以设定初始值,初始值为1叫做互斥信号量,大于1则称为计数信号量
信号量的使用方法如下:
//定义一个信号量
static DECLARE_MUTEX(mr_sem);
//获取一个信号
if(down_interruptible(&mr_sem)) {
//没有获取到信号量,进入休眠等待状态
}
// 否则进入临界区代码
//释放信号量
up(&mr_sem);
信号量源码定义如下:
//信号量结构体
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
//获取信号,获取成功返回0,否则线程休眠,被信号唤醒后,返回-4
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
//加锁
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_interruptible(sem);
//解锁
raw_spin_unlock_irqrestore(&sem->lock, flags);
return result;
}
//线程休眠
static inline int __sched __down_common(struct semaphore *sem, long state,
long timeout)
{
struct task_struct *task = current;
struct semaphore_waiter waiter;
//创建一个信号等待者,添加到信号的等待队列中
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = task;
waiter.up = false;
for (;;) {
if (signal_pending_state(state, task))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_task_state(task, state);
raw_spin_unlock_irq(&sem->lock);
//调度当前线程
timeout = schedule_timeout(timeout);
raw_spin_lock_irq(&sem->lock);
if (waiter.up)
return 0;
}
//进入
timed_out:
list_del(&waiter.list);
return -ETIME;
//被中断唤醒后,将当前任务从等待队列中删除,并返回-4
interrupted:
list_del(&waiter.list);
return -EINTR;
}
//调度当前线程,如果超时时间为MAX_SCHEDULE_TIMEOUT,则当前线程直接休眠,等待被唤醒
signed long __sched schedule_timeout(signed long timeout)
{
struct timer_list timer;
unsigned long expire;
switch (timeout)
{
case MAX_SCHEDULE_TIMEOUT:
schedule();
goto out;
....
}
//进程调度函数
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
}
//进程调度核心实现
static void __sched notrace __schedule(bool preempt)
{
...
/* 更新全局状态,
* 标识当前CPU发生上下文的切换 */
rcu_note_context_switch();
...
/* 挑选一个优先级最高的任务将其排进队列 */
next = pick_next_task(rq, prev);
/* 进程之间上下文切换 */
rq = context_switch(rq, prev, next); /* unlocks the rq */
...
}
//进程切换操作
//1.切换到新进程中的虚拟内存映射 2.切换到新进程的处理器状态,包含栈信息、寄存器信息
// kernel/sched/core.c
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct pin_cookie cookie)
{
...
}
互斥体 mutex
互斥体在使用上和信号量差不多,
mutex使用:
//创建并初始化mutex
struct mutex my_mutex;
mutex_init(&my_mutex);
//加锁
void mutex_lock(&my_mutex);
int mutex_lock_interruptible(&my_mutex);
int mutex_trylock(&my_mutex);
//解锁
void mutex_unlock(&my_mutex);
mutex定义如下:
//结构体
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
};
//加锁
void __sched mutex_lock(struct mutex *lock)
{
might_sleep();
/*
* The locking fastpath is the 1->0 transition from
* 'unlocked' into 'locked' state.
*/
__mutex_fastpath_lock(&lock->count, __mutex_lock_slowpath);
mutex_set_owner(lock);
}
//经过一系列函数调用后,进入__mutex_lock_common函数中
static __always_inline int __sched
__mutex_lock_common(struct mutex *lock, long state, unsigned int subclass,
struct lockdep_map *nest_lock, unsigned long ip,
struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx)
{
...
//其中关键代码,如果获取不到锁,则当前进程休眠,调度其他进程
/* didn't get the lock, go to sleep: */
spin_unlock_mutex(&lock->wait_lock, flags);
//调度其他进程,并禁止抢占。
schedule_preempt_disabled();
spin_lock_mutex(&lock->wait_lock, flags);
...
}
schedule_preempt_disabled()函数最终也会调用到schedule()中,与信号量一致,互斥体和信号量都会引起进程休眠。
管程Monitor
管程是由C.A.R.Hoare和Per Brinch Hansen提出来的。它实际上是对Semaphore机制的延伸和改善,是一种控制更为简单的同步手段。
在涉及庞大且复杂的系统时,对于必须成对配套使用Semaphore机制的P和V操作原语,这一原则并不好控制,并且程序易读性较差。对于信号量的管理也分散在各个参与对象中,容易引发死锁、进程饿死等问题。
管程是可以被多个进程/线程安全访问的对象或模块。
管程中的方法都是受Mutex保护的。
完成量 completion
完成量使用方式如下:
struct completion my_completion;
init_completion(&my_completion);
reinit_completion(&my_completion);
//等待某个完成量
void wait_for_completion(struct completion *c);
//通知某个完成量已完成
void completion(struct completion *c);
void completion(struct completion *c);
完成量定义如下:
//goldfish/include/linux/completion.h
//goldfish/kernel/sched/completion.c
//结构体定义
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
//等待某个完成量的最终函数实现如下:
//循环判断x->done是否大于0或超时
static inline long __sched
do_wait_for_common(struct completion *x,
long (*action)(long), long timeout, int state)
{
if (!x->done) {
DECLARE_WAITQUEUE(wait, current);
__add_wait_queue_tail_exclusive(&x->wait, &wait);
do {
if (signal_pending_state(state, current)) {
timeout = -ERESTARTSYS;
break;
}
__set_current_state(state);
spin_unlock_irq(&x->wait.lock);
timeout = action(timeout);
spin_lock_irq(&x->wait.lock);
} while (!x->done && timeout);
__remove_wait_queue(&x->wait, &wait);
if (!x->done)
return timeout;
}
x->done--;
return timeout ?: 1;
}
//当某个线程完成后,会将done值加1,等待的线程则会终止循环。
void complete(struct completion *x)
{
unsigned long flags;
spin_lock_irqsave(&x->wait.lock, flags);
x->done++;
__wake_up_locked(&x->wait, TASK_NORMAL, 1);
spin_unlock_irqrestore(&x->wait.lock, flags);
}
从上面代码可以看出,completion机制实际上不会使线程休眠,而是让CPU忙等。
Java
原子操作
在java.util.concurrent.atomic包下,提供了一系列不同数据类型的原子操作类,如AtomicBoolean、AtomicInteger等。他们内部的实现都是一个不对外公开的类sun.misc.Unsafe。
为什么开发者不能直接使用这个类呢?
- 没有对调用者的参数进行合法性检查
- 提供的较低级别的功能,如内存操作、线程调度、存取对象、CAS操作等
所以为了防止调用者滥用或误用,将该类定义为只有被引导类加载器或平台类加载器加载的类才可以访问。
public final class Unsafe {
static {
Reflection.registerMethodsToFilter(Unsafe.class, Set.of("getUnsafe"));
}
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
private static final jdk.internal.misc.Unsafe theInternalUnsafe = jdk.internal.misc.Unsafe.getUnsafe();
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
//只有在BootstrapClassLoader或PlatformClassLoader加载时才合法
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
//存取数据
public int getInt(Object o, long offset) {
return theInternalUnsafe.getInt(o, offset);
}
public void putInt(Object o, long offset, int x) {
theInternalUnsafe.putInt(o, offset, x);
}
//CAS操作
@ForceInline
public final boolean compareAndSwapInt(Object o, long offset,
int expected,
int update) {
return theInternalUnsafe.compareAndSetInt(o, offset, expected, update);
}
//获取一块内存的指针
public long getAddress(long address) {
return theInternalUnsafe.getAddress(address);
}
//分配堆外内存
//如DirectByteBuffer类
public long allocateMemory(long bytes) {
return theInternalUnsafe.allocateMemory(bytes);
}
//修改内存大小
public long reallocateMemory(long address, long bytes) {
return theInternalUnsafe.reallocateMemory(address, bytes);
}
//获取本地指针长度,32位系统为4,64位系统为8
public int addressSize() {
return theInternalUnsafe.addressSize();
}
//获取本地内存页大小,返回值一定是2次方幂
public int pageSize() {
return theInternalUnsafe.pageSize();
}
//阻塞线程
public void park(boolean isAbsolute, long time) {
theInternalUnsafe.park(isAbsolute, time);
}
//唤醒线程
public void unpark(Object thread) {
theInternalUnsafe.unpark(thread);
}
//内存栅栏设置
public void loadFence() {
theInternalUnsafe.loadFence();
}
public void storeFence() {
theInternalUnsafe.storeFence();
}
public void fullFence() {
theInternalUnsafe.fullFence();
}
//对象操作
//绕过构造函数、初始化代码、JVM安全检查,创建指定类的对象
//应用:Gson反序列化,存在默认构造函数的情况使用返回生成对象,否则通过反射拿到Unsafe对象,并调用此方法生成对象。
public Object allocateInstance(Class<?> cls)
throws InstantiationException {
return theInternalUnsafe.allocateInstance(cls);
}
...
}
Unsafe类提供了很多低级的,但是很有用的Api。这里我们主要查看CAS操作相关Api的实现。下面是compareAndSwapInt(o, offset, expected, update)函数的实现:
// hotspot/share/prims/unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
//obj是AtomicInteger的对象,通过JNIHandles::resolve()获取obj在内存中oop的实例
oop p = JNIHandles::resolve(obj);
//接下来就要通过p找到需要修改的变量的地址,p是基地址,offset是偏移量
//那么要修改的变量地址 = 基地址 + 偏移量
if (p == NULL) {
//如果基地址为空,addr就等于偏移量offset
volatile jint* addr = (volatile jint*)index_oop_from_field_offset_long(p, offset);
//执行RawAccess类下的atomic_cmpxchg函数,如果返回值等于旧值则代表更新成功,否则失败
return RawAccess<>::atomic_cmpxchg(addr, e, x) == e;
} else {
//如果基地址不为空,就执行HeapAccess类下的atomic_cmpxchg_at函数
return HeapAccess<>::atomic_cmpxchg_at(p, (ptrdiff_t)offset, e, x) == e;
}
} UNSAFE_END
最终的原子操作会依据不同的CPU平台,使用对应平台的CPU指令完成。下面以linux_x86和linux_arm为例分析CAS的实现。
// hotspot/os_cpu/linux_x86/atomic_linux_x86.hpp
// 四个参数,分别是目的地址、旧值、新值、内存存储顺序
// <4>代表存储的数据类型占4个字节,不同的字节数对应不同的指令
inline T Atomic::PlatformCmpxchg<4>::operator()(T volatile* dest,
T compare_value,
T exchange_value,
atomic_memory_order /* order */) const {
STATIC_ASSERT(4 == sizeof(T));
// a 代表exa寄存器,r 代表任意一个寄存器
// %1为exchange_value新值,%3为dest目的地址,旧值存储到了eax寄存器中
// cmpxchgl指令将比较旧值与目的地址存储的内容是否相等,如果相等则将新值写到目的地址中。
// 最终返回eax中的旧值
__asm__ volatile ("lock cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest)
: "cc", "memory");
return exchange_value;
}
// hotspot/cpu/arm/stubGenerator_arm.cpp
address generate_atomic_cmpxchg() {
address start;
StubCodeMark mark(this, "StubRoutines", "atomic_cmpxchg");
start = __ pc();
Register cmp = R0;
Register newval = R1;
Register dest = R2;
Register temp1 = R3;
Register temp2 = Rtemp; // Rtemp free (native ABI)
__ membar(MEMBAR_ATOMIC_OP_PRE, temp1);
// atomic_cas returns previous value in R0
__ atomic_cas(temp1, temp2, cmp, newval, dest, 0);
__ membar(MEMBAR_ATOMIC_OP_POST, temp1);
__ bx(LR);
return start;
}
// hotspot/os_cpu/linux_arm/macroAssembler_linux_arm_32.cpp
// 由于arm属于精简指令集,没有类似x86中cmpxchgl指令可以一次性完成比较和交换操作,
// 需要使用ldrex和strex独占加载和存储指令完成比较操作操作
void MacroAssembler::atomic_cas(Register temp1, Register temp2, Register oldval, Register newval, Register base, int offset) {
if (temp1 != R0) {
// try to read the previous value directly in R0
if (temp2 == R0) {
// R0 declared free
temp2 = temp1;
temp1 = R0;
} else if ((oldval != R0) && (newval != R0) && (base != R0)) {
// free, and scratched on return
temp1 = R0;
}
}
//如果arm架构版本大于等于6,即armv6的版本之后使用ldrex和strex指令
if (VM_Version::supports_ldrex()) {
Label loop;
assert_different_registers(temp1, temp2, oldval, newval, base);
bind(loop);
//加载目的地址中的值到temp1寄存器中
ldrex(temp1, Address(base, offset));
//比较是否与期望值相等
cmp(temp1, oldval);
//修改目的地址中的值为新值
strex(temp2, newval, Address(base, offset), eq);
//比较
cmp(temp2, 1, eq);
b(loop, eq);
if (temp1 != R0) {
mov(R0, temp1);
}
} else if (VM_Version::supports_kuser_cmpxchg32()) {
// On armv5 platforms we must use the Linux kernel helper
// function for atomic cas operations since ldrex/strex is
// not supported.
//
// This is a special routine at a fixed address 0xffff0fc0
//
// input:
// r0 = oldval, r1 = newval, r2 = ptr, lr = return adress
// output:
// r0 = 0 carry set on success
// r0 != 0 carry clear on failure
//
// r3, ip and flags are clobbered
//
Label done;
Label loop;
push(RegisterSet(R1, R4) | RegisterSet(R12) | RegisterSet(LR));
if ( oldval != R0 || newval != R1 || base != R2 ) {
push(oldval);
push(newval);
push(base);
pop(R2);
pop(R1);
pop(R0);
}
if (offset != 0) {
add(R2, R2, offset);
}
mov(R4, R0);
bind(loop);
ldr(R0, Address(R2));
cmp(R0, R4);
b(done, ne);
mvn(R12, 0xf000);
mov(LR, PC);
sub(PC, R12, 0x3f);
b(loop, cc);
mov(R0, R4);
bind(done);
pop(RegisterSet(R1, R4) | RegisterSet(R12) | RegisterSet(LR));
} else {
// Should never run on a platform so old that it does not have kernel helper
stop("Atomic cmpxchg32 unsupported on this platform");
}
}
LDREX和STREX是通过ARM内核的一个叫Exclusive Monitor的机制实现的,EM是一个状态机。
LDREX指令将Monitor置为Exclusive状态,STREX指令将Exclusive状态置回为Open状态,由此保证访问的唯一性。
volatile
Java的volatile关键字和C/C++的volatile关键字实现的语义基本相同:可见性、顺序性、不可优化性。
但Java的volatile中的顺序性有了极大的增强。
- 所有volatile变量写操作之前的针对其他任何变量的读写操作,都不会在编译器、CPU优化后,乱序到volatile变量的写操作之后执行。
- 所有volatile变量写操作之后的针对其他任何变量的读写操作,都不会在编译器、CPU优化后,乱序到volatile变量的写操作之前执行。
public class VolatileTest {
int a;
volatile int v1 = 1;
void readAndWrite() {
a = 1;
v1 = 2;
}
public static void main(String[] args) {
new VolatileTest().readAndWrite();
}
}
通过hsdis工具将上面源码的class文件编译成汇编后
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
-XX:CompileCommand=dontinline,*VolatileTest.readAndWrite
-XX:CompileCommand=compileonly,*VolatileTest.readAndWrite
[PakageName]/source_code/thread/VolatileTest > VolatileTest.asm
截取关键指令如下:
...
0x00000001071b1dbb: movl $0x1,0xc(%rsi) ;*putfield a
; - [PakageName].source_code.thread.VolatileTest::readAndWrite@2 (line 16)
0x00000001071b1dc2: mov $0x2,%edi
0x00000001071b1dc7: mov %edi,0x10(%rsi)
0x00000001071b1dca: lock addl $0x0,(%rsp) ;*putfield v1
; - [PakageName].source_code.thread.VolatileTest::readAndWrite@7 (line 17)
...
其原理是在编译后的汇编指令中添加一个带锁的加0指令:
lock addl $0x0,(%rsp)
lock前缀指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回主内存。
- 这个内存回写操作会使在其他CPU缓存了该内存地址的数据无效。
synchronized
对于synchronized关键字,有两种使用方法,一种是直接修饰方法,如下面代码中的add()方法。
另一种是单独写成同步代码块,如get()方法。
public class SynchronizedTest {
private int i = 0;
public synchronized void add(int count) {
i += count;
}
public int get() {
synchronized (this) {
return i;
}
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
synchronizedTest.add(2);
synchronizedTest.get();
}
}
通过查看字节码文件可以看出,使用synchronized包裹的代码块,会在其字节码指令前天添加monitorenter和monitorexit指令。而synchronized修饰的方法不会添加monitorxxx指令,只是在方法声明的头部有synchronized标识。
虽然这两种方式生层的字节码不完全相同,但是当进行字节码解释时都会统一走到InterpreterRuntime::monitorenter方法中。
synchronized修饰方法时的调用流程为(以armCPU/模板解释器为例):
// hotspot/cpu/arm/templateInterpreterGenerator_arm.cpp
generate_normal_entry(bool synchronized) -> lock_method()
// hotspot/cpu/arm/interp_masm_arm.cpp
-> lock_object(R1)
synchronized修饰代码块是的调用流程为:
public synchronized void add(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //synchronized同步方法标识
Code:
stack=3, locals=2, args_size=2
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iload_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 15: 0
line 16: 10
public int get();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //监视器锁进入
4: aload_0
5: getfield #2 // Field i:I
8: aload_1
9: monitorexit //监视器锁正常退出
10: ireturn
11: astore_2
12: aload_1
13: monitorexit //监视器锁异常退出
14: aload_2
15: athrow
Exception table:
from to target type
4 10 11 any
11 14 11 any
LineNumberTable:
line 19: 0
line 20: 4
line 21: 11
为了搞清楚monitorenter和monitorexit的原理,需要先搞清楚对象头和monitor管程这两个东西。
Java实例对象在内存中的结构分为:对象头、实例数据和对齐数据。使用“org.openjdk.jol:jol-core:0.10”工具包可以打印出一个实例对象的内存占用信息。
public class ObjectLayoutTest {
public static void main(String... args) {
Test t = new Test();
String objectLayout = ClassLayout.parseInstance(t).toPrintable();
System.out.println("对象内存布局:" + objectLayout);
}
static class Test {
private boolean b = false;
}
}
上面代码输出如下:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 41 c1 00 f8 (01000001 11000001 00000000 11111000) (-134168255)
12 1 boolean Test.b false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
可以看出来,对象头占12字节,Test类的boolean实例变量占1个字节,对齐数据占3字节,所以Test实例对象在内存中共占用16字节。因为JVM规定对象占用内存大小必须为8的倍数,所以末尾存在3字节的对齐数据。
其中,对象头是一定存在的,而实力数据和对齐填充部分不一定存在。
那么对象头中的12字节又代表什么含义呢?
对象头
在虚拟机中,用oopDesc类来描述一个对象指针。
// hotspot/share/oops/oopDesc.hpp
class oopDesc {
private:
volatile markWord _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
...
}
oopDesc是描述对象类型的顶层类,其子类则具体对应描述了Java语言中的不同的数据类型。
- instanceOopDesc 描述实例对象
- arrayOopDesc 描述数组
- objArrayOopDesc 描述对象数组
- typeArrayOopDesc 描述原生数组类型
其中oopDesc中定义的两个私有成员变量_mark和_metadata就是对象头部信息。markWord描述了当前对象的各种标识,不同的标识代表了不同的对象状态,Klass描述了当前对象所属的类的元数据信息。下面主要介绍对象标识的含义。
// hotspot/share/oops/markWord.hpp
class markWord {
private:
// uintptr_t是一个无符号整型类型。区分32位机器和64位机器。
// 在32位机器上代表整型,长度32bit,在64位机器上代表长整型,长度64bit。
uintptr_t _value;
...
}
我们可以看到对象头中的标识为就是一个无符号整型或无符号长整型,将32/64位划分为不同的部分来存储不同含义的数据。如下说明:
hash: 保存对象的哈希码
age: 保存对象的分代年龄
biased_lock: 偏向锁标识位
lock: 锁状态标识位
JavaThread*: 保存持有偏向锁的线程ID
epoch: 保存偏向时间戳
32位机器上划分方式:
普通对象 | 高25位:对象哈希值 | 4位:分代年龄 | 1位:是否偏向锁 | 2位:锁状态 | |
---|---|---|---|---|---|
偏向锁定对象 | 高23位:持有偏向锁的线程ID | 2位:偏向时间戳 | 4位:分代年龄 | 1位:是否偏向锁 | 2位:锁状态 |
64位机器上划分方式:
普通对象 | 高25位:无用 | 31位:对象哈希值 | 1位:未使用 | 4位:分代年龄 | 1位:是否偏向锁 | 2位:锁状态 |
---|---|---|---|---|---|---|
偏向锁定对象 | 高54位:持有偏向锁的线程ID | 2位:偏向时间戳 | 1位:未使用 | 4位:分代年龄 | 1位:是否偏向锁 | 2位:锁状态 |
其中偏向锁标识和锁状态标识位一共占3位,共定义了五种状态:
10进制值 | 二进制表示 | 含义 |
---|---|---|
0 | 000 | 轻量锁 |
1 | 001 | 未锁定 |
2 | 010 | 监视器锁 |
3 | 011 | GC(标记清除)标记 |
5 | 101 | 偏向锁 |
下面根据对象头的定义,解释不同的锁是什么含义。
未锁定
这个是最简单的,即没有任何锁操作添加到当前对象上,也就是普通对象。
偏向锁
偏向锁是指一旦线程第一次获得监视对象,之后让监视对象”偏向“这个线程,也就是同一线程再次获取锁时,则认为已经获得了锁,提高了效率。
其原理就是:
1、线程想要获取锁时,判断锁头中偏向锁标识是否1来判断是否可以进入可偏向的状态。
2、如果为1, 则判断是否存在偏向线程id,如果没有则进入临界区,并设置为当前线程id
3、如果存在偏向线程id,并且与当前线程id一致,则进入临界区
4、如果与当前线程id不一致,说明发生了多线程竞争。等到达安全点时持有偏向锁的线程被挂起,偏向锁升级为轻量级锁。
轻量级锁
轻量级锁是指通过线程自旋(一般是for/while循环)的形式等待锁释放。
轻量级锁不会主动休眠线程,所以没有线程切换。
当一个线程持有偏向锁时,又有一个线程来请求锁的话,偏向锁就会升级为轻量级锁。
轻量级锁的实现为:
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
重量级锁
当等待线程自旋超过一定次数,或者有一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
上面几种锁的源码流程如下:
// hotspot/share/interpreter/bytecodeinterpreter.cpp
CASE(_monitorenter): {
oop lockee = STACK_OBJECT(-1);
// 偏向锁
...
// 偏向锁失败,锁升级
if (!success) {
markWord displaced = lockee->mark().set_unlocked();
entry->lock()->set_displaced_header(displaced);
bool call_vm = UseHeavyMonitors;
if (call_vm || lockee->cas_set_mark(markWord::from_pointer(entry), displaced) != displaced) {
// 轻量级锁重入
if (!call_vm && THREAD->is_lock_owned((address) displaced.clear_lock_bits().to_pointer())) {
entry->lock()->set_displaced_header(markWord::from_pointer(NULL));
} else {
//执行锁升级
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
}
}
}
// hotspot/share/interpreter/interpreterRuntime.cpp
JRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
ObjectSynchronizer::enter(h_obj, elem->lock(), CHECK);
// hotspot/share/runtime/synchronizer.cpp
void ObjectSynchronizer::enter(Handle obj, BasicLock* lock, TRAPS) {
//在安全点将偏向锁撤销
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::revoke(obj, THREAD);
} else {
BiasedLocking::revoke_at_safepoint(obj);
}
}
markWord mark = obj->mark();
if (mark.is_neutral()) {
lock->set_displaced_header(mark);
//升级为轻量级锁
if (mark == obj()->cas_set_mark(markWord::from_pointer(lock), mark)) {
return;
}
// Fall through to inflate() ...
} else if (mark.has_locker() &&
THREAD->is_lock_owned((address)mark.locker())) {
lock->set_displaced_header(markWord::from_pointer(NULL));
return;
}
lock->set_displaced_header(markWord::unused_mark());
//升级为重量级锁
inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}
// hotspot/share/runtime/objectMonitor.cpp
void ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD;
//判断_owner的值是否为NULL,返回值cur等于_owner的旧值。
//如果是NULL,则直接返回,说明还没有加过锁,属于异常情况。
void * cur = Atomic::cmpxchg(&_owner, (void*)NULL, Self);
if (cur == NULL) {
return;
}
//如果_owner的旧值等于当前线程地址,说明当前线程已经拥有这个锁,属于重入情况,仅将锁重入的次数记录加1
if (cur == Self) {
_recursions++;
return;
}
//如果cur的值指向了当前线程中某个栈帧的记录,说明当前线程已经拥有轻量级锁。则直接返回。
if (Self->is_lock_owned((address)cur)) {
_recursions = 1;
_owner = Self;
return;
}
//前面的逻辑是对无锁、重入、轻量级锁的情况判断,并没有发生真正的竞争。
//而下面则是处理出现真正竞争的逻辑。
Self->_Stalled = intptr_t(this);
// 尝试通过自旋获取锁,避免进入等待队列,从而引起昂贵的状态转换。
// 即原子性的判断_owner指针是否为NULL,如果为NULL则赋值为当前线程,如果赋值成功则代表获取锁成功。失败了则通过CPU的pause指令,让CPU睡眠30个时钟周期。
// 这里的自旋是依据上次自旋的结果来决定本次自旋次数:
// 1. 固定自旋次数为11,如果自旋成功,则记录下次最小自旋次数为1000 + 100次。
// 2. 如果自旋11次失败,则按照上次记录的自旋次数再次进入自旋。如果自旋成功,记录下次最小自旋次数为1100 + 100 次,这个自旋次数的最大值为5000。
// 3. 如果是第一次自旋,那么上次记录的自旋次数为0,所以会直接返回自旋失败。
// 4. 如果上次记录的自旋次数不为0,则再次自旋。
// 4. 如果自旋失败,则记录下次最小自旋次数为当前自旋次数减200。如果成功则加100。
if (TrySpin(Self) > 0) {
Self->_Stalled = 0;
return;
}
JavaThread * jt = (JavaThread *) Self;
//将竞争数加1,用于锁降级时,判断当前锁是否忙碌的条件之一。
Atomic::inc(&_contentions);
{
// 更改Java线程状态以指示在监视器进入时被阻塞。
JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
//设置线程当前的排队监视器
Self->set_current_pending_monitor(this);
// 通过自旋执行EnterI函数等待锁的释放,EnterI函数中会引起休眠
for (;;) {
jt->set_suspend_equivalent();
EnterI(THREAD);
if (!ExitSuspendEquivalent(jt)) break;
_recursions = 0;
_succ = NULL;
exit(false, Self);
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
Atomic::dec(&_contentions);
Self->_Stalled = 0;
OM_PERFDATA_OP(ContendedLockAttempts, inc());
}
void ObjectMonitor::EnterI(TRAPS) {
Thread * const Self = THREAD;
//尝试获取锁
if (TryLock (Self) > 0) {
return;
}
// 在进入等待队列前,再次尝试自旋获取锁
if (TrySpin(Self) > 0) {
return;
}
//由当前线程构建一个锁等待对象
//然后添加到ObjectMonitor的_cxq链表的头部,直到当前线程获取到锁。
ObjectWaiter node(Self);
Self->_ParkEvent->reset();
node._prev = (ObjectWaiter *) 0xBAD;
node.TState = ObjectWaiter::TS_CXQ;
ObjectWaiter * nxt;
for (;;) {
//通过原子操作不停的将node设置为链表头节点
node._next = nxt = _cxq;
if (Atomic::cmpxchg(&_cxq, nxt, &node) == nxt)
break;
//如果在设置过程中,CAS失败了,说明_cxq链表发生的变更,则再次尝试获取锁。
//这个优化可以再次降低线程进入等待的概率。
if (TryLock (Self) > 0) {
return;
}
}
if (nxt == NULL && _EntryList == NULL) {
Atomic::replace_if_null(&_Responsible, Self);
}
int nWakeups = 0;
int recheckInterval = 1;
for (;;) {
if (TryLock(Self) > 0)
break;
//ParkEvent->park()的实现是使用C++函数pthread_cond_wait/pthread_cond_timedwait将线程阻塞住
//他们需要配合一个pthread_cond_t(线程条件)和一个pthread_mutex_t(线程互斥锁)一起使用
//当通过pthread_cond_signal()函数发出一个信号时,会激活等待该信号条件的线程
if (_Responsible == Self) {
//如果
Self->_ParkEvent->park((jlong) recheckInterval);
// 提高重复检查间隔
recheckInterval *= 8;
if (recheckInterval > MAX_RECHECK_INTERVAL) {
recheckInterval = MAX_RECHECK_INTERVAL;
}
} else {
Self->_ParkEvent->park();
}
//线程被唤醒后、或睡眠超时后,尝试获取锁
if (TryLock(Self) > 0) break;
++nWakeups;
//如果没有获取到锁,再次自旋
if (TrySpin(Self) > 0) break;
if (_succ == Self) _succ = NULL;
OrderAccess::fence();
}
UnlinkAfterAcquire(Self, &node);
if (_succ == Self) _succ = NULL;
if (_Responsible == Self) {
_Responsible = NULL;
OrderAccess::fence();
}
return;
}
上面就是使用synchronized关键字实现的加锁流程,总结来说就是当一个线程尝试进入临界区的时候,会先去获取该临界区对应的锁,实际原理就是一个锁就是一个对象,这个对象头部记录着不同的锁的状态,获取锁的操作就是设置对象头部的锁标识或者是通过对象头部存储的ObjectMonitor指针设置_owner字段,这个设置操作是原子性的,并且加入了自适应循环来降低线程休眠的几率。如果获取锁成功,会继续执行后面的字节码。如果获取锁失败,则使用pthread_cond_wait函数将当前线程阻塞直至被唤醒。
当持有锁的线程执行完毕临界区代码后,会释放当前持有的锁,底层是通过pthread_cond_signal()函数唤醒对应的线程继续执行。
那么偏向锁、轻量级锁、自旋锁、重量级锁的区别是什么?
偏向锁是通过一次CAS操作,在锁对象的头部标识中存储了进入临界区的线程对象地址。下次只需要判断前后两次获取锁的线程对象是否一致即可。
轻量级锁是通过两次CAS操作在线程执行栈中,建立一个锁记录的栈帧。
Monitor
Monitor, 管程,也称为监视器。它提供了一种同步机制,可以让多个线程针对共享资源进行互斥访问。
每个对象都对应着一个监视器,叫做ObjectMonitor。当需要对一个对象进行加锁时,这个监视器被存储到了对象头部信息中的MarkWord中。
//hotspot/share/oops/markword.hpp
//在一个对象标识中设置监视器
static markWord encode(ObjectMonitor* monitor) {
uintptr_t tmp = (uintptr_t) monitor;
return markWord(tmp | monitor_value);
}
//从一个对象标识中获取监视器
ObjectMonitor* monitor() const {
return (ObjectMonitor*) (value() ^ monitor_value);
}
当多个线程同时竞争一个锁时、调用对象的wait、notify、notifyAll方法时,就是通过ObjectMonitor监视器来实现的访问控制、线程调度。
对应JVM中的实现为ObjectMonitor类。其声明如下:
// hotspot/share/runtime/objectMonitor.hpp
// ObjectMonitor类是实现了JavaMonitor的重量级版本
// ObjectMonitor是由轻量级锁 BasicLock/Stack锁版本升级而来,
// 这个锁升级是由竞争或使用Object.wait()引起的。
class ObjectMonitor {
volatile markWord _header; //要监视的对象头部标识字
void* volatile _object;//当前监视器监视的对象指针
ObjectMonitor* _next_om;//下一个ObjectMonitor指针
//这里对于ObjectMonitor中成员变量的顺序和间隔有些要求
//1. _header字段必须放在第一个位置,为了方便从ObjectMonitor中获取markWord,而不需要在markWord中存储ObjectMonitor指针。
//2. _header和_owner这两个字段中间应该被足够大的空闲空间来间隔开,
// 避免由于多线程的并发访问引起错误的共享。
//添加最小间隔
DEFINE_PAD_MINUS_SIZE(0, DEFAULT_CACHE_LINE_SIZE,
sizeof(volatile markWord) + sizeof(void* volatile) +
sizeof(ObjectMonitor *));
void* volatile _owner;//拥有当前监视器的线程或基础锁指针
volatile jlong _previous_owner_tid;//上次持有该监视器的线程id
volatile intx _recursions;//重入监视器的计数,第一次进入时为0
ObjectWaiter* volatile _EntryList;//处理阻塞状态的线程代理队列
ObjectWaiter* volatile _cxq;//最近阻塞的线程队列
Thread* volatile _succ;//
Thread* volatile _Responsible;
volatile int _Spinner;
volatile int _SpinDuration;
volatile jint _contentions;//处于活动状态竞争者的数量,用于判断当前监视器是否可以降级
ObjectWaiter* volatile _WaitSet;//等待当前监视器的线程队列
volatile jint _waiters;//等待线程的数量
volatile int _WaitSetLock;//自旋锁,保护等待线程队列
//下面是主要用到的函数
bool check_owner(Thread* THREAD);//判断当前线程是否拥有该监视器
void enter(TRAPS);//进入该监视器
void exit(bool not_suspended, TRAPS);//退出该监视器
void wait(jlong millis, bool interrupttable, TRAPS);//暂时让出当前监视器
void notify(TRAPS);//唤醒等待该监视器其他线程
void notifyAll(TRAPS);//唤醒所有等待该监视器其他线程
void reenter(intx recursions, TRAPS);//重新进入该监视器
}
上面是一个对象监视器的大概结构,当多线程竞争比较激烈时,会升级为重量级锁,此时调用enter函数来判断当前线程是获得锁、还是等待、还是休眠。
对象监视器中有三个ObjectWaiter队列_cxq、_EntryList、_WaitSet,他们的作用分别是:
_cxq: 用于存放最近发生的由于不能获取到锁而休眠的线程队列。
_EntryList: 当有线程释放锁时,将_cxq中的所有元素移动到_EntryList中
_WaitSet: 是指持有锁的线程调用Object.wait方法,此时会将该线程加入_WaitSet队列中。当调用Object.notify()或notifyAll()时,会将_WaitSet中的线程移动到_cxq或_EntryList中。
具体加锁流程已在前一节讲过,这里给出一个基于对象监视器实现重量级锁的示意图。
JVM中的字节码解释器
在Hotspot虚拟机中存在两种不同的解释器:模板解释器(汇编语言版本)和C++解释器(高级语言版本)。
具体分工如下:
模板解释器 | C++解释器 | 功能 |
---|---|---|
templateTable* | bytecodeInterpreter* | 实际的字节码解释器 |
templateInterpreter* | cppInerpreter* | 1. 生成创建和管理解释器运行时帧的汇编代码 2. 用于填充在非优化期间创建的解释器框架的代码 |
ReentrantLock
ReentrantLock为可重入锁,分为公平锁和非公平锁两种模式,使用方法如下:
//默认构造函数为非公平锁
ReentrantLock reentrantLock = new ReentrantLock();
//也可以通过有参构造函数指定为公平锁
ReentrantLock reentrantLock = new ReentrantLock(true);
//获取锁和释放锁
//当锁被其他线程占用时,当前线程会休眠
reentrantLock.lock();
reentrantLock.unlock();
//尝试获取锁,如果锁被其他线程占用,则立即返回false,不会休眠
boolean suc = reentrantLock.tryLock();
//在指定到期时间内获取锁,如果到期后仍没有获取到锁则返回false。
//如果是用的公平锁,那么在等待期间如果有其他线程也在等待锁,那么当前线程是无法获取锁的
boolean suc = reentrantLock.tryLock(1000, TimeUnit.MILLISECONDS);
//可以通过两种形式的组合来允许对公平锁进行插入
if(reentrantLock.tryLock() || reentrantLock.tryLock(1000, TimeUnit.MILLISECONDS)) {
...
}
在这里引出两个问题:
- ReentrantLock加锁是如何阻塞线程的?
- 与Synchronized关键字加锁逻辑有什么区别?
1.ReentrantLock加锁是如何阻塞线程的?
和Synchronized关键字相同,也是通过Native层线程阻塞函数pthread_cond_wait/pthread_cond_timedwait
// java/util/concurrent/locks/LockSupport.java
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, 0L);
setBlocker(t, null);
}
// hotspot/share/prims/unsafe.cpp
UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
HOTSPOT_THREAD_PARK_BEGIN((uintptr_t) thread->parker(), (int) isAbsolute, time);
EventThreadPark event;
JavaThreadParkedState jtps(thread, time != 0);
//阻塞当前线程
thread->parker()->park(isAbsolute != 0, time);
if (event.should_commit()) {
const oop obj = thread->current_park_blocker();
if (time == 0) {
post_thread_park_event(&event, obj, min_jlong, min_jlong);
} else {
if (isAbsolute != 0) {
post_thread_park_event(&event, obj, min_jlong, time);
} else {
post_thread_park_event(&event, obj, time, min_jlong);
}
}
}
HOTSPOT_THREAD_PARK_END((uintptr_t) thread->parker());
} UNSAFE_END
2.与Synchronized关键字加锁逻辑有什么区别?
Synchronized关键字在整个加锁流程中比较复杂,涉及到了多种类型的锁(偏向锁、轻量级锁、重量级锁),而且还有锁升级策略。优点是使用起来简单,虚拟机来保证锁被安全释放。而且,Synchronized属于非公平锁。
ReentrantLock则由开发者自己选择加锁和解锁的位置,而且可以指定公平锁或非公平锁,使用更灵活。
公平锁和非公平锁
对于锁空闲状态下,公平锁和非公平锁是一样的,都是通过CAS操作判断是否可以获取锁,然后获取锁。
区别在于,锁被长期占用时,由多个想要获取锁的线程组成了一个等待队列。两种锁都会有等待队列,关键在于当占用的锁被释放时,该锁是由等待队列的头部线程获取到,还是由一个新的想要获取锁,但还没有进入等待队列的线程获取到。前者属于公平锁,后者属于非公平锁。
公平锁的实现原理就是当前线程在尝试获取锁之前先判断等待队列中是否存在其他等待者,如果有的话,当前线程也要先进入等待队列。
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//先判断是否存在已经排队的等待者
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;
}
非公平锁的效率比公平锁高的原因是,减少了许多线程上下文的切换。
通过查看下面代码的输出,可以看出两种锁的差异。
class LockTest {
ReentrantLock mLock;
public LockTest(boolean fair) {
mLock = new ReentrantLock(fair);
}
void doSomething() {
try {
mLock.lock();
System.out.println(Thread.currentThread().getName() + "获得锁");
} finally {
mLock.unlock();
}
}
public static void main(String[] args) {
// LockTest lockTest = new LockTest(false);
LockTest lockTest = new LockTest(true);
Thread task = new Thread() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName + " 进来了");
lockTest.doSomething();
}
}
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<5;i++) {
exec.execute(thread);
}
exec.shutdown();
}
}
共享锁和排它锁
共享锁是指多个线程可以同时获得锁,既可以同时进入临界区。ReadWriteLock中的读锁就是一种共享锁。
排它锁是指同一时间只能有一个线程获得锁,其他线程必须等待。ReentrantLock就是一种排他锁。
ReentrantReadWriteLock 可重入读写锁
ReentrantReadWriteLock是专门为常见的读写场景设计的。一般情况,读操作比写操作要频繁的多,并且多个读操作可以同时执行而不会影响数据安全。所以其中的读锁是共享的,多个线程可以同时持有。但写锁之间、写锁与读锁之间是互斥的。
需要注意的是,当释放当前持有的锁时,一般情况下会为等待时间最长的写线程分配写锁定,也就是写锁的优先级高于读锁。但是如果有一组读线程的等待时间比所有写线程的等待时间还要长,那么将会为这组读线程分配读锁定。所以会出现写线程饥饿的情况。
ReentrantReadWriteLock支持锁降级,不支持锁升级。即拥有写锁的线程可以再获取读锁,反过来不行。
写锁支持Condition,读锁不支持。
读锁最大数量为65535个,写锁最大数量为65535个,也就是2的16次方减一个。超出这个数量后再次获取锁则会抛出错误。
ReentrantReadWriteLock使用state整型值的高16位存储共享锁的数量,低16位存储排它锁的数量。
它的获取锁逻辑如下:
//java.util.concurrent.locks.ReentrantReadWriteLock.java
//获取排它锁
//如果返回ture,代表获取写锁成功,写线程继续执行后续代码,否则入队列并阻塞
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//c代表了读锁和写锁的数量
int c = getState();
//w为写锁的数量
int w = exclusiveCount(c);
if (c != 0) {
// 如果读写锁总数不为0,但是写锁数为0,说明存在读锁,所以加锁失败
// 如果当前线程没有拥有写锁,说明其他线程占用了写锁,也会加锁失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//如果是重入情况,要判断是否超过了最大锁记录的值,即65535个
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//没有超出最大值,更新state字段值
setState(c + acquires);
return true;
}
//如果目前没有任何锁,根据锁类型判断此次加锁是否需要入队列。
//如果是非公平锁,则通过CAS操作修改state值,修改成功则表明获取锁成功。
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
//获取共享锁
//如果返回值大于0,则获取成功,读线程可以继续后面代码
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//如果其他线程拥有排它锁,失败。
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//如果是非公平锁,并且没有超出最大值,则通过CAS设置更新STATE的值
//c+SHARED_UNIT代表存储到c的高16位中
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//读锁数为0,记录第一个获取读锁的线程和读锁数
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 第一个读锁线程,读锁重入
firstReaderHoldCount++;
} else {
// 从ThreadLocal中取出当前线程已获得的读锁数,并加一
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 如果是公平锁,或CAS失败,则需要执行完整版本的读锁获取逻辑。
//这里可以处理CAS丢失和锁重入
return fullTryAcquireShared(current);
}
// 获取共享锁的剩余部分代码
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
// 其他线程存在写锁,所以获取读锁失败
// 如果是当前线程存在写锁,则跳出判断
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// 如果队列中有等待获取读锁的线程,则不能立即获取读锁。
// 如果是第一个获取读锁的线程,跳出判断,符合直接获取读锁的条件
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 如果是新线程想要获取读锁,则需要入队列
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
//如果当前读锁数等于最大数量65535个,则抛出异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//尝试CAS修改c值,修改成功则获取读锁成功,否则继续循环。
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
CountDownLatch 闭锁
CountDownLatch由一个正整数n初始化,在n大于0之前,await()方法的调用会一直阻塞,每调用一次countdown()方法,n减一。当n等于0时,await()方法继续执行后续逻辑。
//下面代码展示了调用countdown()方法时,对初始化n的减一操作。
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
//执行await()方法时,根据n是否等于0来决定是否阻塞当前线程
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
由于有可能多个线程调用await()方法并阻塞,所以CountDownLatch使用的共享锁,即多个线程等待同一把锁。
需要注意的是,CountDownLatch一经被初始化,只能使用一次,正整数n不能被重置。
CyclicBarrier 回环栅栏
CyclicBarrier严格来说不属于锁,而是基于ReentrantLock实现的特定场景的助手类。
这个同步助手类可以允许多个线程在运行过程中互相等待,等待所有线程都达到一个栅栏点后再继续向下执行。
它非常适合在以下场景使用:多个线程分别执行任务的一部分,而且必须在执行过程中,能够偶尔的等待一下其他线程。
在CyclicBarrier定义时,还可以指定最后一个到达栅栏点的线程额外执行一些任务。
CyclicBarrier是可以重复使用的,当所有线程都通过栅栏后,CyclicBarrier会自动初始化下次的状态。
下面是实例代码:
//一个子线程,执行部分任务,每个线程都要在同一个回环栅栏上等待
public static class ChildTaskRun implements Runnable {
private CyclicBarrier cyclicBarrier;
public ChildTaskRun(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("子线程" + Thread.currentThread().getName() + "开始处理任务...");
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程" + Thread.currentThread().getName() + "处理任务完成,等待其他线程...");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("所有子线程完成同步任务, 继续执行其他任务...");
}
}
//初始化CyclicBarrier,并指定子任务数和到达时的栅栏动作
public static void main(String[] args) {
int childTaskNum = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(childTaskNum, new Runnable() {
@Override
public void run() {
System.out.println("当最后一个线程 " + Thread.currentThread().getName() + "完成同步任务时,做一些事情...");
}
});
for (int i = 0; i < childTaskNum; i++) {
Thread thread = new Thread(new ChildTaskRun(cyclicBarrier));
thread.start();
}
}
输出内容如下:
子线程Thread-0开始处理任务...
子线程Thread-1开始处理任务...
子线程Thread-2开始处理任务...
子线程Thread-0处理任务完成,等待其他线程...
子线程Thread-1处理任务完成,等待其他线程...
子线程Thread-2处理任务完成,等待其他线程...
当最后一个线程 Thread-2完成同步任务时,做一些事情...
所有子线程完成同步任务, 继续执行其他任务...
所有子线程完成同步任务, 继续执行其他任务...
所有子线程完成同步任务, 继续执行其他任务...
调用await()方式时的等待逻辑如下:
// java.util.concurrent.CyclicBarrier.java
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
final Generation g = generation;
//栅栏被破坏,抛出异常
if (g.broken)
throw new BrokenBarrierException();
//任务数减一
int index = --count;
//如果减到0了,说明所有线程都到达的栅栏点,需要做两件事:
//1、在当前线程执行栅栏点任务
//2、唤醒其他线程继续执行,并重置执行状态
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
for (;;) {
try {
//如果index不为0,说明还有未到达栅栏点的线程,需要阻塞当前线程
//这里要区分是否设置了超时时间
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
// 当被唤醒时,状态已经被重置,所以局部变量g和全局变量generation已经不相等了
if (g != generation)
return index;
//如果是因为超过等待时间而继续执行的话,则抛出超时异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
Semaphore 信号量
计数信号量,概念上是指一系列的许可证。一个线程通过acquire()方法尝试获取一个许可,如果存在可用的许可证,则可以拥有然后继续执行。如果许可证都用光了,则线程等待。执行完毕后调用release()方法归还当前持有的许可证。
实际上,并没有真实的许可证对象,只是维护了一个整数用于计数。它的思想类似于读写锁中的共享的读锁,只不过添加了数量的限制。
由于许可证数量有限,当线程数量较多时,则会产生队列。当需要唤醒某个线程时,这个策略就又区分为公平和非公平。和ReentrantLock实现的非公平锁原理一致。
使用示例如下:
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(5, true);//公平信号量
// final Semaphore semaphore = new Semaphore(5, false);//非公平信号量
//开启20个线程
for (int i = 0; i < 20; i++) {
final int Num = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
//线程开始时,获取许可
semaphore.acquire();
System.out.println("Thread " + Num + " accessing");
Thread.sleep(100);
//访问完后释放
semaphore.release();
System.out.println("available permit = " + semaphore.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executorService.execute(runnable);
}
executorService.shutdown();
}
StampedLock
StampedLock是在ReentrantReadWriteLock读写锁基础上,解决了写线程饥饿问题的一种锁。
StampedLock是一种带标记的读写锁。它会在每一次的获取锁时设置一个标记,返回给调用者,调用者使用必须使用这个标记去释放锁。除了读写锁,还增加了乐观读操作,乐观读不会加锁,所以不会阻塞写线程,避免写线程饥饿。
为了避免乐观读之后写操作引起读到脏数据,所以需要在乐观读之后,通过返回的标记判断此次读是否有效。如果期间发生了写操作,调用者需要重新获取悲观读锁,再读一次。
//使用乐观读操作获取x、y值,防止写线程饥饿
//使用乐观读 读取数据后,需要判断是否写线程更新过数据。
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); //获得一个乐观读
double currentX = x, currentY = y; //将两个字段读入本地局部变量
if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
System.out.println("distanceFromOrigin: update to readLock");
stamp = sl.readLock(); //如果有,再次获得一个读悲观锁,防止获取到过期数据。否则就可以直接使用当前变量值
try {
currentX = x; // 将两个字段读入本地局部变量
currentY = y; // 将两个字段读入本地局部变量
} finally {
sl.unlockRead(stamp);
}
} else {
System.out.println("distanceFromOrigin: optimistic read");
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
性能测试
针对Synchronized、ReentrantReadWriteLock、StampedLock三种加锁方式进行读写性能测试。
计数100*1000,每个测试执行10遍,比较到达计数条件的平均耗时。
3个读线程,1个写线程:
Synchronized cost: 36ms
ReentrantReadWriteLock cost: 24ms
StampedLock cost: 24ms
10个读线程,10个写线程:
Synchronized cost: 8ms
ReentrantReadWriteLock cost: 255ms
StampedLock cost: 28ms
30个读线程,10个写线程:
Synchronized cost: 30ms
ReentrantReadWriteLock cost: 1826ms
StampedLock cost: 78ms
30个读线程,2个写线程:
Synchronized cost: 87ms
ReentrantReadWriteLock cost: 8504ms
StampedLock cost: 591ms
结果可以看出,当读线程逐渐增多时,由于ReentrantReadWriteLock读写锁互斥,其效率最低。StampedLock乐观读不阻塞写线程,效率比ReentrantReadWriteLock高出不少。Synchronized是表现最稳定的一个。
总结
C++层面的同步方式中,如信号量semphore、互斥体mutex采用的是进程调度来实现对临界资源的访问控制。
原子操作、自旋锁、读写锁等是通过对应硬件平台的CPU指令(如x86上的cmpxchg、arm上的ldrex/strex)独占修改变量实现的。
Java层面的同步方式中,Synchronized关键字是由JVM解析并实现,1.5版本之后对Synchronized做了大量优化,包括偏向锁、轻量级锁、自旋等待、重量级锁。其中重量级锁才会引起线程休眠,其实现为C++中的pthread_cond_wait/pthread_cond_timedwait函数,由pthread_cond_signal唤醒线程。
在JUC包中的,基于AQS机制实现的ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore等同步辅助类,在Java层面实现了公平锁和非公平锁逻辑,其线程阻塞机制与Synchronized一样,也是pthread_cond_wait等相关函数。而其获取锁和释放锁的操作也是通过CAS原子操作修改状态值实现的。