精华内容
下载资源
问答
  • 公平锁和非公平锁

    万次阅读 2020-10-23 03:37:59
    自认为不可能写的更好了,但为了弥补知识盲区,引用一下这篇关于公平锁和非公平锁的地址 一张图读懂非公平锁公平锁 概括来说:公平锁的意思就是,当已有资源释放了锁,则下一个获得锁的线程是线程队列最前面的一个...

    看到一篇文章写的比较好,易懂。自认为不可能写的更好了,但为了弥补知识盲区,引用一下这篇关于公平锁和非公平锁的地址

    一张图读懂非公平锁与公平锁

    概括来说:公平锁的意思就是,当已有资源释放了锁,则下一个获得锁的线程是线程队列最前面的一个。
    非公平锁则不能保证,释放锁的时候刚好来了一个线程那么这个线程就会获得到锁,反而线程队列中等待的线程没有获得到锁,这种体现了非公平的机制故称作非公平锁。

    默认的ReentrantLock和ReadWriteLock都是非公平锁。ReentrantLock可以使用带参数的构造方法创建公平锁,需要传入true
    如下是构造方法的源码截图
    在这里插入图片描述

    展开全文
  • 公平锁&非公平锁.pdf

    2020-10-21 14:18:31
    公平锁&非公平锁.pdf
  • 公平锁与非公平锁

    多人点赞 2019-08-09 13:59:57
    公平锁与非公平锁什么是公平锁概念优点缺点图示什么是非公平锁概念优点缺点图示源码分析公平锁公平锁总结 什么是公平锁 概念 公平锁是指多个线程按照申请锁的顺序来获得锁,线程直接进入队列中排队,队列中的第一...

    什么是公平锁

    概念

    公平锁是指多个线程按照申请锁的顺序来获得锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

    优点

    等待锁的线程不会饿死。

    缺点

    整体吞吐效率比非公平锁要低,等待队列中除第一个以外的线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁要大。

    图示

    在这里插入图片描述

    什么是非公平锁

    概念

    非公平锁是多个线程加锁时直接尝试获得锁,获取不到才会进入等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获得锁的场景。

    优点

    可以减少唤醒线程的开销,整体吞吐效率高,因为线程有几率不阻塞直接获得锁。

    缺点

    处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

    图示

    在这里插入图片描述

    源码分析

    ReentranLock中默认使用非公平锁,也可以通过构造器显式指定公平锁。

    公平锁

            /**
             * 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();
                if (c == 0) {
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
        }
    

    非公平锁

            /**
             * Performs non-fair tryLock.  tryAcquire is implemented in
             * subclasses, but both need nonfair try for trylock method.
             */
            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;
            }
    

    通过上图中的源代码对比,我们可以明显地看出公平锁和非公平锁的区别就在于公平锁在获取同步状态时多了一个限制条件hasQueuedPredecessors();

        public final boolean hasQueuedPredecessors() {
            // The correctness of this depends on head being initialized
            // before tail and on head.next being accurate if the current
            // thread is first in queue.
            Node t = tail; // Read fields in reverse initialization order
            Node h = head;
            Node s;
            return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
        }
    

    该方法主要做一件事情,就是判断当前线程是否位于同步对列的第一个,如果是则返回true,如果不是,返回false。

    总结

    综上所述,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁。非公平锁在加锁时不考虑排队等待情况,直接获取锁,所以可能出现后申请的线程先获得锁的情况。

    展开全文
  • 公平锁与非公平锁的对比

    千次阅读 2019-11-02 10:43:11
    公平锁一定不公平吗?公平锁与非公平锁的性能谁更好?

    扫描下方二维码或者微信搜索公众号菜鸟飞呀飞,即可关注微信公众号,阅读更多Spring源码分析Java并发编程文章。

    微信公众号

    1. 问题

    • 在上一篇文章中结合源码介绍了公平锁和非公平锁的实现【文章链接】。这一篇文章将从公平性和性能方面对比一下两者。
    • 在阅读本文之前,可以先思考一下下面两个问题。
    • 1. 非公平锁一定不公平吗?
    • 2. 公平锁与非公平锁的性能谁更好?

    2. 对比

    • 主要从公平性和性能这两个方面来对比一下公平锁和非公平锁。

    2.1 公平性

    • 在上一篇文章的总结处,提到了公平锁和非公平锁是从线程获取锁所等待的时间来区分两者的公平性。公平锁中,多个线程抢锁时,获取到锁的线程一定是同步队列中等待时间最长的线程。而非公平锁中,多个线程抢锁时,获取锁的线程不一定是同步队列中等待时间最长的线程,有可能是同步队列之外的线程先抢到锁。

    • 对于公平锁,线程获取锁的过程可以用如下示意图表示(图片来源于公众号:Mr羽墨青衫,文章链接:深入剖析Java重入锁ReentrantLock的实现原理)。
      公平锁抢锁示意图

    • 从图中可以发现,当持有锁的线程T1释放锁以后,会唤醒同步队列中的T2线程,只要同步队列中有线程在等待获取锁,那么其他刚进来想要获取锁的人,就不能插队,包括T1自己还想要获取锁,也需要去排队,这样就保证了让等待时间最长的线程获取到锁,即保证了公平性。

    • 对于非公平锁,线程获取锁的示意图可以用如下示意图表示。(图片来源于公众号:Mr羽墨青衫,文章链接:深入剖析Java重入锁ReentrantLock的实现原理)。
      非公平锁抢锁示意图

    • 从图中可以发现,当持有锁的线程T1释放锁以后,会唤醒同步队列中的T2线程,此时即使同步队列中有线程在排队,从外面刚进来的线程想要获取锁,此时是可以直接去争抢锁的,包括线程T1自己,这个时候就是T2、T1、外面刚进来的线程一起去抢锁,虽然此时T2线程等待的时间最长,但是不能保证T2一定抢到锁,所以此时是不公平的。

    • 文章开头提到了一个问题,非公平锁一定不公平吗?答案是不一定,对于刚刚上面图中展示的那种情况,此时非公平锁是不公平的。但是存在一种特殊情况,可以参考如下示意图,如当T1线程释放锁以后,AQS同步队列外部没有线程来争抢锁,T1线程在释放锁以后,自己也不需要获取锁了,此时T1因为唤醒的是T2,现在是有T2一个线程来抢锁,所以此时能获取到锁的线程一定是T2,这种情况下,非公平锁又是公平的了,因为此时即使同步队列中有T3、T4、…、Tn线程,但是T2等待的时间最长,所以是T2获取到锁(AQS的同步队列遵循的原则是FIFO)。
      非公平锁抢锁示意图

    • 从非公平锁的示意图中,我们也可以发现,如果线程一旦获取锁失败进入到AQS同步队列后,就会一直排队,直到其他获取到锁的线程唤醒它或者它被其他线程中断,才会出队。即:一朝排队,永远排队。

    性能

    • 公平锁和非公平锁的性能是不一样的,非公平锁的性能会优于公平锁。为什么呢?因为公平锁在获取锁时,永远是等待时间最长的线程获取到锁,这样当线程T1释放锁以后,如果还想继续再获取锁,它也得去同步队列尾部排队,这样就会频繁的发生线程的上下文切换,当线程越多,对CPU的损耗就会越严重。
    • 非公平锁性能虽然优于公平锁,但是会存在导致线程饥饿的情况。在最坏的情况下,可能存在某个线程一直获取不到锁。不过相比性能而言,饥饿问题可以暂时忽略,这可能就是ReentrantLock默认创建非公平锁的原因之一了。
    • 下面以一个demo为例,对比了一下公平锁与非公平锁的性能。
    public class Demo {
    
        // 公平锁
        private static Lock fairLock = new ReentrantLock(true);
    
        // 非公平锁
        private static Lock nonFairLock = new ReentrantLock(false);
    
        // 计数器
        private static int fairCount = 0;
    
        // 计数器
        private static int nonFairCount = 0;
    
        public static void main(String[] args) throws InterruptedException {
            System.out.println("公平锁耗时:   " + testFairLock(10));
            System.out.println("非公平锁耗时: " + testNonFairLock(10));
            System.out.println("公平锁累加结果: " + fairCount);
            System.out.println("非公平锁累加结果: " + nonFairCount);
        }
    
        public static long testFairLock(int threadNum) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(threadNum);
            // 创建threadNum个线程,让其以公平锁的方式,对fairCount进行自增操作
            List<Thread> fairList = new ArrayList<>();
            for (int i = 0; i < threadNum; i++) {
                fairList.add(new Thread(() -> {
                    for (int j = 0; j < 10000; j++) {
                        fairLock.lock();
                        fairCount++;
                        fairLock.unlock();
                    }
                    countDownLatch.countDown();
                }));
            }
    
            long startTime = System.currentTimeMillis();
            for (Thread thread : fairList) {
                thread.start();
            }
            // 让所有线程执行完
            countDownLatch.await();
            long endTime = System.currentTimeMillis();
    
            return endTime - startTime;
        }
    
        public static long testNonFairLock(int threadNum) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(threadNum);
            // 创建threadNum个线程,让其以非公平锁的方式,对nonFairCountCount进行自增操作
            List<Thread> nonFairList = new ArrayList<>();
            for (int i = 0; i < threadNum; i++) {
                nonFairList.add(new Thread(() -> {
                    for (int j = 0; j < 10000; j++) {
                        nonFairLock.lock();
                        nonFairCount++;
                        nonFairLock.unlock();
                    }
                    countDownLatch.countDown();
                }));
            }
            long startTime = System.currentTimeMillis();
            for (Thread thread : nonFairList) {
                thread.start();
            }
            // 让所有线程执行完
            countDownLatch.await();
            long endTime = System.currentTimeMillis();
    
            return endTime - startTime;
        }
    }
    
    • 上面的Demo中,创建了threadNum个线程,然后让这threadNum个线程并发的对变量进行累加10000次的操作,分别用公平锁和非公平锁来保证线程安全,最后分别统计出公平锁和非公平锁的耗时结果。
    • threadNum = 10时,重复三次测试的结果如下。
    次数公平锁非公平锁
    1618ms22ms
    2544ms20ms
    3569ms15ms
    • threadNum = 20时,重复三次测试的结果如下。
    次数公平锁非公平锁
    11208ms25ms
    21146ms26ms
    31215ms19ms
    • threadNum = 30时,重复三次测试的结果如下。
    次数公平锁非公平锁
    11595ms28ms
    21543ms31ms
    31601ms31ms
    • 测试环境:macOS 10.14.6,处理器:2.9GHZ,Intel Core i7,内存:16G 2133MHz
    • 从上面的测试结果可以发现,非公平锁的耗时远远小于公平锁的耗时,这说明非公平锁在并发情况下,性能更好,吞吐量更大。当线程数越多时,差异越明显。

    相关推荐

    微信公众号

    展开全文
  • 深入剖析ReentrantLock公平锁与非公平锁源码实现

    万次阅读 多人点赞 2017-03-19 15:29:40
    本文以公平与非公平锁的加锁缩成为主线,分析整个加锁过程。准备知识简介ReentrantLock类图: NonfairSync继承关系: Node结点:作为获取锁失败线程的包装类, 组合了Thread引用, 实现为FIFO双向队列...

    原文地址: https://blog.csdn.net/lsgqjh/article/details/63685058

    ReentrantLock是JUC包中重要的并发工具之一,支持中断和超时、还支持尝试机制获取锁, 并且是一种通过编程控制的可重入锁,尽可能减少死锁问题。本文以公平与非公平锁的加锁释放锁过程成为主线,分析ReentrantLock的重要内容。

    目标

    1. 掌握AQS基本原理,理解ReentrantLock的具体实现
    2. 挖掘体会AQS精妙设计

    总之,学习AQS过程,要多思考为什么要这样实现,善于带着问题主动探索答案,不断的回过头去看,去思考,我们对并发会有更深的理解。在学习过程中沉淀的“pattern”认知和学习能力能够轻松的复制到我们工作实践和其它优秀代码的学习中。工作后时不时会遇到业界通用方案不匹配自身业务,到造轮子的时候可能才会后悔之前没有多多沉淀吧~。同时感谢我参考学习过的大神资料。

    准备知识

    ReentrantLock类图:
    7d17b337ea46429eba0af2e00689b787.png

    c63badf948644efcb16eeaefeff6ac6f.png

    理想情况下,线程A拿到锁,执行完后释放锁,线程B恰好到来顺手接下这把锁,一切都那么完美的话,也没必要加锁了,问题就在于在A没释放锁时,后续线程也想得到这把锁,所以只好让这些等待的线程进行排队,进而需要一套数据结构来组织这个队伍:

    Node结点:作为获取锁失败线程的包装类, 组合了Thread引用, 实现为FIFO双向队列。 下图为Node结点的属性描述
    26c6cfea16224251bab82653a2b80c77.png

    下图为用Node节点构成的双向链表图示:
    7ada27dcd65d46238904a50f91222bd6.png

    CLH queues need a dummy header node to get started. But we don’t create them on construction, because it would be wasted effort if there is never contention. Instead, the node is constructed and head and tail pointers are set upon first contention.

    链表初始化的头节点其实是一个虚拟节点,英文名称之为dummy header, 因为它不会像后继节点一样真实的存放线程,并且这个节点只会在真正产生竞争排队情况下才会延迟初始化,避免性能浪费,下面看代码的时候,我会再次提到。
    AbstractQueuedSynchronizer 类是一个模版类,维护了着一个同步队列(双向链表),提供着同步队列一些操作的公共方法,JUC并发包里基于此类实现了很多常用的并发工具类,如 Semaphore, CountDownLatch等。

       /**
         * The synchronization state.
         */
        private volatile int state;
    

    AbstractQueuedSynchronizer维护了一个state变量,来表示同步器的状态,state可以称为AQS的灵魂,基于AQS实现的好多JUC工具,都是通过操作state来实现的,state为0表示没有任何线程持有锁;state为1表示某一个线程拿到了一次锁,state为n(n > 1),表示这个线程获取了n次这把锁,用来表达所谓的“可重入锁”的概念。

    锁的创建

    非公平锁(默认)

    final ReentrantLock lock = new ReentrantLock();
    final ReentrantLock lock = new ReentrantLock(false);
    

    公平锁

    final ReentrantLock lock = new ReentrantLock(true);
    

    非公平锁加锁过程

    lock()方法的逻辑: 多个线程调用lock()方法, 如果当前state为0, 说明当前没有线程占有锁, 那么只有一个线程会CAS获得锁, 并设置此线程为独占锁线程。那么其它线程会调用acquire方法来竞争锁(后续会全部加入同步队列中自旋或挂起)。当有其它线程A又进来想要获取锁时, 恰好此前的某一线程恰好释放锁, 那么A会恰好在同步队列中所有等待获取锁的线程之前抢先获取锁。也就是说所有已经在同步队列中的尚未被 取消获取锁 的线程是绝对保证串行获取锁,而其它新来的却可能抢先获取锁。后面代码解释。

        static final class NonfairSync extends Sync {
            private static final long serialVersionUID = 7316153563782823691L;
    
            final void lock() {
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
    	/**
    	* 此为AQS的protected方法,允许子类重写, 在这里被NonfairSync类重写
    	*/
            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
        }
    
    

    先看acquire方法:
    逻辑:tryAcquire方法仍然尝试获取锁(快速获取锁机制),成功返回false,如果没有成功, 那么就将此线程包装成Node加入同步队列尾部。。Node.EXCLUSIVE 为null表示这是独占锁,如果为读写锁,那就是 共享模式(shared)。

        public final void acquire(int arg) {
        // tryAcquire()方法也是让新来的线程进行第二次插队的机会!!
    	//如果再次获取锁还不成功才会放到队列
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    
    

    再看addWaiter 逻辑:

    1. Node包装当前线程
    2. pred 尾指针不为null,即队列不为空, 则快速CAS将自己设为新的tail
    3. 如果队列为空, 则调用enq强制入队
    4. 如果CAS设置失败,说明在其它线程入队节点争抢了tail,则此线程只能调用enq强制入队

    注意这里在链表尾部添加节点时,先设置的prev,我们先记住这个事情。

     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;
        }
    
    
     Node(Thread thread, Node mode) {     // Used by addWaiter
                this.nextWaiter = mode;
                this.thread = thread;
            }
    

    看Node的构造方法,属性 nextWaiter 值在此被赋予EXCLUSIVE表示独占模式。

    下面是enq方法,方法内是一个for(;;),看来退出的条件只能是当前线程入队成功。之前也提到过,只有在产生锁竞争了,才会去初始化链表头节点。如果队列为空,初始化头尾节点,然后后续循环会走到else,else的逻辑和上线CAS入队的逻辑一样,只不过这里套在for循环里,直到入队成功才退出循环。

    private Node enq(final Node node) {
            for (;;) {
                Node t = tail; 
                if (t == null) { // 判断队列是否为空,空队列应该先初始化头节点
                    if (compareAndSetHead(new Node()))
                        tail = head;//头尾共同指向头结点
                } else {//CAS添加并允许失败, 走for(;;)
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;//返回新插入节点的前置节点
                    }
                }
            }
        }
    

    入队完成后看下acquireQueued 逻辑:

    failed 标记最终acquire是否成功, interrupted标记是否曾被挂起过。注意到for(;😉 跳出的唯一条件就是if (p == head && tryAcquire(arg)) 即当前线程结点是头结点且获取锁成功。从这里我们应该看到,这是一个线程第三次又想着尝试快速获取锁:虽然此时该节点已被加入等待队列,在进行睡眠之前又通过p == head && tryAcquire(arg)方法看看能否获取锁。也就是说只有该线程结点的所有 有效的前置结点都拿到过锁了,当前结点才有机会争夺锁,如果失败了那就通过shouldParkAfterFailedAcquire方法判断是否应该挂起当前结点,等待响应中断。观察 每次调用tryAcquire方法的时机,可以看出作者优化意图:

    1. 尽量在没入队的时候拿到锁,避免过多队列操作维护成本
    2. 尽量在睡眠前拿到锁,避免过多上下文切换
    final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
    			   //如果第一次循环就获取成功那么返回的interrupted是false,不需要自我中断。
    	    //否则 说明在获取到同步状态之前经历过挂起(返回true)。
                for (;;) {// 获取前置结点
                    final Node p = node.predecessor();
                 //如果刚入队的尚未被挂起的节点的前置节点是头节点,那么此节点线程有必要尝试一下获取锁,因为head很可能是 刚初始化的 dummy head,或者 会预设head很快释放锁
                    if (p == head && tryAcquire(arg)) {
                        setHead(node); //设置新的head
                        p.next = null; // help GC
                        failed = false;
                        return interrupted; // fase 因为刚入队还没挂起就拿到锁了
                    }
    				//走到这里说明前置节点不为head或者抢锁失败了
                    //判断当前node线程是否可挂起,是,就调用parkAndCheckInterrupt挂起 ,interrupted设置为true,表示曾经被中断过
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
    			//如果tryAcquire出现异常那么取消当前结点的获取,毕竟tryAcquire是留给子类实现的,谁知道弄出啥幺蛾子
                    cancelAcquire(node);
            }
        }
    

    看下shouldParkAfterFailedAcquire 方法的逻辑:

       private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            int ws = pred.waitStatus;
            if (ws == Node.SIGNAL)
    		/*
                 * Predecessor was cancelled. Skip over predecessors and
                 * indicate retry.
                 */
          /*
          如果前置结点是SIGNAL状态, 那么当前置结点执行完成后是可以唤醒后续结点的,
    	  此时可以安全的挂起当前结点, 不需要进行不必要的for(;;),前置结点自然会通知。
          */
                return true;
            if (ws > 0) {
          //   如果ws>0说明前置结点是被自己取消获取同步的结点(只有线程本身可以取消自己)。
    	  //那么do while循环一直往头结点方向找waitStatus < 0的节点;
    	  //含义就是去除了FIFO队列中的已经自我取消申请同步状态的线程。
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node; // 终于找到一个“正常”的节点,赶紧将它设为自己的后继节点
            } else {
               /*
                 * waitStatus must be 0 or PROPAGATE.  Indicate that we
                 * need a signal, but don't park yet.  Caller will need to
                 * retry to make sure it cannot acquire before parking.
                 */
                 //如果是其它状态waitStatus要不是0或着PROPAGATE,意味着当前结点需要一个signal但是尚未park,
    			 // 所以调用者必须确保在挂起之前不能获取同步状态。
    			// 并强行设置前置结点为SIGNAL。之所以CAS设置是因为,pred线程也会操作cancelAcquire来取消自己和node线程对pred的操作产生竞争条件。
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }
    	
    

    上面整个流程是线程通过addWaiter方法入队后,在挂起之前尝试获取锁,如果失败,通过shouldParkAfterFailedAcquire 判断是否应该挂起,因为刚入队的node的前置节点也是刚初始化没多久的节点,默认的waitStatus=0, 所以第一次走shouldParkAfterFailedAcquire 方法if (ws == Node.SIGNAL)分支进不去,并在else逻辑里CAS设置前置节点waitStatus=SIGNAL, 注意这里可能会CAS失败,最终方法应该会返回false。对于shouldParkAfterFailedAcquire 方法的调用上层别忘了有个for(;;),再次进入此方法就满足了if (ws == Node.SIGNAL)分支(预设之前CAS设置SIGNAL成功,即使失败,再次进来时do while 也会干掉cancel的节点),总之,总之,for(;;) 一定会控制在当前线程挂起之前设置好了前置节点能够将来SIGNAL自己。
    这个获取锁的流程我整理了一个流程图,方便理解:
    f50905017e8b497e85d1868f6c61daf6-image.png

    接下来看ReentrantLock中的NonfairSync对同步器方法 tryAcquire 的重写:

    protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
    

    下面是nonfairTryAcquire

    final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) { //如果没有线程获取锁,则当前线程CAS获取锁。并设置自己为当前锁的独占线程
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
    			// 如果存在锁竞争,判断获取锁的线程是否是当前线程, 因为ReentrantLock是可重入锁,
    			//用state变量表示重入次数,即:使当前state+1;
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
    			//如果不是当前线程,则不能获取同步状态
                return false;
            }
    
    

    公平锁的加锁过程

    之前实习面美团金融的时候,被问到ReentrantLock中公平锁和 非公平锁在哪里体现的,问的一脸懵逼,挂电话后打开代码开始自我检讨。我们对比下两者对tryAcquire 的实现:

    lock方法对比非公平锁, 没有了if else 也就意味着新来的线程没有插队的机会, 所有来的线程必须扔到队列尾部, acquire方法也会像非公平锁一样首先调用tryAcquire插队试试,但是只有队列为空或着本身就是head,那么才可能成功,如果 队列非空那么肯定被扔到队列尾部去了, 插个毛线。

     static final class FairSync extends Sync {
            private static final long serialVersionUID = -3000897897090466540L;
    
            final void lock() {
                acquire(1);
            }
    
          //只有队列为空或着当前tryAcquire的node线程是head
            protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();//拿到当前的同步状态, 如果是无锁状态, 则进行hasQueuedPredecessors方法逻辑
    			//逻辑含义是:当前队列为空或本身是同步队列中的头结点。如果满足条件CAS获取同步状态,并设置当前独占线程。
                if (c == 0) {
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                //重入锁逻辑 和非公平锁一样 不解释
                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()
    和非公平锁对比多了这个方法逻辑, 也就意味着没有了新来线程插队的情况,保证了公平锁的获取串行化。

     public final boolean hasQueuedPredecessors() {
            Node t = tail; // Read fields in reverse initialization order
            Node h = head;
            Node s;
            return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
        }
    

    有个有趣的地方: 既然h != t 还能h.next==null ? ,我这里能想到的一处地方,在于头节点初始化时先CAS的head,第二步才将tail=head,大概只有在这两步的间隙,这个条件才会成立吧!

    公平锁和非公平锁的释放逻辑

    公平锁和非公平锁的释放逻辑是一致的 都是通过sync.release(1);
    ReentrantLock中的unlock()

    public void unlock() {
            sync.release(1);
        }
    
    

    AbstractQueuedSynchronizer中的release方法:
    release之后还要调用unparkSuccessor() 方法唤醒后继结点

        public final boolean release(int arg) {
            if (tryRelease(arg)) {
                Node h = head;// 当前这个头结点是同步队列中的头结点
                //并且不一定当前线程是要释放锁的执行线程, 因为可能是非公平锁的释放,压根就没插入队列就特么插队获得同步了, 总之就是拿到头结点了。
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    

    释放锁的逻辑tryRelease
    只有拥有锁的线程才有资格tryRelease,所以这个方法内不需要任何同步机制

      protected final boolean tryRelease(int releases) {
                int c = getState() - releases;
                //  只有获得锁的线程自己才能释放锁
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;
                if (c == 0) {//c==0说明已经无锁
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                setState(c);//这里就没有CAS,好像reactJs里的 state状态管理~
                //否则更新state为state-1, 也就是加锁多少次,就得释放多少次, lock unlock配对使用。
             
                return free;
            }
    

    unparkSuccessor 方法逻辑:
    某线程释放锁后,要唤醒队列中的首结点或者后继结点来偷锁,当然唤醒后可能会参与和一些新来的线程pk下。会发现其可能会存在一个从tail向前查找的流程,假如刚好这时执行这个流程,从tail向head查找节点显然就会存在问题,所以1和2对调的流程在并发下也是存在问题的。unparkSuccessor 在查找head的下一个有效节点的时候,没有从head到tail方向查找,而是反方向从tail向head查找,正常逻辑肯定是从head向tail方向查找速度更快

       private void unparkSuccessor(Node node) {
        
            int ws = node.waitStatus;
            if (ws < 0)//同样是因为node线程和当前线程有竞争,node本身也可以修改自身状态嘛,因此CAS
                compareAndSetWaitStatus(node, ws, 0);
            Node s = node.next;
            if (s == null || s.waitStatus > 0) {
                s = null;
                //发现这里竟然从tail结点往前找最靠近head结点的且处于非取消状态的结点?这不增加了遍历复杂度么? 留个疑问下面解释!
                for (Node t = tail; t != null && t != node; t = t.prev)
                    if (t.waitStatus <= 0)
                        s = t;
            }
            //s是后继结点 因为后继结点很可能是满足条件的, 如果满足即s!=NULL 直接unpark后继结点。
            if (s != null)
                LockSupport.unpark(s.thread);
        }
    

    为什么在unparkSuccessor 方法中对于需要unpark的线程结点进行从tail 往前查找?再来看一下enq 方法:

    private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                if (t == null) { // Must initialize
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
    

    将上面代码梳理一下,大致分为三个步骤:
    ​ 1、首先node.prev = t ,将当前node的prev指向tail节点;
    ​ 2、CAS将tail 指向新node结点;
    ​ 3、将之前tail节点的next指向当前节点
    示意图如下:

    假如将追加节点的三个步骤顺序调换下,先将tail节点的next指向当前节点,然后cas原子修改tail指向,最后再来修改当前节点的prev指向,即将上面的1和3对调一下,会出现上面情况呢?

    将tail节点的next指向当前节点操作后,紧接着会执行cas操作修改tail指向当前节点,由于存在多线程并发问题,即可能会存在多个线程同时申请锁资源,假如现在t1、t2两个线程都同时做上面两个步骤:
    ​ 1、t1线程修改next后,紧接着t2线程也修改next指向,导致会把t1修改的指向覆盖;
    ​ 2、这时t1线程做cas替换tail指向成功后,t2也来做cas操作就会失败;
    ​ 3、t1由于cas操作成功,最后修改prev指向

    可以发现,由于并发导致追加的t1节点是存在问题的,正常情况下Node1的next应该指向t1节点,但是却被t2节点覆盖了。所以,1和3对调是在并发下是存在问题的。

    假如1和2对调,先进行cas操作,然后修改prev,最后再来修改next又会怎么样呢?首先通过cas原子操作将tail指向当前节点,示意图如下:
    99e2d009efc94c6f89a37adaf705fb8c.png

    tail节点这时还是孤立的节点,prev和next都还没有指向,tail节点和其它节点之间没有关联了,这时如果其它线程需要遍历这个双向链表就比较危险了

    现在我们来分析下为什么源码中这个顺序执行在并发下就不会存在问题。现在假设两个线程同一时间都没有获取到锁,都需要追加到Sync Queue队列尾部,大致流程如下:

    1. 线程t1的节点设置prev指向tail,线程t2节点同时也设置prev指向tail,这时就不会出现上面如果先设置next就会导致后设置把之前设置覆盖情况,因为如果先设置next是对Node1进行操作,存在多个线程对Node1同时操作导致状态不一致问题,而如果这里先设置prev,操作对象是线程本身的节点,是不存在多线程并发问题,示意图如下:

    1. 这时t1和t2都进行cas原子操作,反正会有一个线程会操作成功,假如是t1线程操作成功,然后就可以顺利的设置Node1节点的next指向t1,因为只会存在一个线程操作成功,所以对Node1的操作此时也不会存在并发问题,由于t1的cas操作成功导致t2线程进行cas操作必然失败,此刻示意图如下:

    1. 由于t2线程cas操作失败,因此不再继续操作Node1的next指向自己,而是进入enq()方法中,其源码逻辑是enq方法中通过cas+无限循环方式保证t2节点一定会被追加到Sync Queue尾部的,每次循环都是重新获取最新的tail,然后将t2的prev指向这个最新的tail,然后通过cas操作将tail指向自己,最后在将之前tail节点的next指向t2节点,这个案例中获取的最新tail就是t1节点了,所以t2节点会被追加到t1节点后,这样就能保证即使在高并发下依然可以实现节点正常添加,而不会像之前出现状态不一致情况,示意图如下:
      2e203a8612f84291aadb314afb2db7b9.png

    2. 上面分析unparkSuccessor()在查找head的下一个有效节点的时候,没有从head到tail方向查找,而是反方向从tail向head查找,如果你对我刚才分析得到逻辑理解透彻的话,就比较好解释了。比如:t1设置prev指向Node1,然后cas操作将tail指向了t1,这时Queue的结构如下,假如这时候执行unparkSuccessor(),Node0查找它的后驱节点为Node1,假如Node1是无效节点,Node1需要继续查找它的后驱节点,但是这时Node1的next并没有设置,是无法查找到的,所以必须从tail向head方向查找才行。

    取消获取锁cancelAcquire() 方法

    取消获取锁意味着结点的出队。包括三个场景下的出队:

    1. node是tail
    2. node既不是tail,也不是head的后继节点
    3. node是head的后继节点
     private void cancelAcquire(Node node) {
            // Ignore if node doesn't exist
            if (node == null)
                return;
    
            node.thread = null;
    
            // 跳过所有被取消的前置结点
            Node pred = node.prev;
            while (pred.waitStatus > 0)
                node.prev = pred = pred.prev;
    
            Node predNext = pred.next;
    
            node.waitStatus = Node.CANCELLED;
    
            // `` 如果node是tail,更新tail为pred,并使pred.next指向null``
            if (node == tail && compareAndSetTail(node, pred)) {
                compareAndSetNext(pred, predNext, null);
            } else {
                // 如果node既不是tail,又不是head的后继节点
                int ws;
                // 如果前置结点状态是SIGNAL || 如果不是CAS设为SIGNAL 那么接下来的执行自然就会唤醒下一个结点
                if (pred != head &&
                    ((ws = pred.waitStatus) == Node.SIGNAL ||
                     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                    pred.thread != null) {
                    Node next = node.next;
                    if (next != null && next.waitStatus <= 0)
    				//设置next-link 使node的通过上面while循环找到的有效的前继结点指向node的下一结点
    				//之所以还是CAS,使下一个结点还是会可能取消自己的
                        compareAndSetNext(pred, predNext, next);
                } else {  // 如果node是head的后继节点,则直接唤醒node的后继节点
                    unparkSuccessor(node);
                }
    
                node.next = node; // help GC
                // 最终把取消acquire的 node的next指针指向的它自己
            }
        }
    

    场景1. node是tail

    node出队的过程如下图所示。

    cfbb15985794420d83e41b7f5bdf3eca.png

    1. cancelAcquire()调用compareAndSetTail()方法将tail指向pred

    2. cancelAcquire()调用compareAndSetNext()方法将pred的next指向空

    场景2. node既不是tail,也不是head的后继节点

    node出队过程如下图所示:
    7ca429ecf00c49659508d962c4d88d1b.png

    cancelAcquire()调用了compareAndSetNext()方法将pred指向successor。将successor指向pred是谁干的?是别的线程做的。当别的线程在调用cancelAcquire()或者shouldParkAfterFailedAcquire()时,会根据prev指针跳过被cancel掉的前继节点,同时,会调整其遍历过的prev指针。代码类似这样;

    cancelAcquire()中:
     // Skip cancelled predecessors
            Node pred = node.prev;
            while (pred.waitStatus > 0)
                node.prev = pred = pred.prev;
    			
    shouldParkAfterFailedAcquire中:
      do {
           node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
                pred.next = node;
    			
    

    场景3 node是head的后继节点

    node出队的过程如下图所示(图中用node*表示前继节点)

    97aad488b40a429caad02185648f89ad.png

    cancelAcquire()调用了unparkSuccessor(),不过,unparkSuccessor()中并没有对队列做任何调整呀。
    比场景2还糟糕,这次,cancelAcquire()对于出队这件事情可以说是啥都没干。
    出队操作实际上是由unparkSuccessor()唤醒的线程执行的。
    unparkSuccessor()会唤醒successor结点,当被唤醒后将会实际执行出队操作。
    现在需要搞清楚successor是从什么地方恢复执行的呢?这要看successor是在哪里被挂起的。

    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);
            }
        }
    

    successor线程是从parkAndCheckInterrupt被挂起的,恢复执行时,也从此处开始重新执行。successor将会重新执行for循环,此时,node尚未出队,successor的前继节点依然是node,而不是head。所以,successor会执行到shouldParkAfterFailedAcquire()处。而从场景2中可以得知,shouldParkAfterFailedAcquire()中将会调整successor的prev指针(同时也调整head的next指针),从而完成了node的出队操作
    fc5bcbd2274a4772ae1b1a5f16bc0f5e.png

    总结

    悲观锁在Java中就是我们所熟知的锁,实现方式主要分为两种:synchronized和Lock,而乐观锁的实现主要通过CAS操作实现。这里我们来比较下synchronized和Lock方式的大致差别:

    • synchronized主要依赖JVM底层实现,而Lock是通过编码方式实现,其实现方式差别还是比较大,可以参考之前的一个文章https://blog.csdn.net/lsgqjh/article/details/61915074
    • synchronized由于其简单方便,只需要声明在方法、代码块上即可,主要是不需要关心锁释放问题,在一般的编程中使用量还是比较大的,但是在真正的并发编程系统中,Lock方式明显优于synchronized:
      • 在高版本JDK中,已经对synchronized进行了优化,synchronized和Lock方式在性能方面差别已不太明显
        
      • synchronized最致命的缺陷是:synchronized不支持中断和超时,也就是说通过synchronized一旦被阻塞住,如果一直无法获取到所资源就会一直被阻塞,即使中断也没用,这对并发系统的性能影响太大了;Lock支持中断和超时、还支持尝试机制获取锁,对synchronized进行了很好的扩展,所以从灵活性上Lock是明显优于synchronized的

    参考连接:

    [1] Java并发编程的艺术
    [2] https://www.cnblogs.com/yangming1996/p/7612653.html
    [3]https://blog.csdn.net/lsgqjh/article/details/61915074
    [4]https://blog.reactor.top/2018/01/31/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E9%94%81%E4%B9%8BReentrantLock%E6%80%BB%E7%BB%93/
    [5]https://www.jianshu.com/p/01f2046aab64

    展开全文
  • 目录 背景知识 ReentrantLock的组成 概述 公平锁示意图 非公平锁示意图 源码解读 非公平锁 公平锁 代码对比 ...公平锁和非公平锁主要是通过内部类FairSync和NonFairSync中的tryAquire()方...
  • 主要介绍了java 线程公平锁与非公平锁详解及实例代码的相关资料,需要的朋友可以参考下
  • 排队打饭:公平锁和非公平锁(面试)

    万次阅读 2021-01-03 21:04:55
    目录 简介 公平锁、非公平锁概念 synchronized非公平锁 ReentrantLock非公平锁 ...其实公平锁这个概念是JUC工具包才有的,比如ReentrantLock才有公平锁的概念,这篇文章我们结合生活中的实例用.
  • 今天小编就为大家分享一篇关于Java线程公平锁和非公平锁的差异讲解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
  • 目录同花顺源码分析执行流程有什么不同说人话公平锁/非公平锁优劣公平锁公平锁为什么非公平锁的性能优于公平锁 同花顺 物竞天择,优胜劣汰。 源码分析 直接上源码 执行流程 /** * * ReentrantLock类中有...
  • 公平锁与非公平锁 公平锁:获取锁的先后顺序与请求锁的先后顺序保持一致,具有FIFO特点。 非公平锁:反之则为非公平锁。 什么是可重入? 同一个线程重复多次获取同一把锁,释放的时候也需要释放多次。 Java...
  • Java公平锁与非公平锁

    千次阅读 2020-02-10 22:02:26
    Java公平锁与非公平锁定义实例非公平锁之synchroized实现非公平锁之ReentrantLock实现公平锁 ReentrantLock实现性能测试性能测试场景测试结果结论 定义 当程序使用多线程里,难免有多线程争夺资源的情况。而资源只能...
  • Java 公平锁与非公平锁

    千次阅读 2019-03-23 22:39:44
    公平锁与非公平锁: 锁Lock分为公平锁和非公平锁公平锁:表示线程获取锁的顺序是按照加锁的顺序来分配的,及先来先得,先进先出的顺序。 非公平锁:表示获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是...
  • ReentrantLock默认为非公平锁,非公平锁公平锁效率高 非公平锁抢锁源码 公平锁抢锁源码 区别一:源码对比,发现非公平锁先cas一次自旋抢锁,自旋抢锁失败,会执行acquire(1) acquire方法源码 非...
  • synchronized是公平锁还是非公平锁

    千次阅读 2020-06-09 14:30:58
    synchronized是公平锁还是非公平锁
  • 一、公平锁/非公平锁(多线程执行顺序的维度) 公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。 非公平所:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。 例子 ...
  • 公平锁和非公平锁-ReentrantLock是如何实现公平、非公平的 1、什么是公平锁与非公平锁 公平锁公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁。 非公平锁:非公平锁则无法提供这个保障(先到的...
  • 大厂面试题:公平锁/非公平锁/重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁。 1、什么是公平锁和非公平锁 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似与排队打饭,先来后到。 非公平锁:是指多...
  • 公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁在高并发的情况下,有可能会...
  • 公平锁公平锁

    2016-03-01 23:05:46
    今天偶然看ArrayBlockingQueue源码时,发现其中有关于公平锁公平锁。也趁此学习一下什么叫做公平锁公平锁。 下面是ArrayBlockingQueue的部分源码:  public ArrayBlockingQueue(int capacity) {  this...
  • 公平锁和非公平锁的区别 公平锁 指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程 非公平锁 指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾...
  • AQS之公平锁和非公平锁(二) 一、概念 注意:因为ReentrantLock 类可以实现公平锁和非公平锁,所以本文的讲解以该类为主。 1.1 公平锁 ​ 多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 61,436
精华内容 24,574
关键字:

公平锁