java同步锁工作原理_Java Synchronized 重量级锁原理深入剖析下(同步篇)

本文深入剖析Java中的重量级锁工作原理,探讨wait/notify/notifyAll在同步场景下的使用,分析为何需要在同步块中调用这些方法,以及它们在多线程并发中的作用。通过示例代码,揭示了wait/notify/notifyAll的内部实现,包括线程的等待、唤醒和通知过程,并提供了相关流程图以帮助理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

线程并发系列文章:

上篇分析了重量级锁在线程互斥场景下加锁、释放锁的过程,本篇将分析重量级锁在线程同步下的等待、通知机制。

通过本篇文章,你将了解到:

1、为什么 wait/notify/notifyAll 需要上锁

2、wait/notify/notifyAll 源码入口

3、Object.wait(xx) 流程解析

4、Object.notify()/Object.notifyAll() 流程解析

5、wait/notify/notifyAll 流程图

6、线程互斥同步下 锁的流程图

7、wait/notify/notifyAll 疑难点解析

1、为什么 wait/notify/notifyAll 需要上锁

一个Demo

线程同步需要在某种条件下调用wait/notify进行同步,先来看简单的例子:

public class TestThread {

static Object object = new Object();

static int count = 1;

public static void main(String args[]) {

//线程 A 消费者

new Thread(new Runnable() {

@Override

public void run() {

try {

count--;

if (count == 0) {

//count == 0 才会等待

object.wait();

}

} catch (Exception e) {

}

}

}).start();

//线程 B 生产者

new Thread(new Runnable() {

@Override

public void run() {

try {

//生产好了就通知线程A

count++;

object.notify();

} catch (Exception e) {

}

}

}).start();

}

}

上述功能很简单:线程B生产东西(增加count值),线程A消费东西(减少count值),线程A发现没东西可用了就调用wait挂起等待相应的条件满足后再次运行。

此处的条件即是:count的值。

正常情况下是:线程A等待count值,线程B通知线程A count值已经准备好了,这就是线程之间的同步。

wait 之前为什么需要获取锁

现在从多线程并发的角度来看这Demo,可能的运行顺序如下:

1、count初始值为1。

2、线程A先执行到count==0,准备调用object.wait()。

3、此时线程B已经修改好了count值,并且调用了Object.notify()。

4、线程A此时调用Object.wait()后,因为错过了Object.notify(),所以就永远阻塞于此处。

导致上面问题的原因是:count是线程间共享的,对它的修改存在并发问题,因此需要加锁来实现互斥访问count。

notify 之前为什么需要获取锁

你也许会说:既然锁是为了保护count,那么只保护对应的共享变量即可,notify可以不上锁啊。如下代码:

//线程A

synchronized (object) {

count--;

if (count == 0) {

//count == 0 才会等待

object.wait();

}

}

//线程B

synchronized (object) {

//生产好了就通知线程A

count++;

}

object.notify();

notify 的目的是将等待队列里的线程插入到同步队列里,假设是notify没有在同步代码块里,那么线程B修改count值后释放锁,因为还没有notify,因此A没有移动到同步队列里,最终无法唤醒线程A,A就会一直阻塞等待。

notifyAll也是一样的道理。

notify具体原理接下来会详细分析。

JVM如何避免不正常地调用

wait/notify/notifyAll 需要在同步块里调用,而用户不一定这么操作,因此JVM会在调用wait/notify/notifyAll 时检测当前线程是否已经获取了锁,没有锁则会抛出异常。

#ObjectMonitor.cpp

void ObjectMonitor::notify(TRAPS) {

CHECK_OWNER();

...

}

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {

...

CHECK_OWNER();

...

}

void ObjectMonitor::notifyAll(TRAPS) {

CHECK_OWNER();

...

}

#define CHECK_OWNER() \

do { \

if (THREAD != _owner) {

//当前线程不是重量级锁的获得者 \

if (THREAD->is_lock_owned((address) _owner)) { \

//当前线程是之前轻量级锁的获得者

_owner = THREAD ; /* Convert from basiclock addr to Thread addr */ \

_recursions = 0; \

OwnerIsThread = 1 ; \

} else {

//没有获取抛出异常 \

TEVENT (Throw IMSX) ; \

THROW(vmSymbols::java_lang_IllegalMonitorStateException()); \

} \

} \

} while (false)

