美文网首页
公平锁与非公平锁

公平锁与非公平锁

作者: Magic旭 | 来源:发表于2019-07-26 10:17 被阅读0次

公平锁与非公平锁含义

公平锁:按照线程等待时间来排序,等待越久的先被执行。内部代码实现说白就是一个链表(等待队列),有对应的preview和next,每次锁释放后从链表头部开始取,然后修改head的位置。(First in First out)
非公平锁:非公平锁发生在对象A.unLock同时,对象B里面想获取锁,这时候对象A来没来得及通知等待队列里面的非洲孩子(thread),就被对象B抢先占用了锁,这就是非公平锁。而公平锁在这种情况下还是会让B进入等待队列去排队,等待取号。


源码分析前奏-volatile

Java内存模型

这部分在Jvm的第五部分,高效并发,具体详细可以自行看书。主要要了解,线程对所有值的操作都是基于自己的工作内存操作的,而工作内存中的值又是从主内存拷贝过来的。当线程对值操作结束后,工作内存会重新写入到主内存中,所以在多线程问题下处理变量才需要我们额外花点心思。


image.png
volatitle作用(JVM内存模式章节)

volatitle的轻量级的同步机制,作用是:一、保证变量对所有的线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说都是可以立即得知的(但并非立即可见)。这句话简单来说,就是工作内存每次使用前都会从主内存中获取一次该变量的值。
注意:以下两种场景满足时候,我们可以通过volatitle去实效锁的效果:一、运算结果并不依赖变量的当前值(i++这种属于依赖当前值了),或者能够确保只有单一的线程修改变量值。二、变量不需要与其他的状态变量共同参与不变约束。


源码分析

ReentrantLock锁
  1. ReentrantLock可以实现公平/非公平锁的效果。
  2. 构造函数中可以传入boolean,从而实现你需要实现哪个锁的效果。
//默认构造方法,非公平锁
public ReentrantLock() {
        sync = new NonfairSync();
    }
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
public void lock() {
        sync.lock();
    }
非公平锁
获取锁过程
  1. 非公平锁在lock方法里面可以看到,只会判断当前锁是否被对象持有,不会判断等待队列里面是否有线程。compareAndSetState(int expect, int update)是期待0,如果是0,将状态改成1。0是锁未被持有状态。如果期待值不是0,就会走到acquire方法,里面的第一个判断条件,又会走进具体的NonfairSync/FairSync的tryAcquire方法里面。
  2. nonfairTryAcquire方法里判断当前锁的状态,如果未被持有直接就让当前线程持有锁(与下面对比可以明显看出if条件少了个判断,剩下的都基本大同小异,这里就不详细讲了)。
static final class NonfairSync extends Sync { 
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //添加到等待队列里
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
//nonfairTryAcquire方法在Sync父类已经默认实现了(只能感叹什么都是相对公平而已,连个程序都告诉没有默认的公平,要公平得自己创造)
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;
        }
公平锁
获取锁过程
  1. 公平锁在lock方法里面可以看到与上面的明显区别,公平锁根本没给你机会判断锁的使用情况,直接把你丢进acquire(1)里。acquire方法第一个判断条件,又会走进具体的NonfairSync/FairSync的tryAcquire方法里面。
  2. tryAcquire方法里判断当前锁的状态,如果未被持有,还要判断等待队列里是否有线程在排队了,如果都符合:1.锁未被持有;2.等待队列为空,就让当前线程持有锁。
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
           //直接添加到等待队列里面
            acquire(1);
        }
        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;
                }
            }
            //这里可以看出ReentrantLock属于重入锁
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
等待队列
Note结点
  1. 等待队列就是自行定义的一个数据结构,里面很多变量都用到volatitle,这也是为什么开头我就要介绍volatitle字段的用法和含义。
static final class Node {
……
//线程等待状态
volatile int waitStatus;
//
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
……
}
尾部插入等待结点
  1. 公平锁/非公平锁的acquire方法里第二个判断条件就有个addWaiter方法,这个方法就是构建等待队列的关键。
private Node addWaiter(Node mode) {
        //重新创建是为了在构造函数中把当前线程存储起来
        Node node = new Node(mode);
        for (;;) {
            //获取链表的尾部结点
            Node oldTail = tail;
            if (oldTail != null) {
                U.putObject(node, Node.PREV, oldTail);
                //判断链表尾部是否为oldTail,是的会就更新成node,这个很多同步操作都可以看到用到这个底层API去实现的
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                //如果尾部的Node为空,证明还没有等待队列
                //创建等待队列
                initializeSyncQueue();
            }
        }
    }
判断队列中线程状态
  1. 等待队列中的添加线程,首先要判断线程自身状态,因为线程只能自己中断自己,别人无法帮你做这个东西。所以入队列的时候需要检测自己是否已经被中断了。
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //线程自己设置自己的中断状态
            selfInterrupt();
 }
//只看acquireQueued里面的shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * 从链表中删除掉已经被置成cancelled的线程
             * 
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 刚开始所以加入等待队列的线程都被置成SIGNAL状态(等待被唤醒)
             */
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

//检测
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        //👇这个和线程的interrupt很像,但这只不过是获取线程是否中断状态
        return Thread.interrupted();
    }

等待队列中取出线程

  1. 因为我们在占用锁的时候,把当前线程附值给了exclusiveOwnerThread变量,所以当锁调用unlock方法时候,我们要这个变量置null
public void unlock() {
        //调用抽象类AbstractQueuedSynchronizer的方法,然后回调到抽象类Syn里面
        sync.release(1);
}
//AbstractQueuedSynchronizer的方法
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
//Syn的方法
protected final boolean tryRelease(int releases) {
            //参数默认值都是1,然后c最后等于0,就证明释放干净了
            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;
}
  1. 将等待队列里面的下一个线程取出,里面你会发现head其实是指当前这个持有锁的线程,并非说是等待线程。这里就很简单,取队列前判断里面有没有线程被cancelled了,有就删除掉。然后把下个准备持有锁的线程解除阻塞状态(唤醒线程去持有锁)。
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            //waitStatus大于0都是需要cancelled的线程
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//解除阻塞状态
    }

总结

  1. 发现很多的同步机制都依赖compareAndSet****方法,这个可能后面有时间想去研究下里面的实现。
  2. 看了一次锁的同步机制,后面自己在写多线程的调用时能避免踩更多的坑。

相关文章

网友评论

      本文标题:公平锁与非公平锁

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