每日一面 - java里的wait()和sleep()的区别有哪些?

简介: 每日一面 - java里的wait()和sleep()的区别有哪些?

一句话总结:sleep方法是当前线程休眠,让出cpu,不释放锁,这是Thread的静态方法;wait方法是当前线程等待,释放锁,这是Object的方法。同时要注意,Java 14 之后引入的 inline class 是没有 wait 方法的


Sleep()原理


public static native void sleep(long millis) throws InterruptedException;

sleep()是Thread中的static方法,也是native实现。就是调用底层的 sleep 函数实现:

void THREAD_sleep(int seconds) {
#ifdef windows
    Sleep(1000L * seconds);
#else
    sleep(seconds);
#endif
}

linux的sleep函数参考 sleep: https://man7.org/linux/man-pages/man3/sleep.3.html


wait(), notify(), notifyAll()


这些属于基本的Java多线程同步类的API,都是native实现:

public final native void wait(long timeout) throws InterruptedException;
public final native void notify();
public final native void notifyAll();

那么底层实现是怎么回事呢? 首先我们需要先明确JDK底层实现共享内存锁的基本机制。 每个Object都有一个ObjectMonitor,这个ObjectMonitor中包含三个特殊的数据结构,分别是CXQ(实际上是Contention List),EntryList还有WaitSet;一个线程在同一时间只会出现在他们三个中的一个中。首先来看下CXQ:


微信图片_20220624205047.jpg


一个尝试获取Object锁的线程,如果首次尝试(就是尝试CAS更新轻量锁)失败,那么会进入CXQ;进入的方法就是CAS更新CXQ指针指向自己,如果成功,自己的next指向剩余队列;CXQ是一个LIFO队列,设计成LIFO主要是为了:

  1. 进入CXQ队列后,每个线程先进入一段时间的spin自旋状态,尝试获取锁,获取失败的话则进入park状态。这个自旋的意义在于,假设锁的hold时间非常短,如果直接进入park状态的话,程序在用户态和系统态之间的切换会影响锁性能。这个spin可以减少切换;
  2. 进入spin状态如果成功获取到锁的话,需要出队列,出队列需要更新自己的头指针,如果位于队列前列,那么需要操作的时间会减少 但是,如果全部依靠这个机制,那么理所当然的,CAS更新队列头的操作会非常频繁。所以,引入了EntryList来减少争用:


微信图片_20220624205111.jpg


假设Thread A是当前锁的Owner,接下来他要释放锁了,那么如果EntryList为null并且cxq不为null,就会从cxq末尾取出一个线程,放入EntryList(注意,EntryList为双向队列),并且标记EntryList其中一个线程为Successor(一般是头节点,这个EntryList的大小可能大于一,一般在notify时,后面会说到),这个Successor接下来会进入spin状态尝试获取锁(注意,在第一次自旋过去后,之后线程一直处于park状态)。如果获取成功,则成为owner,否则,回到EntryList中。 这种利用两个队列减少争用的算法,可以参考: Michael Scott's "2Q" algorithm 接下来,进入我们的正题,wait方法。如果一个线程成为owner后,执行了wait方法,则会进入WaitSet: Object.wait()底层实现


微信图片_20220624205130.jpg


void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
    //检查线程合法性
    Thread *const Self = THREAD;
    assert(Self->is_Java_thread(), "Must be Java thread!");
    JavaThread *jt = (JavaThread *) THREAD;
    DeferredInitialize();
    //检查当前线程是否拥有锁
    CHECK_OWNER();
    EventJavaMonitorWait event;
    // 检查中断位
    if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
        if (JvmtiExport::should_post_monitor_waited()) {
            JvmtiExport::post_monitor_waited(jt, this, false);
        }
        if (event.should_commit()) {
            post_monitor_wait_event(&event, 0, millis, false);
        }
        TEVENT(Wait - ThrowIEX);
        THROW(vmSymbols::java_lang_InterruptedException());
        return;
    }
    TEVENT(Wait);
    assert(Self->_Stalled == 0, "invariant");
    Self->_Stalled = intptr_t(this);
    jt->set_current_waiting_monitor(this);
    //建立放入WaitSet中的这个线程的封装对象
    ObjectWaiter node(Self);
    node.TState = ObjectWaiter::TS_WAIT;
    Self->_ParkEvent->reset();
    OrderAccess::fence();
    //用自旋方式获取操作waitset的lock,因为一般只有owner线程会操作这个waitset(无论是wait还是notify),所以竞争概率很小(除非响应interrupt事件才会有争用),采用spin方式效率高
    Thread::SpinAcquire(&_WaitSetLock, "WaitSet - add");
    //添加到waitset
    AddWaiter(&node);
    //释放锁,代表现在线程已经进入了waitset,接下来要park了
    Thread::SpinRelease(&_WaitSetLock);
    if ((SyncFlags & 4) == 0) {
        _Responsible = NULL;
    }
    intptr_t save = _recursions; // record the old recursion count
    _waiters++;                  // increment the number of waiters
    _recursions = 0;             // set the recursion level to be 1
    exit(true, Self);                    // exit the monitor
    guarantee(_owner != Self, "invariant");
    // 确保没有unpark事件冲突影响本次park,方法就是主动post一次unpark
    if (node._notified != 0 && _succ == Self) {
        node._event->unpark();
    }
    // 接下来就是park操作了
    。。。。。。。。。
    。。。。。。。。。
}