可以看出,调用wait/notify/notifyAll的时候会调用宏CHECK_OWNER()去检测当前线程是否获取了锁,没有则抛出IllegalMonitorStateException 异常。

小结:

wait/notify/notifyAll 需要包在synchronized 同步块里的原因是保护同步的条件在并发场景下能够被正确访问。

2、wait/notify/notifyAll 源码入口

wait/notify/notifyAll 方法是声明在Java顶级类Object.java里的,通过寻找发现是native层实现的。

#Object.c

static JNINativeMethod methods[] = {

{"hashCode", "()I", (void *)&JVM_IHashCode},

{"wait", "(J)V", (void *)&JVM_MonitorWait},

{"notify", "()V", (void *)&JVM_MonitorNotify},

{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},

{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},

};

可以看到是动态注册的JNI函数。

以wait为例,继续寻找:

#jvm.cpp

JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))

...

ObjectSynchronizer::wait(obj, ms, CHECK);

JVM_END

又到了熟悉的ObjectSynchronizer类里了。

3、Object.wait(xx) 流程解析

找到了底层源码的入口,接下来就比较简单了。

#synchronizer.cpp

void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {

if (UseBiasedLocking) {

//如果是偏向锁,则撤销

BiasedLocking::revoke_and_rebias(obj, false, THREAD);

assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");

}

...

//膨胀为重量级锁

ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());

DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);

//调用ObjectMonitor的wait函数

monitor->wait(millis, true, THREAD);

...

}

此处可以看出,调用了wait函数后synchronized 膨胀为重量级锁了。

此时调用已经流转到ObjectMonitor里了。

#ObjectMonitor.cpp

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {

...

if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {

...

//调用wait 的时候线程可以被中断

THROW(vmSymbols::java_lang_InterruptedException());

return ;

}

//构造节点,封装了当前线程

ObjectWaiter node(Self);

//节点状态为TS_WAIT

node.TState = ObjectWaiter::TS_WAIT ;

Self->_ParkEvent->reset() ;

OrderAccess::fence(); // ST into Event; membar ; LD interrupted-flag

//操作等待队列需要获取锁

Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;

//将当前节点加入等待队列里------->(1)

AddWaiter (&node) ;

//释放等待队列的锁

Thread::SpinRelease (&_WaitSetLock) ;

...

//释放锁+唤醒线程------->(2)

exit (true, Self) ; // exit the monitor

...

{ // State transition wrappers

OSThread* osthread = Self->osthread();

OSThreadWaitState osts(osthread, true);

{

...

if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {

// Intentionally empty

} else

if (node._notified == 0) {

//挂起自己------->(3)

if (millis <= 0) {

Self->_ParkEvent->park () ;

} else {

ret = Self->_ParkEvent->park (millis) ;

}

}

...

} // Exit thread safepoint: transition _thread_blocked -> _thread_in_vm

//这之后是线程被唤醒后的操作

...

ObjectWaiter::TStates v = node.TState ;

if (v == ObjectWaiter::TS_RUN) {

//正常获取锁的流程

enter (Self) ;

} else {

//此时v的状态是在同步队列里--------------->(4)

ReenterI (Self, &node) ;

node.wait_reenter_end(this);

}

...

} // OSThreadWaitState()

}

列出了4个重点:

(1)

在上篇线程互斥加锁、释放锁的流程中可知:引入了同步队列(_cxq、_EntryList)。

竞争锁失败时最终会加入到同步队列里,当线程释放锁后会从同步队列里取出节点唤醒。

而在线程同步过程中,当调用wait函数后,节点会被加入到等待队列_WaitSet里。

来看看源码是如何实现的:

#ObjectMonitor.cpp

inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {

if (_WaitSet == NULL) {

//等待队列里没有节点,则当前节点被当作头节点

_WaitSet = node;

node->_prev = node;

node->_next = node;

} else {

//节点插入到队列尾部

ObjectWaiter* head = _WaitSet ;

ObjectWaiter* tail = head->_prev;

assert(tail->_next == head, "invariant check");

tail->_next = node;

head->_prev = node;

node->_next = head;

node->_prev = tail;

}

}

_pre指的是前驱节点,_next指的是后驱节点。

_WaitSet 是双向循环链表。

9e02bed1387c

image.png

如上图所示,线程A先调用wait(xx)方法,接着是线程B,最后是线程C。线程A在队列头,线程C在队列尾。

(2)

exit(xx)在上篇文章已经分析过了,其主要功能:

