ReentrantLock源码分析和使用案例

2,985 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

源码分析

构造函数

/**
     * 初始化的时候默认给了一个不公平锁
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * 也可以加参数来初始化指定使用公平锁还是不公平锁
     *
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

常用方法

void  lock()   //加锁 
void  unlock()  //释放锁
tryLock() //仅在调用时锁定未被另一个线程保持的情况下才获取锁定。
tryLock(long timeout, TimeUnit unit) //如果锁定在给定的时间内没有被另一个线程保持且当前线程没有被中断,则获取这个锁定。
boolean isHeldByCurrentThread();   // 当前线程是否保持锁定
boolean isLocked()  // 是否存在任意线程持有锁资源
void lockInterruptbly()  // 如果当前线程未被中断,则获取锁定;如果已中断,则抛出异常(InterruptedException)
int getHoldCount()   // 查询当前线程保持此锁定的个数,即调用lock()方法的次数
int getQueueLength()   // 返回正等待获取此锁定的预估线程数
int getWaitQueueLength(Condition condition)  // 返回与此锁定相关的约定condition的线程预估数
boolean hasQueuedThread(Thread thread)  // 当前线程是否在等待获取锁资源
boolean hasQueuedThreads()  // 是否有线程在等待获取锁资源
boolean hasWaiters(Condition condition)  // 是否存在指定Condition的线程正在等待锁资源
boolean isFair()   // 是否使用的是公平锁

公平锁与非公平锁

static final class FairSync extends Sync {
    final void lock() { // 1 注意对比公平锁和非公平锁的这个方法
        acquire(1);
    }
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 2 和非公平锁相比,这里多了一个判断:是否有线程在等待
            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;
    }
}

static final class NonfairSync extends Sync {
    final void lock() {
      //  1 和公平锁相比,这里会直接先进行一次CAS,如果当前正好没有线程持有锁,
      // 如果成功获取锁就直接返回了,就不用像公平锁那样一定要进行后续判断
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
 
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 2 这里没有对阻塞队列进行判断
        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()时,非公平锁首先会进行一次CAS尝试抢锁,如果此时没有线程持有锁或者正好此刻有线程执行完释放了锁(state == 0),那么如果CAS成功则直接占用锁返回。 (2) 如果非公平锁在上一步获取锁失败了,那么就会进入nonfairTryAcquire(int acquires),在该方法里,如果state的值为0,表示当前没有线程占用锁或者刚好有线程释放了锁,那么就会CAS抢锁,如果抢成功了,就直接返回了,不管是不是有其他线程早就到了在阻塞队列中等待锁了。而公平锁在这里抢到锁了,会判断阻塞队列是不是空的,毕竟要公平就要讲先来后到,如果发现阻塞队列不为空,表示队列中早有其他线程在等待了,那么公平锁情况下线程会乖乖排到阻塞队列的末尾。 如果非公平锁 (1)(2) 都失败了,那么剩下的过程就和非公平锁一样了。 (3) 从(1)(2) 可以看出,非公平锁可能导致线程饥饿,但是非公平锁的效率要高。

使用案例

lock()、unlock()

@Slf4j
@ThreadSafe
public class ReentrantLockExample {

    /**
     * 请求总数
     */
    public static int clientTotal = 5000;

    public static int count = 0;
    /**
     * 定义一个可重入锁
     */
    final static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws Exception {
        //创建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //计数器  (把请求计数)
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    add();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                //计数器减1
                countDownLatch.countDown();
            });
        }
        //当所有请求结束
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        //操作前加锁
        lock.lock();
        try {
            count++;
        } catch (Exception e) {

        } finally {
            //操作后在finally中关闭锁,确保锁成功释放,避免死锁
            lock.unlock();
        }
    }
}

多次输出结果都是5000,证明线程安全。

19:43:01.841 [main] INFO com.zjq.concurrency.lock.ReentrantLockExample - count:5000

new ReentrantLock(true)公平锁

	/**
     * 定义一个公平锁 ReentrantLock(true)参数为true,表明实现公平锁机制
     */
    final static ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) throws Exception {
        new Thread(() -> testLock(), "线程壹号").start();
        new Thread(() -> testLock(), "线程贰号").start();
        new Thread(() -> testLock(), "线程叁号").start();
    }

    private static void testLock() {
        for (int i = 0; i < 2; i++) {
            //操作前加锁
            lock.lock();
            try {
                log.info("{}获取了锁", Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //操作后在finally中关闭锁,确保锁成功释放,避免死锁
                lock.unlock();
            }
        }
    }

输出结果,两次执行顺序完全一样。 image.png

new ReentrantLock()非公平锁

    /**
     * 定义一个非公平锁,new一个ReentrantLock的时候参数默认为false,可以不用指定为false
     */
    final static ReentrantLock lock = new ReentrantLock();

非公平锁输出没有规律,随机的获取,谁运气好,cpu时间片轮到哪个线程,哪个线程就能获取锁 image.png

lockInterruptbly()响应中断

 /**
     * 定义一个非公平锁,new一个ReentrantLock的时候参数默认为false,可以不用指定为false
     */
    final static ReentrantLock lock1 = new ReentrantLock();
    final static ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(new ThreadTest(lock1, lock2), "线程壹号");
        Thread thread2 = new Thread(new ThreadTest(lock2, lock1), "线程贰号");
        thread1.start();
        thread2.start();
        thread1.interrupt();
    }


    static class ThreadTest implements Runnable {

        Lock lock111;
        Lock lock222;

        public ThreadTest(Lock lock111, Lock lock222) {
            this.lock111 = lock111;
            this.lock222 = lock222;
        }

        @Override
        public void run() {
            try {
                //如果当前线程未被中断,则获取锁定;如果已中断,则抛出异常
                lock111.lockInterruptibly();
                TimeUnit.MILLISECONDS.sleep(50);
                lock222.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock111.unlock();
                lock222.unlock();
                log.info("{}获取到了锁", Thread.currentThread().getName());
            }

        }
    }