当另一个owner线程调用notify时,根据Knob_MoveNotifyee这个值,决定将从waitset里面取出的一个线程放到哪里(cxq或者EntrySet)Object.notify()底层实现

void ObjectMonitor::notify(TRAPS) {
    //检查当前线程是否拥有锁
    CHECK_OWNER();
    if (_WaitSet == NULL) {
        TEVENT(Empty - Notify);
        return;
    }
    DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);
    //决定取出来的线程放在哪里
    int Policy = Knob_MoveNotifyee;
    //同样的,用自旋方式获取操作waitset的lock
    Thread::SpinAcquire(&_WaitSetLock, "WaitSet - notify");
    ObjectWaiter *iterator = DequeueWaiter();
    if (iterator != NULL) {
        TEVENT(Notify1 - Transfer);
        guarantee(iterator->TState == ObjectWaiter::TS_WAIT, "invariant");
        guarantee(iterator->_notified == 0, "invariant");
        if (Policy != 4) {
            iterator->TState = ObjectWaiter::TS_ENTER;
        }
        iterator->_notified = 1;
        Thread *Self = THREAD;
        iterator->_notifier_tid = Self->osthread()->thread_id();
        ObjectWaiter *List = _EntryList;
        if (List != NULL) {
            assert(List->_prev == NULL, "invariant");
            assert(List->TState == ObjectWaiter::TS_ENTER, "invariant");
            assert(List != iterator, "invariant");
        }
        if (Policy == 0) {       // prepend to EntryList
            if (List == NULL) {
                iterator->_next = iterator->_prev = NULL;
                _EntryList = iterator;
            } else {
                List->_prev = iterator;
                iterator->_next = List;
                iterator->_prev = NULL;
                _EntryList = iterator;
            }
        } else if (Policy == 1) {      // append to EntryList
            if (List == NULL) {
                iterator->_next = iterator->_prev = NULL;
                _EntryList = iterator;
            } else {
                // CONSIDER:  finding the tail currently requires a linear-time walk of
                // the EntryList.  We can make tail access constant-time by converting to
                // a CDLL instead of using our current DLL.
                ObjectWaiter *Tail;
                for (Tail = List; Tail->_next != NULL; Tail = Tail->_next);
                assert(Tail != NULL && Tail->_next == NULL, "invariant");
                Tail->_next = iterator;
                iterator->_prev = Tail;
                iterator->_next = NULL;
            }
        } else if (Policy == 2) {      // prepend to cxq
            // prepend to cxq
            if (List == NULL) {
                iterator->_next = iterator->_prev = NULL;
                _EntryList = iterator;
            } else {
                iterator->TState = ObjectWaiter::TS_CXQ;
                for (;;) {
                    ObjectWaiter *Front = _cxq;
                    iterator->_next = Front;
                    if (Atomic::cmpxchg_ptr(iterator, &_cxq, Front) == Front) {
                        break;
                    }
                }
            }
        } else if (Policy == 3) {      // append to cxq
            iterator->TState = ObjectWaiter::TS_CXQ;
            for (;;) {
                ObjectWaiter *Tail;
                Tail = _cxq;
                if (Tail == NULL) {
                    iterator->_next = NULL;
                    if (Atomic::cmpxchg_ptr(iterator, &_cxq, NULL) == NULL) {
                        break;
                    }
                } else {
                    while (Tail->_next != NULL) Tail = Tail->_next;
                    Tail->_next = iterator;
                    iterator->_prev = Tail;
                    iterator->_next = NULL;
                    break;
                }
            }
        } else {
            ParkEvent *ev = iterator->_event;
            iterator->TState = ObjectWaiter::TS_RUN;
            OrderAccess::fence();
            ev->unpark();
        }
        if (Policy < 4) {
            iterator->wait_reenter_begin(this);
        }
        // _WaitSetLock protects the wait queue, not the EntryList.  We could
        // move the add-to-EntryList operation, above, outside the critical section
        // protected by _WaitSetLock.  In practice that's not useful.  With the
        // exception of  wait() timeouts and interrupts the monitor owner
        // is the only thread that grabs _WaitSetLock.  There's almost no contention
        // on _WaitSetLock so it's not profitable to reduce the length of the
        // critical section.
    }
    //释放waitset的lock
    Thread::SpinRelease(&_WaitSetLock);
    if (iterator != NULL && ObjectMonitor::_sync_Notifications != NULL) {
        ObjectMonitor::_sync_Notifications->inc();
    }
}

对于NotifyAll就很好推测了,这里不再赘述;

相关文章
|
28天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
67 14
|
24天前
|
Java
java中面向过程和面向对象区别?
java中面向过程和面向对象区别?
22 1
|
1月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
54 8
|
1月前
|
安全 Java
Java中WAIT和NOTIFY方法调用时机的深层解析
在Java多线程编程中,`wait()`和`notify()`方法的正确使用对于线程间的协调至关重要。这两个方法必须在同步块或同步方法中调用,这一规定的深层原因是什么呢?本文将深入探讨这一机制。
41 5
|
1月前
|
安全 Java 开发者
Java中WAIT和NOTIFY方法必须在同步块中调用的原因
在Java多线程编程中,`wait()`和`notify()`方法是实现线程间协作的关键。这两个方法必须在同步块或同步方法中调用,这一要求背后有着深刻的原因。本文将深入探讨为什么`wait()`和`notify()`方法必须在同步块中调用,以及这一机制如何确保线程安全和避免死锁。
44 4
|
1月前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
70 9
|
1月前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
40 3
|
3天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
5天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
5天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。