目 录CONTENT

文章目录

ReentrantLock

FatFish1
2024-10-23 / 0 评论 / 0 点赞 / 67 阅读 / 0 字 / 正在检测是否收录...

ReentrantLock的概念

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相比synchronized,ReentrantLock具备以下特点:

  • 可中断

  • 可以设置超时时间

  • 可以设置为公平锁

  • 支持多个条件变量

  • 与 synchronized 一样,都支持可重入

ReentrantLock与synchronized的区别

synchronized和ReentrantLock的区别:

  • synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;

  • synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断

  • synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的

  • synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的

  • 在发生异常时synchronized会自动释放锁而ReentrantLock需要开发者在finally块中显示释放锁

  • ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;

  • synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁

使用ReentrantLock

同步执行

public class ReentrantLockDemo {
    private static  int sum = 0;
    private static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(()->{
                //加锁
                lock.lock();
                try {
                    // 临界区代码
                    // TODO 业务逻辑:读写操作不能保证线程安全
                    for (int j = 0; j < 10000; j++) {
                        sum++;
                    }
                } finally {
                    // 解锁--一定要在finally中解锁,防止业务代码异常,无法释放锁
                    lock.unlock();
                }
            });
            thread.start();
        }

        Thread.sleep(2000);
        System.out.println(sum);
    }
}

必须要在finally种解锁,防止业务抛出异常,导致锁永远放不掉

可重入锁

可重入锁就是 A(加锁)–>调用—>B(加锁)–>调用–>C(加锁),从A到C即使B/C都有加锁,也可以进入。

public class ReentrantLockDemo2 {
    public static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        method1();
    }

    public static void method1() {
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public static void method2() {
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }
    public static void method3() {
        lock.lock();
        try {
            log.debug("execute method3");
        } finally {
            lock.unlock();
        }
    }
}

锁中断

可以使用lockInterruptibly来进行锁中断

lockInterruptibly()方法能够中断等待获取锁的线程。当两个线程同时通过lock.lockInterruptibly()获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

public class ReentrantLockDemo3 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {

            log.debug("t1启动...");

            try {
                lock.lockInterruptibly();
                try {
                    log.debug("t1获得了锁");
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("t1等锁的过程中被中断");
            }

        }, "t1");

        lock.lock();
        try {
            log.debug("main线程获得了锁");
            t1.start();
            //先让线程t1执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t1.interrupt();
            log.debug("线程t1执行中断");
        } finally {
            lock.unlock();
        }
    }
}

锁等待超时

可以让线程等待指定的时间,如果还未获取锁则进行失败处理。

如下代码,首先让主线程获得锁,然后让子线程启动尝试获取锁,但是由于主线程获取锁之后,让线程等待了2秒,而子线程获得锁的超时时间只有1秒,如果未获得锁,则进行return失败处理

Thread t1 = new Thread(() -> {
            log.debug("t1启动...");
            //超时
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("等待 1s 后获取锁失败,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
            try {
                log.debug("t1获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

上面的代码线程1修改如下逻辑,主线程sleep(2000)

公平锁

公平锁和非公平锁:

非公平锁是ReentrantLock的默认实现,当AQS阻塞队列中有node等待时,前面释放了锁,这时又来了一个新线程要求获取锁。这时候新线程可以直接跟等锁的node竞争,并可能获取到锁,使node再次加锁失败。

公平锁就是有新线程要求锁时,先判断等待队列中是否有节点,有的话要排到队尾等待。

ReentrantLock默认不公平

分两个循环各创建500个线程占用锁,公平锁情况下,上面500个都执行完了,下面才能一起去获取锁。

public class ReentrantLockDemo5 {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock(true); //公平锁

        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "t" + i).start();
        }
        // 1s 之后去争抢锁
        Thread.sleep(1000);

        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    log.debug(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "强行插入" + i).start();
        }
    }
}

那ReentrantLock为什么默认使用非公平锁呢?实际上就是为了提高性能,如果使用公平锁,当前锁对象释放之后,还需要去队列中获取第一个排队的线程,然后进行加锁处理。而非公平锁,可能再当前对象释放锁之后,正好有新的线程在获取锁,这样就可以直接进行加锁操作,不必再去队列中读取

reentrantLock代码分析

ReentrantLock基于AQS开发,核心成员变量有:sync、state。sync即锁的同步式控制。此外就是AQS继承来的volatile变量state,它是锁控制的关键

private final Sync sync;

private volatile int state;  // 继承自AQS

其中包含两个子类:NonfairSync和FairSync。

sync继承自AbstractQueuedSynchronizer,自然也继承了exclusiveOwnerThread持锁对象。

非公平锁NonfairSync

lock - 加锁

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

非公平锁的加锁方法,可见上来是先检查状态为0,配置为1。如果配置成功,则加锁成功,然后将当前线程信息配置给exclusiveOwnerThread持锁对象。如果配置不成功,则意味着没竞争到锁,则尝试基于AQS的逻辑重新获取锁。

nonfairTryAcquire

NonfairSync对tryAcquire的实现委托给nonfairTryAcquire方法实现

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

如果当前state=0,则可以正常加锁,修改state为1并且配置持锁线程。

如果state不为0,判断当前线程是否为持锁线程,如果是,则state自增。这里是可重入锁的逻辑,是否的时候也得一层一层释放

如果state不为0,持锁线程也不是当前线程,则证明加不上锁,返回false

这里需要关注的是state是volatile变量,具备同步特性,因此就等于锁被本线程独占了。

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) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

因为是可重入锁,这里是要state进行一层一层释放,直到c为0,才真正说明锁已经释放了,把持锁线程set为null。

可以看到,释放锁最后写state值

公平锁FairSync

公平锁的tryAcquire没有委托,与NonfairSync调用的nonFairTryAcquire相比,区别在于加锁的时候做了额外判断:

if (c == 0) {
    if (!hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}

比nonFair多判断了一个!hasQueuedPredecessors,即加入了同步队列中当前节点是否有前驱节点的判断

0

评论区