Java锁的概念在内部是如何工作的?

25

每个Object类的实例(及其子类)都拥有一个锁,该锁在进入synchronized方法时获取,并在退出时自动释放。

这是否意味着我们创建的任何对象实例都默认内部具有一个“锁”(作为字段实现)?

我对这个“锁”的概念感到困惑,我想知道它在内部实际上是做什么的。

能否指导我去一些可找到更多信息的地方?


http://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html - JB Nizet
2
https://dev59.com/M2855IYBdhLWcg3w75Eu - Brian Roach
3个回答

26
作为常规,JLS提供了答案(17.1)
其中最基本的方法是同步,它使用监视器实现。Java中的每个对象都与一个监视器相关联,线程可以锁定或解锁该监视器。一次只有一个线程可以持有监视器上的锁。任何试图锁定该监视器的其他线程都将被阻塞,直到它们可以获得该监视器的锁。线程t可以多次锁定特定的监视器;每次解锁将撤销一个锁操作的效果。
因此,lock不像Object中的字段(如您可以通过简单查看Object的源代码来看到的那样)。相反,每个Object都与一个“监视器”相关联,这就是要锁定或解锁的监视器。
我想指出一个更详细的参考资料,介绍了“Java是如何做到的”,以确保不被忽视。这位用户在下面发现了C++代码的评论中提到了该资料,我鼓励所有点赞都给他的答案。您可以在提供的链接中查看完整的源代码。
  126 // -----------------------------------------------------------------------------
  127 // Theory of operations -- Monitors lists, thread residency, etc:
  128 //
  129 // * A thread acquires ownership of a monitor by successfully
  130 //   CAS()ing the _owner field from null to non-null.
  131 //
  132 // * Invariant: A thread appears on at most one monitor list --
  133 //   cxq, EntryList or WaitSet -- at any one time.
  134 //
  135 // * Contending threads "push" themselves onto the cxq with CAS
  136 //   and then spin/park.
  137 //
  138 // * After a contending thread eventually acquires the lock it must
  139 //   dequeue itself from either the EntryList or the cxq.
  140 //
  141 // * The exiting thread identifies and unparks an "heir presumptive"
  142 //   tentative successor thread on the EntryList.  Critically, the
  143 //   exiting thread doesn't unlink the successor thread from the EntryList.
  144 //   After having been unparked, the wakee will recontend for ownership of
  145 //   the monitor.   The successor (wakee) will either acquire the lock or
  146 //   re-park itself.
  147 //
  148 //   Succession is provided for by a policy of competitive handoff.
  149 //   The exiting thread does _not_ grant or pass ownership to the
  150 //   successor thread.  (This is also referred to as "handoff" succession").
  151 //   Instead the exiting thread releases ownership and possibly wakes
  152 //   a successor, so the successor can (re)compete for ownership of the lock.
  153 //   If the EntryList is empty but the cxq is populated the exiting
  154 //   thread will drain the cxq into the EntryList.  It does so by
  155 //   by detaching the cxq (installing null with CAS) and folding
  156 //   the threads from the cxq into the EntryList.  The EntryList is
  157 //   doubly linked, while the cxq is singly linked because of the
  158 //   CAS-based "push" used to enqueue recently arrived threads (RATs).
  159 //
  160 // * Concurrency invariants:
  161 //
  162 //   -- only the monitor owner may access or mutate the EntryList.
  163 //      The mutex property of the monitor itself protects the EntryList
  164 //      from concurrent interference.
  165 //   -- Only the monitor owner may detach the cxq.
  166 //
  167 // * The monitor entry list operations avoid locks, but strictly speaking
  168 //   they're not lock-free.  Enter is lock-free, exit is not.
  169 //   See http://j2se.east/~dice/PERSIST/040825-LockFreeQueues.html
  170 //
  171 // * The cxq can have multiple concurrent "pushers" but only one concurrent
  172 //   detaching thread.  This mechanism is immune from the ABA corruption.
  173 //   More precisely, the CAS-based "push" onto cxq is ABA-oblivious.
  174 //
  175 // * Taken together, the cxq and the EntryList constitute or form a
  176 //   single logical queue of threads stalled trying to acquire the lock.
  177 //   We use two distinct lists to improve the odds of a constant-time
  178 //   dequeue operation after acquisition (in the ::enter() epilog) and
  179 //   to reduce heat on the list ends.  (c.f. Michael Scott's "2Q" algorithm).
  180 //   A key desideratum is to minimize queue & monitor metadata manipulation
  181 //   that occurs while holding the monitor lock -- that is, we want to
  182 //   minimize monitor lock holds times.  Note that even a small amount of
  183 //   fixed spinning will greatly reduce the # of enqueue-dequeue operations
  184 //   on EntryList|cxq.  That is, spinning relieves contention on the "inner"
  185 //   locks and monitor metadata.
  186 //
  187 //   Cxq points to the the set of Recently Arrived Threads attempting entry.
  188 //   Because we push threads onto _cxq with CAS, the RATs must take the form of
  189 //   a singly-linked LIFO.  We drain _cxq into EntryList  at unlock-time when
  190 //   the unlocking thread notices that EntryList is null but _cxq is != null.
  191 //
  192 //   The EntryList is ordered by the prevailing queue discipline and
  193 //   can be organized in any convenient fashion, such as a doubly-linked list or
  194 //   a circular doubly-linked list.  Critically, we want insert and delete operations
  195 //   to operate in constant-time.  If we need a priority queue then something akin
  196 //   to Solaris' sleepq would work nicely.  Viz.,
  197 //   http://agg.eng/ws/on10_nightly/source/usr/src/uts/common/os/sleepq.c.
  198 //   Queue discipline is enforced at ::exit() time, when the unlocking thread
  199 //   drains the cxq into the EntryList, and orders or reorders the threads on the
  200 //   EntryList accordingly.
  201 //
  202 //   Barring "lock barging", this mechanism provides fair cyclic ordering,
  203 //   somewhat similar to an elevator-scan.
  204 //
  205 // * The monitor synchronization subsystem avoids the use of native
  206 //   synchronization primitives except for the narrow platform-specific
  207 //   park-unpark abstraction.  See the comments in os_solaris.cpp regarding
  208 //   the semantics of park-unpark.  Put another way, this monitor implementation
  209 //   depends only on atomic operations and park-unpark.  The monitor subsystem
  210 //   manages all RUNNING->BLOCKED and BLOCKED->READY transitions while the
  211 //   underlying OS manages the READY<->RUN transitions.
  212 //
  213 // * Waiting threads reside on the WaitSet list -- wait() puts
  214 //   the caller onto the WaitSet.
  215 //
  216 // * notify() or notifyAll() simply transfers threads from the WaitSet to
  217 //   either the EntryList or cxq.  Subsequent exit() operations will
  218 //   unpark the notifyee.  Unparking a notifee in notify() is inefficient -
  219 //   it's likely the notifyee would simply impale itself on the lock held
  220 //   by the notifier.
  221 //
  222 // * An interesting alternative is to encode cxq as (List,LockByte) where
  223 //   the LockByte is 0 iff the monitor is owned.  _owner is simply an auxiliary
  224 //   variable, like _recursions, in the scheme.  The threads or Events that form
  225 //   the list would have to be aligned in 256-byte addresses.  A thread would
  226 //   try to acquire the lock or enqueue itself with CAS, but exiting threads
  227 //   could use a 1-0 protocol and simply STB to set the LockByte to 0.
  228 //   Note that is is *not* word-tearing, but it does presume that full-word
  229 //   CAS operations are coherent with intermix with STB operations.  That's true
  230 //   on most common processors.
  231 //
  232 // * See also http://blogs.sun.com/dave
  233 
  234 
  235 // -----------------------------------------------------------------------------

