美文网首页
Java并发编程之ReentrantLock(可重入锁)

Java并发编程之ReentrantLock(可重入锁)

作者: 逍遥白亦 | 来源:发表于2020-12-14 18:56 被阅读0次

    1. 定义

    重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

    2. 公平性与非公平性

    如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。

        public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    

    事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

    2.1 公平锁

    公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合 请求的绝对时间顺序,也就是FIFO。

    最终会调用Fair.tryAcquire(int acquires)方法

            /**
             * Fair version of tryAcquire.  Don't grant access unless
             * recursive call or no waiters or is first.
             */
            protected final boolean tryAcquire(int acquires) {
                //获取当前线程
                final Thread current = Thread.currentThread();
                //获取同步状态
                int c = getState();
                //同步状态为0,意味着可以获取锁
                if (c == 0) {
                    //判断当前节点前面有没有节点等待获取锁
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                //当前节点前面有节点在等待获取锁,所以本次获取状态加1
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
        }
    

    hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

        public final boolean hasQueuedPredecessors() {
            //如果当前线程前边有线程等待,返回true;当前线程处于队列的最前面或者队列为空,返回false
            Node t = tail; 
            Node h = head;
            Node s;
            return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
        }
    

    2.2 非公平锁

    与公平锁的区别就是,没有hasQueuedPredecessors()的判断,只要有线程获取锁,就立马获取成功。

    final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    

    2.3 代码测试

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.List;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class FairAndNoFairTest {
    
        private static Lock fairLock = new ReentrantLockMine(true);
    
        private static Lock unFairLock = new ReentrantLockMine(false);
    
        private static class Job implements Runnable {
    
            private Lock lock;
    
            public Job(Lock lock) {
                this.lock = lock;
            }
    
            @Override
            public void run() {
                for (int i = 0; i <2 ; i++) {
                    lock.lock();
                    try {
                        Thread.sleep(1000);
                        System.out.println("Lock by [" + Thread.currentThread().getName() + "]"
                                + ", Waiting by " + ((ReentrantLockMine)lock).getQueuedThreads() + "");
                    } catch (InterruptedException e){
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                }
            }
        }
    
        public static void testLock(Lock lock) throws InterruptedException {
    
            for (int i = 0; i < 5 ; i++) {
                Thread thread = new Thread(new Job(lock)) {
                    @Override
                    public String toString() {
                        return getName();
                    }
                };
    
                thread.setName("" + i);
                thread.start();
    
            }
            Thread.sleep(1000);
    
        }
    
        public static class ReentrantLockMine extends ReentrantLock{
    
            public ReentrantLockMine(boolean fair) {
                super(fair);
            }
    
            @Override
            protected Collection<Thread> getQueuedThreads() {
                List<Thread> list = new ArrayList<>(super.getQueuedThreads());
                Collections.reverse(list);
                return list;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
    //        testLock(fairLock);
            testLock(unFairLock);
        }
    }
    

    公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁出现了一个线程连续获取锁的情况。

    回顾nonfairTryAcquire(int acquires)方法,当一 个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。

    非公平性锁可能使线程“饥饿”,为什么它又被设定成默认的实现呢?再次观察上表的结果,如果把每次不同线程获取到锁定义为1次切换,公平性锁在测试中进行了10次切换,而非公平性锁只有5次切换,这说明非公平性锁的开销更小。

    公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

    3. 重入实现

    重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

    • 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
    • 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

    ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实 现为例,获取同步状态的代码如下所示:

    final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                //判断当前线程是否为获取锁的线程
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    

    该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。

    成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值。

            protected final boolean tryRelease(int releases) {
                int c = getState() - releases;
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;
                if (c == 0) {
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                setState(c);
                return free;
            }
    

    如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同 步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

    相关文章

      网友评论

          本文标题:Java并发编程之ReentrantLock(可重入锁)

          本文链接:https://www.haomeiwen.com/subject/blowgktx.html