释放锁,然后唤醒同步队列里的节点。

(3)

还是调用ParkEvent挂起自己。

(4)

当线程被唤醒后(此时线程是在同步队列里),调用ReenterI(xx)竞争锁。

void ATTR ObjectMonitor::ReenterI (Thread * Self, ObjectWaiter * SelfNode) {

...

for (;;) {

//尝试一次加锁

if (TryLock (Self) > 0) break ;

//自旋加锁

if (TrySpin (Self) > 0) break ;

{

//加锁失败,挂起

jt->set_suspend_equivalent();

if (SyncFlags & 1) {

Self->_ParkEvent->park ((jlong)1000) ;

} else {

Self->_ParkEvent->park () ;

}

}

//被唤醒后继续加锁

if (TryLock(Self) > 0) break ;

...

}

//走到这,表示获取锁成功

//从同步队列里移出节点

UnlinkAfterAcquire (Self, SelfNode) ;

}

值得注意的是:因为已经在同步队列里,所以即使抢占锁失败后也不会加入到同步队列里了。

总结来说,调用Object.wait(xx) 方法底层主要做了四件事:

1、封装节点并加入到等待队列里。

2、释放锁并唤醒同步队列里的线程。

3、挂起自己。

4、被唤醒后继续竞争锁。

4、Object.notify()/Object.notifyAll() 流程解析

既然调用了wait(xx)后线程被挂起了,那么它什么时候被移出等待队列并且被唤醒呢?接着来看看Object.notify()。

notify 解析

与Object.wait(xx)方法入口类似:

#synchronizer.cpp

void ObjectSynchronizer::notify(Handle obj, TRAPS) {

if (UseBiasedLocking) {

//偏向锁,先撤销

BiasedLocking::revoke_and_rebias(obj, false, THREAD);

}

markOop mark = obj->mark();

if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {

//是轻量级锁并且锁被当前线程持有的话,直接退出。

return;

}

//膨胀为重量级锁,并调用ObjectMonitor.notify()函数

ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);

}

最终还是调用了ObjectMonitor.notify()函数:

void ObjectMonitor::notify(TRAPS) {

...

if (_WaitSet == NULL) {

//等待队列为空,直接返回

return ;

}

//notify 策略,默认是2

int Policy = Knob_MoveNotifyee ;

//获取操作等待队列的锁

Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;

//将队头节点移出队列-------->(1)

ObjectWaiter * iterator = DequeueWaiter() ;

if (iterator != NULL) {

...

ObjectWaiter * List = _EntryList ;

if (List != NULL) {

//修改节点状态为TS_ENTER

...

}

if (Policy == 0) { // prepend to EntryList

//插入_EntryList 头部

...

} else

if (Policy == 1) { // append to EntryList

//插入到_EntryList 尾部

...

} else

if (Policy == 2) { // prepend to cxq

// 默认策略-------------->(2)

if (List == NULL) {

//_EntryList 为空,则将节点插入到_EntryList 头

iterator->_next = iterator->_prev = NULL ;

_EntryList = iterator ;

} else {

//修改状态为TS_CXQ

iterator->TState = ObjectWaiter::TS_CXQ ;

for (;;) {

ObjectWaiter * Front = _cxq ;

iterator->_next = Front ;

//插入到_cxq头部

if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {

break ;

}

}

}

} else

if (Policy == 3) { // append to cxq

//插入到_cxq尾部

...

} else {

//都不满足,直接唤醒线程

ParkEvent * ev = iterator->_event ;

iterator->TState = ObjectWaiter::TS_RUN ;

OrderAccess::fence() ;

ev->unpark() ;

}

...

}

//释放等待队列的锁

Thread::SpinRelease (&_WaitSetLock) ;

...

}

依然列出了两个重点:

(1)

将节点从等待队列里移出:

#ObjectMonitor.cpp

inline ObjectWaiter* ObjectMonitor::DequeueWaiter() {

//取队头节点

ObjectWaiter* waiter = _WaitSet;

if (waiter) {

DequeueSpecificWaiter(waiter);

}

return waiter;

}

inline void ObjectMonitor::DequeueSpecificWaiter(ObjectWaiter* node) {

...

ObjectWaiter* next = node->_next;

if (next == node) {

//后驱节点与当前节点一致,说明当前等待队列里只有一个节点

//队列置空

_WaitSet = NULL;

} else {

//从队列里移除当前节点,当前节点是队头节点

ObjectWaiter* prev = node->_prev;

next->_prev = prev;

prev->_next = next;

if (_WaitSet == node) {

//将_WaitSet往后移动,指向下一个节点

_WaitSet = next;

}

}

node->_next = NULL;

node->_prev = NULL;

}

