java并发的一些细节

并发的细节

本文只想说明3个结论

1、synchronized关键字使用MONITORENTER、MONITOREXIT指令实现。

2、多个线程由于synchronized等待,那么执行顺序是FILO。

3、使用ReentrantLock是正常队列FIFO顺序。

如果您对细节感兴趣可以细看。

示例

关键java并发的一个小细节。先从一段代码说起。

	public void doWork(String id) {
    synchronized (this) {
			if (StringUtils.isEmpty(id)) { try { Thread.sleep(1000 * 10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()); } }

synchronized原理

上面是一段非常简单控制并发的java代码。从class字节码可以看到使用了MONITORENTER、MONITOREXIT两个关键指令。

  // access flags 0x1
  public doWork(Ljava/lang/String;)V //对应方法 // parameter id TRYCATCHBLOCK L0 L1 L2 java/lang/InterruptedException TRYCATCHBLOCK L3 L4 L5 null TRYCATCHBLOCK L5 L6 L5 null L7 LINENUMBER 7 L7 ALOAD 0 //加载this变量 DUP //运行区数据作为Raw Type数据,压入栈顶 ASTORE 2 //this 到局部变量3 MONITORENTER //对应的synchronized L3 LINENUMBER 8 L3 ALOAD 1 //加载局部变量 id INVOKESTATIC org/apache/commons/lang3/StringUtils.isEmpty (Ljava/lang/CharSequence;)Z //调用静态方法 IFEQ L8 //通过栈顶数值判断 L0 LINENUMBER 10 L0 LDC 10000 //加载常量至栈顶 INVOKESTATIC java/lang/Thread.sleep (J)V //静态方法 L1 LINENUMBER 13 L1 GOTO L8 L2 LINENUMBER 11 L2 FRAME FULL [com/ruijie/groupview/port/adpter/web/contorllers/A java/lang/String java/lang/Object] [java/lang/InterruptedException] ASTORE 3 //第三个变量到栈顶 L9 LINENUMBER 12 L9 //打印异常信息 ALOAD 3 //加载第三个变量 INVOKEVIRTUAL java/lang/InterruptedException.printStackTrace ()V L8 // 打印线程名 LINENUMBER 15 L8 FRAME SAME GETSTATIC java/lang/System.out : Ljava/io/PrintStream; INVOKESTATIC java/lang/Thread.currentThread ()Ljava/lang/Thread; INVOKEVIRTUAL java/lang/Thread.getName ()Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L10 LINENUMBER 16 L10 //原文第16行,方法执行完毕。 ALOAD 2 MONITOREXIT //退出monitor L4 GOTO L11 L5 FRAME SAME1 java/lang/Throwable ASTORE 4 ALOAD 2 MONITOREXIT //异常退出monitor L6 ALOAD 4 ATHROW //异常 L11 LINENUMBER 17 L11 FRAME CHOP 1 RETURN //返回 L12 //常量 LOCALVARIABLE e Ljava/lang/InterruptedException; L9 L8 3 LOCALVARIABLE this Lcom/ruijie/groupview/port/adpter/web/contorllers/A; L7 L12 0 LOCALVARIABLE id Ljava/lang/String; L7 L12 1 MAXSTACK = 2 MAXLOCALS = 5

synchronized使用java底层的monitor关键指令实现。也被称为管程的方式实现控制并发。

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使 用管程(Monitor)来支持的。 方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作(§2.11.8)之中。虚拟机可以从方法常量池中的方法表结构(method_info Structure,§4.6) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程, 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期 间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法 之外时自动释放。
同步一段指令集序列通常是由 Java 语言中的 synchronized 块来表示的,Java 虚拟机的 指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义, 正确实现 synchronized 关键字需要编译器与 Java 虚拟机两者协作支持(读者可参见§3.14 中关于同步的描述)。
结构化锁定(Structured Locking)是指在方法调用期间每一个管程退出都与前面的管程 进入相匹配的情形。因为无法保证所有提交给 Java 虚拟机执行的代码都满足结构化锁定,所以 Java 虚拟机允许(但不强制要求)通过以下两条规则来保证结构化锁定成立。假设 T 代表一条线 程,M 代表一个管程的话:
1. T 在方法执行时持有管程 M 的次数必须与 T 在方法完成(包括正常和非正常完成)时释 放管程 M 的次数相等。
2. 找方法调用过程中,任何时刻都不会出现线程 T 释放管程 M 的次数比 T 持有管程 M 次数 多的情况。
  请注意,在同步方法调用时自动持有和释放管程的过程也被认为是在方法调用期间发生。
 
--引用java虚拟机规范。

jdk的处理

synchronized由于底层使用类似stack的结构,因此执行顺序是FILO。

//openjdk中源码,进入锁的方法
void ObjectMonitor::EnterI(TRAPS) {
  Thread * const Self = THREAD;
  assert(Self->is_Java_thread(), "invariant"); assert(((JavaThread *) Self)->thread_state() == _thread_blocked, "invariant"); // Try the lock - TATAS if (TryLock (Self) > 0) { assert(_succ != Self, "invariant"); assert(_owner == Self, "invariant"); assert(_Responsible != Self, "invariant"); return; } DeferredInitialize(); // We try one round of spinning *before* enqueueing Self. // // If the _owner is ready but OFFPROC we could use a YieldTo() // operation to donate the remainder of this thread's quantum // to the owner. This has subtle but beneficial affinity // effects. if (TrySpin (Self) > 0) { assert(_owner == Self, "invariant"); assert(_succ != Self, "invariant"); assert(_Responsible != Self, "invariant"); return; } // The Spin failed -- Enqueue and park the thread ... assert(_succ != Self, "invariant"); assert(_owner != Self, "invariant"); assert(_Responsible != Self, "invariant"); // Enqueue "Self" on ObjectMonitor's _cxq. // // Node acts as a proxy for Self. // As an aside, if were to ever rewrite the synchronization code mostly // in Java, WaitNodes, ObjectMonitors, and Events would become 1st-class // Java objects. This would avoid awkward lifecycle and liveness issues, // as well as eliminate a subset of ABA issues. // TODO: eliminate ObjectWaiter and enqueue either Threads or Events. //ObjectWaiter 为等待对象。 ObjectWaiter node(Self); Self->_ParkEvent->reset(); node._prev = (ObjectWaiter *) 0xBAD; node.TState = ObjectWaiter::TS_CXQ; // Push "Self" onto the front of the _cxq. // Once on cxq/EntryList, Self stays on-queue until it acquires the lock. // Note that spinning tends to reduce the rate at which threads // enqueue and dequeue on EntryList|cxq. ObjectWaiter * nxt; for (;;) { node._next = nxt = _cxq; if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt) break; // Interference - the CAS failed because _cxq changed. Just retry. // As an optional optimization we retry the lock. if (TryLock (Self) > 0) { assert(_succ != Self, "invariant"); assert(_owner == Self, "invariant"); assert(_Responsible != Self, "invariant"); return; } } // Check for cxq|EntryList edge transition to non-null. This indicates // the onset of contention. While contention persists exiting threads // will use a ST:MEMBAR:LD 1-1 exit protocol. When contention abates exit // operations revert to the faster 1-0 mode. This enter operation may interleave // (race) a concurrent 1-0 exit operation, resulting in stranding, so we // arrange for one of the contending thread to use a timed park() operations // to detect and recover from the race. (Stranding is form of progress failure // where the monitor is unlocked but all the contending threads remain parked). // That is, at least one of the contended threads will periodically poll _owner. // One of the contending threads will become the designated "Responsible" thread. // The Responsible thread uses a timed park instead of a normal indefinite park // operation -- it periodically wakes and checks for and recovers from potential // strandings admitted by 1-0 exit operations. We need at most one Responsible // thread per-monitor at any given moment. Only threads on cxq|EntryList may // be responsible for a monitor. // // Currently, one of the contended threads takes on the added role of "Responsible". // A viable alternative would be to use a dedicated "stranding checker" thread // that periodically iterated over all the threads (or active monitors) and unparked // successors where there was risk of stranding. This would help eliminate the // timer scalability issues we see on some platforms as we'd only have one thread // -- the checker -- parked on a timer. if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) { // Try to assume the role of responsible thread for the monitor. // CONSIDER: ST vs CAS vs { if (Responsible==null) Responsible=Self } Atomic::replace_if_null(Self, &_Responsible); } // The lock might have been released while this thread was occupied queueing // itself onto _cxq. To close the race and avoid "stranding" and // progress-liveness failure we must resample-retry _owner before parking. // Note the Dekker/Lamport duality: ST cxq; MEMBAR; LD Owner. // In this case the ST-MEMBAR is accomplished with CAS(). // // TODO: Defer all thread state transitions until park-time. // Since state transitions are heavy and inefficient we'd like // to defer the state transitions until absolutely necessary, // and in doing so avoid some transitions ... TEVENT(Inflated enter - Contention); int nWakeups = 0; int recheckInterval = 1; for (;;) { if (TryLock(Self) > 0) break; assert(_owner != Self, "invariant"); if ((SyncFlags & 2) && _Responsible == NULL) { Atomic::replace_if_null(Self, &_Responsible); } // park self if (_Responsible == Self || (SyncFlags & 1)) { TEVENT(Inflated enter - park TIMED); Self->_ParkEvent->park((jlong) recheckInterval); // Increase the recheckInterval, but clamp the value. recheckInterval *= 8; if (recheckInterval > MAX_RECHECK_INTERVAL) { recheckInterval = MAX_RECHECK_INTERVAL; } } else { TEVENT(Inflated enter - park UNTIMED); Self->_ParkEvent->park(); } if (TryLock(Self) > 0) break; // The lock is still contested. // Keep a tally of the # of futile wakeups. // Note that the counter is not protected by a lock or updated by atomics. // That is by design - we trade "lossy" counters which are exposed to // races during updates for a lower probe effect. TEVENT(Inflated enter - Futile wakeup); // This PerfData object can be used in parallel with a safepoint. // See the work around in PerfDataManager::destroy(). OM_PERFDATA_OP(FutileWakeups, inc()); ++nWakeups; // Assuming this is not a spurious wakeup we'll normally find _succ == Self. // We can defer clearing _succ until after the spin completes // TrySpin() must tolerate being called with _succ == Self. // Try yet another round of adaptive spinning. if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0) break; // We can find that we were unpark()ed and redesignated _succ while // we were spinning. That's harmless. If we iterate and call park(), // park() will consume the event and return immediately and we'll // just spin again. This pattern can repeat, leaving _succ to simply // spin on a CPU. Enable Knob_ResetEvent to clear pending unparks(). // Alternately, we can sample fired() here, and if set, forgo spinning // in the next iteration. if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) { Self->_ParkEvent->reset(); OrderAccess::fence(); } if (_succ == Self) _succ = NULL; // Invariant: after clearing _succ a thread *must* retry _owner before parking. OrderAccess::fence(); } // Egress : // Self has acquired the lock -- Unlink Self from the cxq or EntryList. // Normally we'll find Self on the EntryList . // From the perspective of the lock owner (this thread), the // EntryList is stable and cxq is prepend-only. // The head of cxq is volatile but the interior is stable. // In addition, Self.TState is stable. assert(_owner == Self, "invariant"); assert(object() != NULL, "invariant"); // I'd like to write: // guarantee (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ; // but as we're at a safepoint that's not safe. UnlinkAfterAcquire(Self, &node); if (_succ == Self) _succ = NULL; assert(_succ != Self, "invariant"); if (_Responsible == Self) { _Responsible = NULL; OrderAccess::fence(); // Dekker pivot-point // We may leave threads on cxq|EntryList without a designated // "Responsible" thread. This is benign. When this thread subsequently // exits the monitor it can "see" such preexisting "old" threads -- // threads that arrived on the cxq|EntryList before the fence, above -- // by LDing cxq|EntryList. Newly arrived threads -- that is, threads // that arrive on cxq after the ST:MEMBAR, above -- will set Responsible // non-null and elect a new "Responsible" timer thread. // // This thread executes: // ST Responsible=null; MEMBAR (in enter epilogue - here) // LD cxq|EntryList (in subsequent exit) // // Entering threads in the slow/contended path execute: // ST cxq=nonnull; MEMBAR; LD Responsible (in enter prolog) // The (ST cxq; MEMBAR) is accomplished with CAS(). // // The MEMBAR, above, prevents the LD of cxq|EntryList in the subsequent // exit operation from floating above the ST Responsible=null. } // We've acquired ownership with CAS(). // CAS is serializing -- it has MEMBAR/FENCE-equivalent semantics. // But since the CAS() this thread may have also stored into _succ, // EntryList, cxq or Responsible. These meta-data updates must be // visible __before this thread subsequently drops the lock. // Consider what could occur if we didn't enforce this constraint -- // STs to monitor meta-data and user-data could reorder with (become // visible after) the ST in exit that drops ownership of the lock. // Some other thread could then acquire the lock, but observe inconsistent // or old monitor meta-data and heap data. That violates the JMM. // To that end, the 1-0 exit() operation must have at least STST|LDST // "release" barrier semantics. Specifically, there must be at least a // STST|LDST barrier in exit() before the ST of null into _owner that drops // the lock. The barrier ensures that changes to monitor meta-data and data // protected by the lock will be visible before we release the lock, and // therefore before some other thread (CPU) has a chance to acquire the lock. // See also: http://gee.cs.oswego.edu/dl/jmm/cookbook.html. // // Critically, any prior STs to _succ or EntryList must be visible before // the ST of null into _owner in the *subsequent* (following) corresponding // monitorexit. Recall too, that in 1-0 mode monitorexit does not necessarily // execute a serializing instruction. if (SyncFlags & 8) { OrderAccess::fence(); } return; }
inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
  assert(node != NULL, "should not add NULL node"); assert(node->_prev == NULL, "node already in list"); assert(node->_next == NULL, "node already in list"); // put node at end of queue (circular doubly linked list) 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; } }
inline void ObjectMonitor::DequeueSpecificWaiter(ObjectWaiter* node) {
  assert(node != NULL, "should not dequeue NULL node"); assert(node->_prev != NULL, "node already removed from list"); assert(node->_next != NULL, "node already removed from list"); // when the waiter has woken up because of interrupt, // timeout or other spurious wake-up, dequeue the // waiter from waiting list ObjectWaiter* next = node->_next; if (next == node) { assert(node->_prev == node, "invariant check"); _WaitSet = NULL; } else { ObjectWaiter* prev = node->_prev; assert(prev->_next == node, "invariant check"); assert(next->_prev == node, "invariant check"); next->_prev = prev; prev->_next = next; if (_WaitSet == node) { _WaitSet = next; } } node->_next = NULL; node->_prev = NULL; } 

ReentrantLock的方式

	ReentrantLock lock = new ReentrantLock();
	public void doWork() { lock.lock(); if(StringUtils.isEmpty(id)){ try { Thread.sleep(1000 * 10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()); lock.unlock(); }

从源码中可以看出通过队列处理的。

//public class ReentrantLock implements Lock, java.io.Serializable {
				final void lock() {
            acquire(1);
        }
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; //放在队尾 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }

因此ReentrantLock,是一个队列方式。

如果分析过程中存在错误点,请大家评批指点。一起学习,共同进步。

作者:仁蕴。
邮箱:[email protected] 
github:https://github.com/jiangwh/blog

猜你喜欢

转载自www.cnblogs.com/jiangwh/p/12710054.html