跳至主要內容

ReentrantLock(独占锁)

Yaien Blog原创大约 5 分钟并发线程安全Lock

ReentrantLock(独占锁)

ReentrantLock 是Java中的一个工具类,位于 java.util.concurrent.locks 包下,是一种可重入的互斥锁,是 Lock
接口的一个实现。ReentrantLock
是由java提供的一种能够进行显示同步操作的锁,和synchronized不同的是,它是通过代码的方式来控制锁的获取和释放。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void accessResource() {
        lock.lock();  // 获取锁

        try {
            // 保护的代码块
            System.out.println(Thread.currentThread().getName() + " is accessing the resource");
        } finally {
            lock.unlock();  // 释放锁
        }
    }

    public static void main(String[] args) {
        final ReentrantLockExample example = new ReentrantLockExample();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                example.accessResource();
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                example.accessResource();
            }
        });

        t1.start();
        t2.start();
    }
}

这个例子中,我们使用 ReentrantLock 来保护一个共享资源的访问,通过调用 lock 方法获取锁,然后在 try 代码块中访问共享资源,最后在
finally 代码块中释放锁,这样可以确保即使在访问共享资源时发生异常,也可以正确地释放锁。 main
方法中,我们创建了两个线程同时访问这个共享资源。由于我们对访问共享资源的方法使用了锁进行了保护,所以即使有多个线程同时访问,也可以保证每次只有一个线程能访问这个方法。

公平锁和非公平锁

ReentrantLock 在创建时可以通过构造函数传入 fair 参数来决定这把锁是公平锁还是非公平锁。如果不传,默认是非公平锁。

/**
* 公平锁
*/
private static ReentrantLock lock = new ReentrantLock(true);

/**
* 非公平锁
*/
private static ReentrantLock lock = new ReentrantLock();

公平锁

是指多个线程按照申请锁的顺序来获取锁,避免"饥饿"现象。在ReentrantLock内部,通过一个双向链表来管理等待锁的线程,越早等待的线程越早获取锁。

加锁底层实现

    // 加锁逻辑
    final boolean nonfairTryAcquire(int acquires) {
         // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取state状态
        int c = getState();
        if (c == 0) { // 没有线程持有锁
            // cas尝试获取锁
            if (compareAndSetState(0, acquires)) { // 加锁成功
                // 标记当前线程,用来实现可重入锁
                setExclusiveOwnerThread(current);
                // 返回加锁成功
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) { // 有线程持有锁,判断是不是当前线程持有的
            // 锁标识+1(可重入实现)
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 更新state状态
            setState(nextc);
            return true;
        }
        // 加锁失败
        return false;
    }
  1. 创建非公平的lock,调用lock.lock()进行加锁
  2. 调用CAS尝试加锁(修改state值由0->1,如果上一个持有锁的线程刚好释放,有可能会直接获取到)
    • cas操作成功,state值为1,将当前线程赋值到exclusiveOwnerThread属性(独占线程表标识,做可重入锁的判断依据)
    • cas操作失败,进入acquire(1)方法
      • 调用tryAcquire(1)方法,首先判断state是否等于0,是则尝试加锁,否则判断当前线程是不是exclusiveOwnerThread标识的线程,是的话state值+1(可重入实现)
      加锁失败,调用acquireQueued入队,入队前还会判断是否已经有队列,没有先创建队列(通过CAS保证只有一个线程去创建),将当前拿到锁的线程放到队首,然后当前加锁失败的线程加入到队列中等待唤醒,阻塞前还会将前置节点的waitState置为-1,便于唤醒
    // 入队逻辑
    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;
                }
                
                // 阻塞线程前修改waitStatus,然后调用park()阻塞线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

解锁底层实现

    // 解锁逻辑
    protected final boolean tryRelease(int releases) {
        // state-1
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread()) {
            throw new IllegalMonitorStateException();
        }
        // 默认false,有可能是重入锁,c != 0 说明当前线程还有是重入锁,还没有完全释放
        boolean free = false;
        if (c == 0) { // 锁释放成功
            free = true;
            setExclusiveOwnerThread(null);
        }
        // 更新state状态
        setState(c);
        return free;
    }
  1. 调用lock.unlock()
  2. 调用release(1)方法
    • 调用tryRelease(1)方法,首先获取state执行-1操作,如果-1后的值为0,将exclusiveOwnerThread置空,最后将-1后的值赋值到state,释放锁
    • 锁释放成功,唤醒队列中下一个线程节点,当前线程出队

非公平锁

是指多个线程获取锁的顺序并不是按照申请锁的顺序进行的。这样可能造成优先级高的线程因为等待而导致一种称为“饥饿”的现象,但是相对于公平锁,非公平锁的性能上要高。在ReentrantLock内部,如果有线程试图获取锁,且锁当前未被其他线程持有,那么无论等待队列中是否有线程在等待,该线程都能立即获取到锁,即 "
插队"。

实现原理

公平锁的实现与非公平锁实现原理类似,只不过在加锁之后,直接就调用了acquire(1)方法,而非公平锁再进入acquire(1)方法之前还会去尝试加锁。

注意

  1. 在等待队列中的线程时,一定是唤醒下标为1的,因为0是记录当前获取锁的线程
  2. 非公平的实现,只针对于线程入队之前,线程入队后还是会遵循队列的先进先出原则

使用场景

  • 多线程并发控制
  • 超时等待
  • 可中断锁实现
  • 公平锁
  • 多条件锁实现
上次编辑于:
贡献者: yanggl