(2)

将队头节点从等待队列里取出后,该怎么移动到同步队列里(_cxq、_EntryList)不同策略有不同的操作,以默认策略为例:

1、如果_EntryList为空,则将节点加入到_EntryList队列头部。

2、否则将节点加入到_cxq队列头部。

还是以线程A、B、C为例,当三者调用wait(xx)方法阻塞自己后,等待队列节点顺序为:A-->B-->C(从头到尾),现在另一个线程D调用notify()方法后,等待队列如下:

9e02bed1387c

image.png

总结来说,调用Object.notify() 方法底层主要就做了一件事:

将节点从等待队列里移出并加入到同步队列里。

同时通过源码也解释了两个问题:

1、notify 操作没有释放锁。

2、notify 操作正常流程下没有唤醒线程。

notifyAll 解析

顾名思义,就是通知所有的等待节点。

notify是将单个节点从等待队列挪到同步队列,而notifyAll是将等待队列的节点逐个全部挪到同步队列,具体的代码就不贴了,主要看看默认策略下的处理:

#ObjectMonitor.cpp

if (Policy == 2) { // prepend to cxq

// prepend to cxq

iterator->TState = ObjectWaiter::TS_CXQ ;

for (;;) {

ObjectWaiter * Front = _cxq ;

iterator->_next = Front ;

if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {

break ;

}

}

}

与notify不同的是,此处是直接插入到_cxq头部了,也就是说原本在等待队列末尾的节点反而排在了同步队列的前面。

5、wait/notify/notifyAll 流程图

至此,三者原理已经剖析完毕,将三者关系用图串联起来:

9e02bed1387c

image.png

6、线程互斥同步下 锁的流程图

结合上篇、本篇文章,已经将互斥与同步下锁的实现流程分析过了,接下来将这两者结合起来看,希望我们能从全局中把控整个流程。

先看一段伪代码:

//线程A调用

synchronized(object) {

object.wait();

//doSomething

}

//线程B调用

synchronized(object) {

object.notify();

//doSomething

}

之前你兴许会有以下疑惑:

1、线程A成功获取锁后,线程B再次获取锁会失败,线程B该如何自处?

2、线程A成功获取锁后,调用wait()方法,此时线程A处在什么状态?

3、线程A退出临界区释放锁后,又做了什么?

4、线程B调用notify()方法,发生了什么?

...

1、线程A成功获取锁后,线程B再次获取锁会失败,线程B该如何自处?

答:

线程B获取锁失败将自己插入到同步队列里,同步队列包括两个队列:_cxq和_EntryList。

此时插入到_cxq的队列的头部。

线程B将自己挂起。

2、线程A成功获取锁后,调用wait()方法,此时线程A处在什么状态?

答:

线程A将自己加入到等待队列了(_WaitSet),方式是插入到等待队列的头部。

释放占用的锁。

线程A将自己挂起。

3、线程A退出临界区释放锁后,又做了什么?

答:

线程A退出临界区后,先释放锁。

然后从同步队列里唤醒等待锁的线程。

根据不同的策略有不同的处理方式,以默认方式为例:

若是_EntryList队列不为空,则取出_EntryList队头节点并唤醒。

若是_EntryList为空,将_EntryList指向_cxq,并取出队头节点唤醒。

4、线程B调用notify()方法,发生了什么?

答:

线程B将等待队列里的头节点取出,并插入到同步队列里。

根据不同的策略有不同的处理方式,以默认方式为例:

如果_EntryList为空,则将节点加入到_EntryList队列头部。

否则将节点加入到_cxq队列头部。

最后用图表示如下:

9e02bed1387c

image.png

总结以下,重量级锁的理解核心:

1、锁住的是ObjectMonitor里的_owner字段。

2、操作的是同步队列(_cxq/_EntryList)和等待队列(_WaitSet)。

7、wait/notify/notifyAll 疑难点解析

网上有很多解释重量级锁相关知识的文章,有些解释可能比较牵强。通过本篇的源码分析,相信你已经能够辨别,下面举例几个常出现的疑难点:

问:notify 唤醒线程是无序的吗?

答:

