zoukankan      html  css  js  c++  java
  • 共享锁(S锁)和排它锁(X锁)

    共享锁【S锁】
    又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

    排他锁【X锁】
    又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

    1、什么是共享锁和排它锁

     
         共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
         排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。
     
    2、排它锁和共享锁实例
     
         ReentrantLock就是一种排它锁。CountDownLatch是一种共享锁。这两类都是单纯的一类,即,要么是排它锁,要么是共享锁。
       ReentrantReadWriteLock是同时包含排它锁和共享锁特性的一种锁,这里主要以ReentrantReadWriteLock为例来进行分析学习。我们使用ReentrantReadWriteLock的写锁时,使用的便是排它锁的特性;使用ReentrantReadWriteLock的读锁时,使用的便是共享锁的特性。
     
    3、锁的等待队列组成
     
     ReentrantReadWriteLock有一个读锁(ReadLock)和一个写锁(WriteLock)属性,分别代表可重入读写锁的读锁和写锁。有一个Sync属性来表示这个锁上的等待队列。ReadLock和WriteLock各自也分别有一个Sync属性表示在这个锁上的队列
     
    通过构造函数来看,
        public ReentrantReadWriteLock(boolean fair) {
            sync = (fair)? new FairSync() : new NonfairSync();
            readerLock = new ReadLock(this);
            writerLock = new WriteLock(this);
        }
     
    在创建读锁和写锁对象的时候,会把这个可重入的读写锁上的Sync属性传递过去。
    protected ReadLock(ReentrantReadWriteLock lock) {
                sync = lock.sync;
            }
    protected WriteLock(ReentrantReadWriteLock lock) {
                sync = lock.sync;
            }
     
    所以,最终的效果是读锁和写锁使用的是同一个线程等待队列。这个队列就是通过我们在前面介绍过的AbstractQueuedSynchronizer实现的。
     
     
    4、锁的状态
        
    既然读锁和写锁使用的是同一个等待队列,那么这里要如何区分一个锁的读状态(有多少个线程正在读这个锁)和写状态(是否被加了写锁,哪个线程正在写这个锁)。
     
    首先每个锁都有一个exclusiveOwnerThread属性,这是继承自AbstractQueuedSynchronizer,来表示当前拥有这个锁的线程。那么,剩下的主要问题就是确定,有多少个线程正在读这个锁,以及是否加了写锁。
     
    这里可以通过线程获取锁时执行的逻辑来看,下面是线程获取读锁时会执行的一部分代码。
     
      final boolean tryReadLock() {
                Thread current = Thread.currentThread();
                for (;;) {
                    int c = getState();
                    if (exclusiveCount(c) != 0 &&
                        getExclusiveOwnerThread() != current)
                        return false ;
                    if (sharedCount(c) == MAX_COUNT)
                        throw new Error("Maximum lock count exceeded" );
                    if (compareAndSetState(c, c + SHARED_UNIT)) {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != current.getId())
                            cachedHoldCounter = rh = readHolds.get();
                        rh.count++;
                        return true ;
                    }
                }
            }
     
    注意这个函数的调用exclusiveCount(c) ,用来计算这个锁当前的写加锁次数(同一个进程多次进入会累加)。代码如下
     
    /** Returns the number of shared holds represented in count  */
            static int sharedCount( int c)    { return c >>> SHARED_SHIFT; }
            /** Returns the number of exclusive holds represented in count  */
             static int exclusiveCount (int c) { return c & EXCLUSIVE_MASK; }
     
    相关常量的定义如下
     
    static final int SHARED_SHIFT   = 16;
     
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; 
     
    如果从二进制来看EXCLUSIVE_MASK的表示,这个值的低16位全是1,而高16位则全是0,所以exclusiveCount是把state的低16位取出来,表示当前这个锁的写锁加锁次数。
    再来看sharedCount,取出了state的高16位,用来表示这个锁的读锁加锁次数。所以,这里是用state的高16位和低16位来分别表示这个锁上的读锁和写锁的加锁次数。
     
    现在再回头来看tryReadLock实现,首先检查这个锁上是否被加了写锁,同时检查加写锁的是不是当前线程。如果不是被当前线程加了写锁,那么试图加读锁就失败了。如果没有被加写锁,或者是被当前线程加了写锁,那么就把读锁加锁次数加1,通过compareAndSetState(c, c + SHARED_UNIT)来实现
    SHARED_UNIT的定义如下,刚好实现了高16位的加1操作。
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
     
     
     
    5、线程阻塞和唤醒的时机
     
    线程的阻塞和访问其他锁的时机相似,在线程视图获取锁,但这个锁又被其它线程占领无法获取成功时,线程就会进入这个锁对象的等待队列中,并且线程被阻塞,等待前面线程释放锁时被唤醒。
     
    但因为加读锁和加写锁进入等待队列时存在一定的区别,加读锁时,final Node node = addWaiter(Node.SHARED);节点的nextWaiter指向一个共享节点,表明当前这个线程是处于共享状态进入等待队列。
     
    加写锁时如下,
    public final void acquire (int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    线程是处于排它状态进入等待队列的。
     
     
    在线程的阻塞上,读锁和写锁的时机相似,但在线程的唤醒上,读锁和写锁则存在较大的差别。
     
    读锁通过AbstractQueuedSynchronizer的doAcquireShared来完成获取锁的动作。
      private void doAcquireShared( int arg) {
            final Node node = addWaiter(Node.SHARED);
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head) {
                        int r = tryAcquireShared(arg);
                        if (r >= 0) {
                            setHeadAndPropagate(node, r);
                            p.next = null ; // help GC
                            if (interrupted)
                                selfInterrupt();
                            return ;
                        }
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true ;
                }
            } catch (RuntimeException ex) {
                cancelAcquire(node);
                throw ex;
            }
        }
    在tryAcquireShared获取读锁成功后(返回正数表示获取成功),有一个setHeadAndPropagate的函数调用。
     
    写锁通过AbstractQueuedSynchronizer的acquire来实现锁的获取动作。
      public final void acquire( int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    如果tryAcquire获取成功则直接返回,否则把线程加入到锁的等待队列中。和一般意义上的ReentrantLock的原理一样。
     
    所以在加锁上,主要的差别在于这个setHeadAndPropagate方法,其代码如下
     
     
    private void setHeadAndPropagate (Node node, int propagate) {
            Node h = head; // Record old head for check below
            setHead(node);
            /*
             * Try to signal next queued node if:
             * Propagation was indicated by caller,
             * or was recorded (as h.waitStatus) by a previous operation
             * (note: this uses sign-check of waitStatus because
             * PROPAGATE status may transition to SIGNAL.)
             * and
             * The next node is waiting in shared mode,
             * or we don't know, because it appears null
             *
             * The conservatism in both of these checks may cause
             * unnecessary wake-ups, but only when there are multiple
             * racing acquires/releases, so most need signals now or soon
             * anyway.
             */
            if (propagate > 0 || h == null || h.waitStatus < 0) {
                Node s = node.next;
                if (s == null || s.isShared())
                    doReleaseShared();
            }
        }
     
    主要操作是把这个节点设为头节点(成为头节点,则表示不在等待队列中,因为获取锁成功了),同时释放锁(doReleaseShared)。
     
    下面来看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 ;
            }
        }
     
    把头节点的waitStatus这只为0或者Node.PROPAGATE,并且唤醒下一个线程,然后就结束了。
     
    总结一下,就是一个线程在获取读锁后,会唤醒锁的等待队列中的第一个线程。如果这个被唤醒的线程是在获取读锁时被阻塞的,那么被唤醒后,就会在for循环中,又执行到setHeadAndPropagate,这样就实现了读锁获取时的传递唤醒。这种传递在遇到一个因为获取写锁被阻塞的线程节点时被终止。
     
    下面通过代码来理解这种等待和线程唤醒顺序。
     
     
    package lynn.lock;
     
    import java.util.concurrent.locks.ReentrantReadWriteLock;
     
    public class TestThread extends Thread {
        private ReentrantReadWriteLock lock;
        private String threadName;
        private boolean isWriter ;
     
        public TestThread(ReentrantReadWriteLock lock, String name, boolean isWriter) {
            this.lock = lock;
            this.threadName = name;
            this.isWriter = isWriter;
        }
     
        @Override
        public void run() {
            while (true ) {
                try {
                    if (isWriter ) {
                        lock.writeLock().lock();
                    } else {
                        lock.readLock().lock();
                    }
                    if (isWriter ) {
                        Thread. sleep(3000);
                        System. out.println("----------------------------" );
                    }
                    System. out.println(System.currentTimeMillis() + ":" + threadName );
                    if (isWriter ) {
                        Thread. sleep(3000);
                        System. out.println("-----------------------------" );
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (isWriter ) {
                        lock.writeLock().unlock();
                    } else {
                        lock.readLock().unlock();
                    }
                }
                break;
            }
        }
     
    }
     
     
    TestThread是一个自定义的线程类,在生成线程的时候,需要传递一个可重入的读写锁对象进去,线程在执行时会先加锁,然后进行内容输出,然后释放锁。如果传递的是写锁,那线程在输出结果前后会先沉睡3秒,便于区分输出的结果时间。
     
    package lynn.lock;
     
    import java.util.concurrent.locks.ReentrantReadWriteLock;
     
     
    public class Main {
     
        public static void blockByWriteLock() {
            ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
            lock.writeLock().lock();
     
            TestThread[] threads = new TestThread[10];
            for (int i = 0; i < 10; i++) {
                boolean isWriter = (i + 1) % 4 == 0 ? true : false;
                TestThread thread = new TestThread(lock, "thread-" + (i + 1), isWriter);
                threads[i] = thread;
            }
            for (int i = 0; i < threads.length; i++) {
                threads[i].start();
            }
            System. out.println(System.currentTimeMillis() + ": block by write lock");
            try {
                Thread. sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            lock.writeLock().unlock();
        }
     
        public static void main(String[] args) {
            blockByWriteLock();
        }
    }
     
    在Main中构造了10个线程,由于这个锁一开始是被主线程拥有,并且是在排它状态下加锁的,所以我们构造的10个线程,在一开始执行便是按照其编号从小到大在等待队列中(1到10)。然后主线程打印结果,等待3秒后释放锁。由于前3个线程,编号1到3是处于共享状态阻塞的,而第4个线程是处于排它状态阻塞,所以,按照上面的唤醒顺序,唤醒传递到第4个线程时就结束。
     
    依次类推,理论上的打印顺序是 :主线程 [1,2,3]  4  [5,6,7] 8 [9,10]
     
    从下面的执行结果来看,也是符合我们的预期的。
         
    6、读线程之间的唤醒
         
         如果一个线程在共享模式下获取了锁状态,这个时候,它是否要唤醒其它在共享模式下等待在该锁上的线程?
     
         由于多个线程可以同时获取共享锁而不相互影响,所以,当一个线程在共享状态下获取了锁之后,理论上是可以唤醒其它在共享状态下等待该锁的线程。但如果这个时候,在这个等待队列中,既有共享状态的线程,同时又有排它状态的线程,这个时候又该如何唤醒?
     
         实际上对于锁来说,在共享状态下,一个线程无论是获取还是释放锁的时候,都会试着去唤醒下一个等待在这个锁上的节点(通过上面的doAcquireShared代码能看出)。如果下一个线程也是处于共享状态等待在锁上,那么这个线程就会被唤醒,然后接着试着去唤醒下一个等待在这个锁上的线程,这种唤醒动作会一直持续下去,直到遇到一个在排它状态下阻塞在这个锁上的线程,或者等待队列全部被释放为止。
         因为线程是在一个FIFO的等待队列中,所以,这这样一个一个往后传递,就能保证唤醒被传递下去。
  • 相关阅读:
    398. Random Pick Index
    382. Linked List Random Node
    645. Set Mismatch
    174. Dungeon Game
    264. Ugly Number II
    115. Distinct Subsequences
    372. Super Pow
    LeetCode 242 有效的字母异位词
    LeetCode 78 子集
    LeetCode 404 左叶子之和
  • 原文地址:https://www.cnblogs.com/panxuejun/p/8874321.html
Copyright © 2011-2022 走看看