zoukankan      html  css  js  c++  java
  • 13 ReentrantReadWriteLock原理以及StampedLock的作用及应用场景

    1 ReentrantReadWriteLock原理

    1-1 概述

    ReentrantReadWriteLock相关方法

    1)内部实现了AQS的同步器Sync,并派生出NonfairSync与FairSync两个同步器子类(见下面源码

    • 读锁与写锁是共用一个同步器的,所以等待队列,锁的state也是只有一个
    abstract static class Sync extends AbstractQueuedSynchronizer
        
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            /* As a heuristic to avoid indefinite writer starvation,
                 * block if the thread that momentarily appears to be head
                 * of queue, if one exists, is a waiting writer.  This is
                 * only a probabilistic effect since a new reader will not
                 * block if there is a waiting writer behind other enabled
                 * readers that have not yet drained from the queue.
                 */
            return apparentlyFirstQueuedIsExclusive();
        }
    }
    
        /**
         * Fair version of Sync
         */
        static final class FairSync extends Sync {
            private static final long serialVersionUID = -2274990926593161451L;
            final boolean writerShouldBlock() {
                return hasQueuedPredecessors();
            }
            final boolean readerShouldBlock() {
                return hasQueuedPredecessors();
            }
        }
    

    2)由于state是读写锁共享的,所以内部表示锁的32位state进行了切分,写锁状态占了state的低16位,读锁则使用高16位

    Read vs write count extraction constants and functions.
    Lock state is logically divided into two unsigned shorts:
    The lower one representing the exclusive (writer) lock hold count,and the upper the shared (reader) hold count.
    

    the exclusive (writer) lock:独占锁(低16位)

    shared (reader):共享锁(高16位)

    1-2 写锁加锁成功,读锁加锁失败源码分析

    step1:线程t1加写锁成功

    由于之间没有其他线程加锁,因此t1线程直接将state设置为1并且设置线程拥有者为当前线程。
    
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
    ======================================================================================
    w.lock();
    ================01 调用AQS的提供的acquire方法======================================
    public void lock() {
        sync.acquire(1);         // 调用AQS提供的方法,参数1代表将锁的状态设为1
    }
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    ================02 调用读写锁重写的tryAcquire方法===========================================
        protected final boolean tryAcquire(int acquires) {
                /*
                 * Walkthrough:
                 * 1. If read count nonzero or write count nonzero
                 *    and owner is a different thread, fail.
                 * 2. If count would saturate, fail. (This can only
                 *    happen if count is already nonzero.)
                 * 3. Otherwise, this thread is eligible for lock if
                 *    it is either a reentrant acquire or
                 *    queue policy allows it. If so, update state
                 *    and set owner.
                 */
                Thread current = Thread.currentThread();
                int c = getState();
                int w = exclusiveCount(c);
                // 注意:这里c != 0 是能说明读锁和写锁中有锁被使用了,还需要进一步区分
                if (c != 0) {
                    /*w == 0(用于处理加写锁): w表示state的写锁部分,如果等于0,表明当前的锁是读锁,
                      读锁与当前线程要加的写锁互斥,所以false
                      current != getExclusiveOwnerThread():已经知道当前的锁是读锁,
                      如果当前的读锁不是自己加的,同样不能升级为写锁,返回false
                    */
                    // (Note: if c != 0 and w == 0 then shared count != 0)
                    if (w == 0 || current != getExclusiveOwnerThread())
                        return false;
                    // 如果写锁的state值大于16位表示的最大值,抛出异常
                    if (w + exclusiveCount(acquires) > MAX_COUNT)
                        throw new Error("Maximum lock count exceeded");
                    // Reentrant acquire
                    setState(c + acquires);
                    return true;
                }
                // 当前还没有加锁,进入if语句判断
                /*writerShouldBlock():写锁是否应该阻塞,对于非公平锁总是返回false,公平锁则是去检查AQS队                列中是否有线程阻塞。
                   compareAndSetState(c, c + acquires):尝试修改锁的状态,如果成功锁的状态将0改为1.
                */
                if (writerShouldBlock() ||
                    !compareAndSetState(c, c + acquires))
                    return false;
                // 设置锁的拥有者为当前线程
                setExclusiveOwnerThread(current);
                return true;
            }
        
    

    step2:线程t2首次加读锁失败,调用tryAcquireShared失败。

    t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败
    

    tryAcquireShared 返回值说明:

    • 这个函数在读写锁中返回值比较简单-1表示失败,0表示成功,更加复杂的返回值在信号量被用到
    -1 0 正数
    失败 成功,后继节点不会继续唤醒 成功,数值表示后面还有几个后继节点需要唤醒

    tryAcquireShared代码逻辑(如上图所示):

    线程t1进入后,检查state发现有写锁存在,且写锁的线程拥有者不是自己,直接返回-1,表示加锁失败,加锁失败后进入
    AQS的等待队列。
    
    
    =========================================================================
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    r.lock();
    ==========================================================================
            public void lock() {
                sync.acquireShared(1);
            }
        public final void acquireShared(int arg) {
            if (tryAcquireShared(arg) < 0)
                doAcquireShared(arg);
        }
    ==========================================================================
    protected final int tryAcquireShared(int unused) {
                /*
                 * Walkthrough:
                 * 1. If write lock held by another thread, fail.
                 * 2. Otherwise, this thread is eligible for
                 *    lock wrt state, so ask if it should block
                 *    because of queue policy. If not, try
                 *    to grant by CASing state and updating count.
                 *    Note that step does not check for reentrant
                 *    acquires, which is postponed to full version
                 *    to avoid having to check hold count in
                 *    the more typical non-reentrant case.
                 * 3. If step 2 fails either because thread
                 *    apparently not eligible or CAS fails or count
                 *    saturated, chain to version with full retry loop.
                 */
                Thread current = Thread.currentThread();
                int c = getState();
        /* exclusiveCount(c) != 0:存在独占锁即写锁 */                                          
        /* getExclusiveOwnerThread() != current:独占锁不属于当前线程(同一线程可以先加写锁后加读锁)*/
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return -1;
                int r = sharedCount(c);
                if (!readerShouldBlock() &&
                    r < MAX_COUNT &&
                    compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        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;
                }
                return fullTryAcquireShared(current);
            }
    

    step3: 线程t2进入doAcquireShared的逻辑,进入AQS等待队列,并最终park停止运行。

    ReentrantLock的加锁源码

    
    syn.doAcquireShared的逻辑如下所示:
    1)为当前线程创建关联节点并加入到AQS等待队列中,注意节点的状态为Shared。(如上图所示,队列中首次插入节点还会添加一个伪节点)
    2)进入到死循环,t2线程首先检查AQS等待队列中发现只有自己一个线程在等待,进入if再次调用tryAcquireShared去竞争锁
    3)第二次竞争失败依旧失败,调用shouldParkAfterFailedAcquire(p, node)函数将前驱节点的waitStatus设为-1,该函数返回false(表示让前驱节点负责唤醒自己)。
    4)再次进入循环,t2线程再次调用tryAcquireShared 竞争失败,调用 shouldParkAfterFailedAcquire(p, node) 返回true,会再次调用parkAndCheckInterrupt())函数,此时线程会调用park停止运行(上面图中第二张图变为灰色)。
    
    /**
         * Acquires in shared uninterruptible mode.
         * @param arg the acquire argument
         */
        private void doAcquireShared(int arg) {
            // 对比ReentrantLock的acquireQueued源码会发现这里加的Node的属性是SHARED而不是EXCLUSIVE
            /*为当前线程创建节点并加入AQS等待队列,函数返回当前节点。*/
            final Node node = addWaiter(Node.SHARED);
            boolean failed = true;
            try {
                boolean interrupted = false;
                // 进入死循环
                for (;;) {
                    // 获取当前节点的前驱节点
                    final Node p = node.predecessor();
                    // 如果前驱节点是dummy node,说明当前等待队列中只有自己一个,再次尝试竞争锁。
                    if (p == head) {
                        int r = tryAcquireShared(arg);
                        if (r >= 0) {
                            setHeadAndPropagate(node, r);
                            p.next = null; // help GC
                            if (interrupted)
                                selfInterrupt();
                            failed = false;
                            return;
                        }
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    总结:可以看到t2线程从开始加锁到park()停止运行,总共调用了三次tryAcquireShared,值得注意是这里加锁失败后在AQS阻塞队列中的Node状态时Shared而不是Exculsive的,这一点需要Reentrant的lock过程进行区分。

    1-3 读锁加锁失败,写锁加锁失败

      这种情景下,由于线程t1没有解除写锁,所以t3与t4都是加锁失败,进入到AQS的等待队列中,这里需要注意的是t3加读锁,所以其关联的Node实例的状态是Shared,而t4加的是写锁,所以其Node实例状态是Exculsive的。其加锁失败去之前的Reentrantlock的加锁失败原理基本一致
    

    ReentrantLock的lock流程

    注意点:上图中多个线程在阻塞队列中,节点的waitstatus为-1,表示该节点有义务在释放锁的时候唤醒后面节点关联的线程,为0表示是队列中最后一个(队尾)线程

    1-4 写锁解锁源码分析

    step1:t1线程写操作完成,进入锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功。

    此时同步器的state的写锁部分变为0,线程拥有者变为null,如下图所示。
    

    ===================================================================================     
        w.unlock();
    =========================AQS提供的方法===================================================
        public void unlock() {
                sync.release(1);
            }
            public final boolean release(int arg) {
                if (tryRelease(arg)) {
                    Node h = head;
                    if (h != null && h.waitStatus != 0)
                        unparkSuccessor(h);
                    return true;
            }
            return false;
        }
    ============================sync方法======================================================
    protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 将写锁的state减1
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
    }
    

    step2: 设置好同步器的的state以及线程拥有者后,进入unparkSuccessor(h)逻辑唤醒t2线程

        private void unparkSuccessor(Node node) {
            /*
             * If status is negative (i.e., possibly needing signal) try
             * to clear in anticipation of signalling.  It is OK if this
             * fails or if status is changed by waiting thread.
             */
            int ws = node.waitStatus;
            if (ws < 0)
                compareAndSetWaitStatus(node, ws, 0);
    
            /*
             * Thread to unpark is held in successor, which is normally
             * just the next node.  But if cancelled or apparently null,
             * traverse backwards from tail to find the actual
             * non-cancelled successor.
             */
            Node s = node.next;
            if (s == null || s.waitStatus > 0) {
                s = null;
                for (Node t = tail; t != null && t != node; t = t.prev)
                    if (t.waitStatus <= 0)
                        s = t;
            }
            if (s != null)
                LockSupport.unpark(s.thread);   //唤醒AQS阻塞队列的t2线程。
        }
    

    step3: t2线程从之前的park位置(doAcquireShared函数内部)苏醒继续执行for循环内部代码

    1)线程t2之前要加读锁但是由于已经有写锁了,所以主动调用park停止运行,现在在park处唤醒,继续for循环去竞争锁。
    2)调用tryAcquireShared(arg),给state读锁所对应的高16位加上1。
    3)之后调用setHeadAndPropagate(node, r),从这个函数名就可以看出该函数完成2个功能:
    ---功能1:由于线程t2已经获得锁,所以将该线程所在节点设置位新的伪节点 (head节点)
    ---功能2:完成功能1后,查看head.next指向的node的模式是否位Shared模式,如果是说明是读锁,由于读锁可以并发进行,因此将该节点关联的线程也unpark。
    上述2个功能的执行,直到碰到写锁的节点才停止propagate(扩散)
    
        private void doAcquireShared(int arg) {
            final Node node = addWaiter(Node.SHARED);
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head) {
                        int r = tryAcquireShared(arg);  //2)线程t2获取读锁成功,读锁对应的高16位+1
                        if (r >= 0) {
                            setHeadAndPropagate(node, r);//
                            p.next = null; // help GC
                            if (interrupted)
                                selfInterrupt();
                            failed = false;
                            return;
                        }
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())    // 1)线程t2被唤醒后从从此继续执行
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    ======================================================================================
        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);     // 获取state的高l6位数值
                // !readerShouldBlock():当前线程没有被阻塞
                //  r < MAX_COUNT :读锁的数量不超过最大限制
                if (!readerShouldBlock() &&
                    r < MAX_COUNT &&
                    compareAndSetState(c, c + SHARED_UNIT)) {  // 给高16位+1
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        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;
                }
                return fullTryAcquireShared(current);
            }
    ==================================================================================
            private void setHeadAndPropagate(Node node, int propagate) {
            Node h = head; // Record old head for check below
            setHead(node);
            if (propagate > 0 || h == null || h.waitStatus < 0 ||
                (h = head) == null || h.waitStatus < 0) {
                Node s = node.next;             // 3)获取t3线程关联的节点
                if (s == null || s.isShared())  // 如果该节点不为null并且该节点是shared的
                    doReleaseShared();          // 将头节点的waitstatus由-1改为0,然后将线程t3关联的
                                                // 的线程唤醒
            }
        }
    =======================================================================================
            private void doReleaseShared() {
            for (;;) {
                Node h = head;
                if (h != null && h != tail) {
                    int ws = h.waitStatus;
                    if (ws == Node.SIGNAL) {
                        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                            continue;            // loop to recheck cases
                        unparkSuccessor(h);
                    }
                    else if (ws == 0 &&
                             !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                        continue;                // loop on failed CAS
                }
                if (h == head)                   // loop if head changed
                    break;
            }
        }
    



    • 从上面三张图可以看出关联读锁的线程被唤醒会产生连锁反应,AQS阻塞队列中后续的设置读锁的线程都被唤醒

    step4: 线程t2与t3都成功加上读锁。

    1-5 读锁解锁,等待队列中的写锁竞争锁的源码分析

    d

    1)t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入
    doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二
    2)之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点。
    
            r.unlock();
            public void unlock() {
                sync.releaseShared(1);
            }
    ==============step1:调用tryReleaseShared=====================================
        public final boolean releaseShared(int arg) {
            if (tryReleaseShared(arg)) {
                doReleaseShared();
                return true;
            }
            return false;
        }
    ============step2:在tryReleaseShared让写锁对应的state减去1========================
        protected final boolean tryReleaseShared(int unused) {
                Thread current = Thread.currentThread();
                if (firstReader == current) {
                    // assert firstReaderHoldCount > 0;
                    if (firstReaderHoldCount == 1)
                        firstReader = null;
                    else
                        firstReaderHoldCount--;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        rh = readHolds.get();
                    int count = rh.count;
                    if (count <= 1) {
                        readHolds.remove();
                        if (count <= 0)
                            throw unmatchedUnlockException();
                    }
                    --rh.count;
                }
                for (;;) {
                    int c = getState();
                    int nextc = c - SHARED_UNIT;
                    if (compareAndSetState(c, nextc))
                        // Releasing the read lock has no effect on readers,
                        // but it may allow waiting writers to proceed if
                        // both read and write locks are now free.
                        return nextc == 0;
                }
            }
    =========step3:在doReleaseShared()修改头节点并唤醒下一个节点================================== 
    private void doReleaseShared() {
            /*
             * Ensure that a release propagates, even if there are other
             * in-progress acquires/releases.  This proceeds in the usual
             * way of trying to unparkSuccessor of head if it needs
             * signal. But if it does not, status is set to PROPAGATE to
             * ensure that upon release, propagation continues.
             * Additionally, we must loop in case a new node is added
             * while we are doing this. Also, unlike other uses of
             * unparkSuccessor, we need to know if CAS to reset status
             * fails, if so rechecking.
             */
            for (;;) {
                Node h = head;
                if (h != null && h != tail) {
                    int ws = h.waitStatus;
                    if (ws == Node.SIGNAL) {
                        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                            continue;            // loop to recheck cases
                        unparkSuccessor(h);      // 唤醒节点下一个线程
                    }
                    else if (ws == 0 &&
                             !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                        continue;                // loop on failed CAS
                }
                if (h == head)                   // loop if head changed
                    break;
            }
        }
    ========step4:关联写锁的线程会在parkAndCheckInterrupt())处苏醒================
            final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    总结无论是加锁还是解锁主要关注同步器中几个关键属性的维护

    1.锁的state
    2.锁的当前线程j拥有者。
    3.锁的等待队列维护,头节点的维护,节点的waitState的维护。 
    

    在读写锁中,节点的属性Shared主要用于标识加读锁进程方便在竞争锁的时候连锁(propagate)唤醒AQS等待队列中的读进程

    2 StampedLock的作用以及简单应用

    2-1 作用

    为什么需要stampedLock这种类型的读写锁?

    原始的ReentrantReadWriteLock并发读的时候依旧需要进行CAS操作去修改同步器的state,性能还可以进一步优化,而stampedLock正是基于这一点采用stamp进一步优化并发读的效率。

    stampedLock相比较ReentrantReadWriteLock的区别:

    • 提供的乐观读机制:乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读本质上就是不加锁的读并产生一个stamp),读取完毕后需要校验 stamp , 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全
    • 相比ReentrantReadWriteLock,StampedLock 不支持条件变量,不支持可重入
        long stamp = lock.tryOptimisticRead();
           // 验戳
        if(!lock.validate(stamp)){
           // 锁升级
        }
    

    2-2 StampedLock使用的实例

    package chapter8;
    import lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.locks.StampedLock;
    
    @Slf4j(topic = "c.DataContainerStamped")
    class DataContainerStamped {
        private int data;
        private final StampedLock lock = new StampedLock();
        public DataContainerStamped(int data) {
            this.data = data;
        }
        /*采用StampedLock的读操作,首先进行乐观读(不加锁读)并获取一个stamp,
        * 然后校验这个stamp,如果stamp发生改变则说明读取过程中,有线程修改了读取的值,
        *则此时在真正的加锁(锁升级)去读取数据。
        * */
        public int read(int readTime){
            long stamp = lock.tryOptimisticRead();
            log.warn("optimistic read locking...{}", stamp);
            try {
                Thread.sleep(readTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (lock.validate(stamp)) {
                log.warn("read finish...{}, data:{}", stamp, data);
                return data;
            }
            // 锁升级 - 读锁
            log.warn("updating to read lock... {}", stamp);
            try {
                stamp = lock.readLock();
                log.warn("read lock {}", stamp);
                try {
                    Thread.sleep(readTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.warn("read finish...{}, data:{}", stamp, data);
                return data;
            } finally {
                log.warn("read unlock {}", stamp);
                lock.unlockRead(stamp);
            }
        }
        public void write(int newData){
            long stamp = lock.writeLock();
            log.warn("write lock {}", stamp);
            try {
                try {
                    Thread.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.data = newData;
            } finally {
                log.warn("write unlock {}", stamp);
                lock.unlockWrite(stamp);
            }
        }
    }
    
    
    并发读测试
    @Slf4j(topic = "c.TestStampedLock")
    public class test18 {
        public static void main(String[] args) throws InterruptedException {
            DataContainerStamped dataContainer = new DataContainerStamped(1);
            new Thread(() -> {
                dataContainer.read(1);
            }, "t1").start();
            Thread.sleep(500);
            new Thread(() -> {
                dataContainer.read(0);
            }, "t2").start();
        }
    }
    

    执行结果

    • 可以看到只要stamp没有发生改变,多线程环境下就不会为读操作加锁。
    20:53:51.097 [t1] WARN c.DataContainerStamped - optimistic read locking...256
    20:53:51.104 [t1] WARN c.DataContainerStamped - read finish...256, data:1
    20:53:51.596 [t2] WARN c.DataContainerStamped - optimistic read locking...256
    20:53:51.596 [t2] WARN c.DataContainerStamped - read finish...256, data:1
    
    并发读写测试
    @Slf4j(topic = "c.TestStampedLock")
    public class test18 {
        public static void main(String[] args) throws InterruptedException {
            DataContainerStamped dataContainer = new DataContainerStamped(1);
            new Thread(() -> {
                dataContainer.read(1);
            }, "t1").start();
            new Thread(() -> {
                dataContainer.write(100);
            }, "t2").start();
        }
    }
    

    执行结果:当stamp被修改后,需要给读操作加锁(用户自己实现

    20:57:49.483 [t1] WARN c.DataContainerStamped - optimistic read locking...256
    20:57:49.483 [t2] WARN c.DataContainerStamped - write lock 384
    20:57:49.491 [t1] WARN c.DataContainerStamped - updating to read lock... 256
    20:57:49.492 [t2] WARN c.DataContainerStamped - write unlock 384
    20:57:49.492 [t1] WARN c.DataContainerStamped - read lock 513
    20:57:49.493 [t1] WARN c.DataContainerStamped - read finish...513, data:100
    20:57:49.493 [t1] WARN c.DataContainerStamped - read unlock 513
    

    参考资料

    多线程基础课程

  • 相关阅读:
    C#实现清理系统内存
    WinForm 程序加管理员权限
    DataGridView 绑定List集合后实现自定义排序
    swfupload提示“错误302”的解决方法
    C# WinForm捕获全局异常
    C# WinForm应用程序降低系统内存占用方法
    清除webBrowser 缓存和Cookie的解决方案
    ThInkPHP加密和解密cookie(登录操作)
    prestashop 首页轮播幻灯片图片修改
    网页内容分享到微信
  • 原文地址:https://www.cnblogs.com/kfcuj/p/14639593.html
Copyright © 2011-2022 走看看