notify在正常的流程下并不会唤醒线程,而只是将等待队列里的节点根据某个策略挪动到同步队列里。当某个线程释放锁后,会根据某个模式唤醒同步队列里的线程(具体模式/策略请查看第六点分析)。

问:notify/notifyAll 唤醒的例子

答:

public class TestThread {

static Object object = new Object();

static Thread a, b, c;

public static void main(String args[]) {

a = new Thread(new Runnable() {

@Override

public void run() {

try {

synchronized (object) {

System.err.println("A before wait " + System.nanoTime());

b.start();

Thread.sleep(1000);

object.wait();

System.err.println("A after wait " + System.nanoTime());

}

} catch (Exception e) {

}

}

});

a.start();

b = new Thread(new Runnable() {

@Override

public void run() {

try {

synchronized (object) {

System.err.println("B before wait " + System.nanoTime());

c.start();

Thread.sleep(1000);

object.wait();

System.err.println("B after wait " + System.nanoTime());

}

} catch (Exception e) {

}

}

});

c = new Thread(new Runnable() {

@Override

public void run() {

try {

synchronized (object) {

System.err.println("C before wait " + System.nanoTime());

object.notify();

object.notify();

System.err.println("C after wait " + + System.nanoTime());

}

} catch (Exception e) {

}

}

});

}

}

如上,有线程A、B、C,A启动B、B启动C。

打印如下:

9e02bed1387c

image.png

A比B先进同步队列,因此第一个notify的时候先将A移动到_EntryList里,第二个notify将B移动到_cxq头部,最后唤醒的时候优先从_EntryList里取,再从_cxq取。

当将两个notify换成一个notifyAll的时候,结果如下:

9e02bed1387c

image.png

调用notifyAll的时候和单独调用多次notify的结果是相反的,这里是先唤醒了B,再唤醒了A,与我们之前的理论分析一致。

问:什么是虚假唤醒?

答:

public class TestThread {

static Object object = new Object();

static Thread a, b, c;

static int count = 0;

public static void main(String args[]) {

a = new Thread(new Runnable() {

@Override

public void run() {

try {

synchronized (object) {

System.err.println("A before wait " + System.nanoTime());

if (count == 0)

object.wait();

count--;

System.err.println("A count:" + count + " " + System.nanoTime());

}

} catch (Exception e) {

}

}

});

a.start();

b = new Thread(new Runnable() {

@Override

public void run() {

try {

synchronized (object) {

System.err.println("B before wait " + System.nanoTime());

if (count == 0)

object.wait();

count--;

System.err.println("B count:" + count + " " + System.nanoTime());

}

} catch (Exception e) {

}

}

});

b.start();

try {

//尽量确保线程A、B都已经运行

Thread.sleep(1000);

} catch (Exception e) {

}

c = new Thread(new Runnable() {

@Override

public void run() {

try {

synchronized (object) {

System.err.println("C before wait " + System.nanoTime());

count++;

object.notifyAll();

System.err.println("C after wait " + +System.nanoTime());

}

} catch (Exception e) {

}

}

});

c.start();

}

}

如上有线程A、B、C。

A、B开启后判断count == 0,于是调用wait()进行挂起,C修改(生产)count后通知所有等待的线程,A、B被唤醒后修改(消费)count,最后打印如下:

9e02bed1387c

image.png

可以看到,A被唤醒后拿到的count==-1,这并不是想要的数值。想象一下,若是C往队列里添加了一个元素,A、B被唤醒后都从队列里取出元素,现在元素已经被B取出了,A再取的时候会发生异常。

这就是大家熟知的虚假唤醒,修改一下条件即可预防此种问题:

System.err.println("A before wait " + System.nanoTime());

while (count == 0)

object.wait();

count--;

System.err.println("A count:" + count + " " + System.nanoTime());

当线程被唤醒后,继续查看条件变量,不满足就再次挂起。

当然也不一定非得要加while,要看具体场景,比方说只有一个线程A调用wait,另一个线程B调用notify,这时候的A里没必要加。如果你不确定是否需要加或者不想区分场景是否加,那最好加上,毕竟也只是多了一次判断而已,更加保险。

至此,Synchronized相关知识已经分析完毕,接下来将重点分析AQS,并横向和Synchronized比较。

本文源码基于jdk1.8,运行环境也是jdk1.8。

因此上述demo在你的环境下可能有不同的效果,请注意甄别。虽然不同版本可能有不同的策略,但是核心思想都是一致的。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值