Java并发——ReentrantLock

521 阅读5分钟

简介

ReentrantLock即可重入锁(当前线程获取该锁再次获取不会被阻塞),是一种递归无阻塞的同步机制。ReentrantLock基于AQS来实现,相对于内置锁synchronized关键字功能更强大,多了等待可中断、公平性、绑定多个条件等机制,还可以tryLock()避免死锁,而若单独从性能角度出发,更推荐synchronized

ReentrantLock

锁获取锁流程:

lock方法:


    public void lock() {
        sync.lock();
    }

Sync为ReentrantLock里面的一个内部类,它继承AQS,它有两个子类:公平锁FairSync和非公平锁NonfairSync,ReentrantLock里面大部分的功能都是委托给Sync来实现的,以非公平锁为例其lock()方法


    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

若锁未线程占有,把同步器中的exclusiveOwnerThread设置为当前线程
若锁已有线程占有,nonfairTryAcquire方法中,会再次尝试获取锁,在这段时间如果该锁被成功释放,就可以直接获取锁而不用挂起,其完整流程:

图片来自占小狼——深入浅出ReentrantLock

公平锁与非公平锁

公平锁与非公平锁的区别在于获取锁的时候是否按照FIFO的顺序。

  • 非公平锁
  • ReentrantLock默认采用非公平锁(组合方式)

    
        public ReentrantLock() {
            sync = new NonfairSync();
        }
    

    实现非公平锁的核心方法nonfairTryAcquire(),其源码如下:

    
        final boolean nonfairTryAcquire(int acquires) {
                //获取当前线程
                final Thread current = Thread.currentThread();
                //获取同步状态
                int c = getState();
                // 若同步状态为0,表明该锁未被任何线程占有
                if (c == 0) {
                    // CAS设置同步状态
                    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;
            }
    

    其主要逻辑:判断同步状态是否为0,若为0表明该锁未被任何线程占有,CAS设置同步状态;若不为0表明该锁已被线程占有,判断锁占有线程是否是当前线程,若是增加同步状态(可重入性机制实现的关键)

  • 公平锁
  • 公平锁,通过ReentrantLock有参构造方法传入true

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

    实现公平锁的核心方法tryAcquire(),其源码如下:

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

    可以很明显地发现与nonfairTryAcquire()方法唯一的区别在于CAS设置尝试设置state值之前,调用了hasQueuedPredecessors()判断当前线程是否位于CLH同步队列中的第一个,若不是先执行完同步队列中结点的线程,当前线程进入等待状态

    
        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());
        }
    
  • 公平锁与非公平锁
  • 公平锁每次获取到锁为同步队列中的第一个节点,符合请求资源时间上的绝对顺序,而非公平锁可能使线程"饥饿",有些线程可能一直获取不到锁,而刚释放锁的线程可能再次获得该锁,也正因为如此非公平锁会降低一定的上下文切换,降低性能开销,公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换。所以ReentrantLock默认采用非公平锁保证系统更大的吞吐量

    可重入性

    可重入性需要解决以下两个问题:

    ①.线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取 次成功获取
    ②.锁的最终释放:线程重复n次获取了锁,只有在n次释放该锁后,其他线程才能获取到该锁

    在nonfairTryAcquire()、tryAcquire()方法中都有这段代码:

    
        if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
    

    为了支持可重入性,若同步状态不为0时,还会再判断锁持有线程是否是当前请求线程,若是再次获取该锁,同步状态加1。再来看看释放锁:

    
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 同步状态为0时,锁才能释放,将其持有线程置为null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    

    只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

    绑定多个条件

    每一个Lock可以有任意数据的Condition对象,Condition是与Lock绑定的。Condition接口定义的方法,await对应于Object.wait,signal对应于Object.notify,signalAll对应于Object.notifyAll。

    生产者消费者简单demo

    
        public class Resource {
    
        private int num = 1;//当前数量
    
        private int maxNum = 10;//极值
    
        private Lock lock = new ReentrantLock();
    
        private Condition productCon = lock.newCondition();
    
        private Condition consumerCon = lock.newCondition();
    
        public void product() {
            lock.lock();
            try {
                while (num >= maxNum) {
                    try {
                        System.out.println("当前已满");
                        productCon.await();
                    } catch (InterruptedException e) {
    
                    }
                }
                num++;
                System.out.println("生产者" + Thread.currentThread().getName() + "当前有" + num + "个");
                consumerCon.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public void consume() {
            lock.lock();
            try {
                while (num == 0) {
                    try {
                        System.out.println("当前已空");
                        consumerCon.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                num--;
                System.out.println("消费者" + Thread.currentThread().getName() + "当前有" + num + "个");
                productCon.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            final Resource r = new Resource();
            // 生产者
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        r.product();
                    }
                }
            }).start();
            // 消费者
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        r.consume();
                    }
                }
            }).start();
        }
    }
    

    感谢

    《java并发编程的艺术》
    https://www.jianshu.com/p/4358b1466ec9