这是否意味着,如果我有100个对象,就有100个监视器? - nish1013
2
@nish1013 因为任何“对象”都可以充当锁,而且每个“对象”都充当单独的锁,所以是的。 - asteri
监视器就像一个字段。 - Tom Hawtin - tackline
1
这是对监视器锁规范的明确说明,但并没有回答问题,即它们如何被实现。 - Raedwald
@Raedwald 很好的观点。我刚刚看了selig的答案,他成功找到了原生的C++代码。给他加1分。 :) - asteri
显示剩余3条评论

24
另一个答案描述了语言定义,但没有描述“内部发生的事情”。
Java中的每个对象都有两个字的对象头。标记字和klass指针。第一个字(标记字)用于存储锁定信息和缓存哈希码。第二个字是指向klass对象的指针,该对象存储该对象的静态信息(包括方法代码)。
HotSpot JVM具有一些花哨的锁定功能,包括薄锁和偏向锁,这基本上意味着如果您从未锁定对象或从未有任何争用,则永远不会创建监视器对象(存储额外锁定信息的对象)。
监视器对象具有条目集。当您锁定该对象时,如果对象已被锁定,则将您的线程添加到条目集中。解锁对象时,您会唤醒条目集中的一个线程。
并发是一个非常复杂的领域,显然还有很多细节。
更新

对象头在这里有解释,关于对象监视器(如等待集)的详细信息可以在此处的OpenJDK代码中找到。


你有关于你所说的话的参考资料吗?在我看来,OpenJDK文档/代码会是一个不错的补充。 - Thomas Jungblut
在维基百科中,监视器是锁+条件变量。为什么Java称一个简单的Murex锁为监视器? - overexchange

-3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接