我们定义了两个锁lock1和lock2。然后使用两个线程thread1和thread2构造死锁场景。正常情况下,这两个线程相互等待获取资源而处于死循环状态。但是我们此时thread1中断,另外一个线程就可以获取资源,正常地执行了。 输出结果: image.png

tryLock()、tryLock(long timeout, TimeUnit unit)

/**
     * 定义一个非公平锁,new一个ReentrantLock的时候参数默认为false,可以不用指定为false
     */
    final static ReentrantLock lock1 = new ReentrantLock();
    final static ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(new ThreadTest(lock1,lock2),"线程壹号");
        Thread thread2 = new Thread(new ThreadTest(lock2,lock1),"线程贰号");
        thread1.start();
        thread2.start();
    }


    static class ThreadTest implements Runnable{

        Lock lock111;
        Lock lock222;

        public ThreadTest(Lock lock111, Lock lock222) {
            this.lock111 = lock111;
            this.lock222 = lock222;
        }

        @Override
        public void run() {
            try {
                while (!lock1.tryLock()) {
                    TimeUnit.MILLISECONDS.sleep(50);
                    log.info("{}尝试获取锁",Thread.currentThread().getName());
                }
                while (!lock2.tryLock()) {
                    TimeUnit.MILLISECONDS.sleep(50);
                    log.info("{}尝试获取锁",Thread.currentThread().getName());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock111.unlock();
                lock222.unlock();
                log.info("{}获取到了锁",Thread.currentThread().getName());
            }

        }
    }

输出结果:

22:31:13.316 [线程壹号] INFO com.zjq.lock.ReentrantLockTryLockExample - 线程壹号获取到了锁
22:31:13.325 [线程贰号] INFO com.zjq.lock.ReentrantLockTryLockExample - 线程贰号尝试获取锁
22:31:13.325 [线程贰号] INFO com.zjq.lock.ReentrantLockTryLockExample - 线程贰号获取到了锁

Condition的使用

Condition可以非常灵活的操作线程的唤醒,下面是一个线程等待与唤醒的例子,其中用1234序号标出了日志输出顺序

 /**
     * 请求总数
     */
    public static int clientTotal = 5000;

    public static int count = 0;
    /**
     * 定义一个可重入锁
     */
    final static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws Exception {
            //创建一个可重入锁
            ReentrantLock reentrantLock = new ReentrantLock();
            //创建condition
            Condition condition = reentrantLock.newCondition();
            //线程1
            new Thread(() -> {
                try {
                    reentrantLock.lock();
                    log.info("wait signal"); // 1
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("get signal"); // 4
                reentrantLock.unlock();
            }).start();
            //线程2
            new Thread(() -> {
                reentrantLock.lock();
                log.info("get lock"); // 2
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //发送信号
                condition.signalAll();
                log.info("send signal"); // 3
                reentrantLock.unlock();
            }).start();
    }

输出结果: image.png 输出讲解:

  1. 线程1调用了reentrantLock.lock(),线程进入AQS等待队列,输出1号log
  2. 接着调用了awiat方法,线程从AQS队列中移除,锁释放,直接加入condition的等待队列中
  3. 线程2因为线程1释放了锁,拿到了锁,输出2号log
  4. 线程2执行condition.signalAll()发送信号,输出3号log
  5. condition队列中线程1的节点接收到信号,从condition队列中拿出来放入到了AQS的等待队列,这时线程1并没有被唤醒。
  6. 线程2调用unlock释放锁,因为AQS队列中只有线程1,因此AQS释放锁按照从头到尾的顺序,唤醒线程1
  7. 线程1继续执行,输出4号log,并进行unlock操作。

本文内容到此结束了,

如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。

如有错误❌疑问💬欢迎各位大佬指出。

主页共饮一杯无的博客汇总👨‍💻

保持热爱,奔赴下一场山海。🏃🏃🏃

a37032f76d3ebe77e3e3d265ff1e1d7.jpg