侧边栏壁纸
博主头像
再见理想博主等级

只争朝夕,不负韶华

  • 累计撰写 112 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

深入了解重入锁 ReentrantLock

再见理想
2022-05-29 / 0 评论 / 0 点赞 / 328 阅读 / 2,253 字

一,ReentrantLock类简介

ReentrantLock 类,实现了 Lock 接口,是一种可重入的独占锁,在同一个时间点只能被一个线程锁持有,可重入表示,ReentrantLock锁可以被同一个线程多次获取。

通过 lock 生成可一个与之绑定的 Condition 对象,用来替代传统的 Object 的 wait()、notify() 实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的 await()、signal() 这种方式实现线程间协作更加安全和高效

它具有与使用 synchronized 相同的一些基本行为和语义,但功能更灵活更强大。ReentrantLock 内部通过内部类实现了AQS 框架(AbstractQueuedSynchronizer) 的 API 来实现独占锁的功能。

二,主要方法

lock()

①获取锁成功:线程直接返回;

②获取锁失败:加入等待队列,直到获取锁成功;

③等待过程中被中断:不响应,直到获取锁后再自我中断 selfInterrupt()。

tryLock()

线程尝试获取锁,如果获取成功,则返回 true,如果获取失败,则返回 false。

tryLock(long timeout,TimeUnit unit)

线程如果在指定等待时间内获得了锁,就返回true,否则返回 false。

lockInterruptibly()

对线程 interrupt 方法做出响应。

三,ReentrantLock类原理

通过内部类实现了 AQS 框架,Lock 接口的实现仅仅是对 AQS 的 api 的简单封装。

lock() 源码

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

Sync 继承 AbstractQueuedSynchronizer (AQS),对AQS 的方法进行封装。

非公平锁

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);  // 调用AQS 中的方法
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

可见,非公平锁中,首先通过 compareAndSetState 抢锁,抢锁失败,再调用的是 AQS 中的 acquire 获取独享锁方法 AQS源码解读-独占模式下获取锁和释放锁

公平锁

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

当 state = 0 时,通过 hasQueuedPredecessors 先判断等待队列中是否有节点,有节点在等待则放弃争抢锁,体现了获取锁的公平性。

四,中断响应

对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待,synchronized在等待锁时不能被中断

而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,通过 lockInterruptibly() 方法,程序可以根据需要取消对锁的请求。

五,锁申请等待限时

我们可以使用 tryLock() 方法尝试获得锁,超过限定时间仍未成功获得锁,将放弃对锁的获取。可以避免死锁。

六,公平锁

ReentrantLock 类的其中一个构造器提供了指定公平策略 / 非公平策略的功能,默认为非公平策略。

公平策略:在多个线程争用锁的情况下,公平策略倾向于将访问权授予等待时间最长的线程。也就是说,相当于有一个线程等待队列,先进入等待队列的线程后续会先获得锁,这样按照“先来后到”的原则,对于每一个等待线程都是公平的。

非公平策略:在多个线程争用锁的情况下,能够最终获得锁的线程是随机的(由底层OS调度)。

七,简单案例

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockService implements Runnable {

    private static ReentrantLock lock = new ReentrantLock();
    private static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j < 100; j++) {
            lock.lock();
            try {
                i++;
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockService service = new ReentrantLockService();
        Thread t1 = new Thread(service);
        Thread t2 = new Thread(service);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

八,重入锁好搭档:Condition条件

Condtion是与重入锁相关联的,通过Lock接口(重入锁就实现了这一接口) 的Condition newCondition() 方法可以生成一个与当前重入锁绑定的 Condition 实例。利用 Condition 对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。

当线程使用 Condition await时,要求线程持有相关的重入锁,在 Condition await调用后,这个线程会释放这把锁。同理,在 Condition signal() 方法调用时,也要求线程先获得相关的锁。在 signal()方法调用后,系统会从当前 Condition 对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在 signal()方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让它可以继续执行。

几个重要方法:

await():方法会使当前线程等待,同时释放当前锁,当其他线程中使用 signal() 或者 signal All() 方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和 Object. wait()方法很相似。

awaitUninterruptibly():方法与 await() 方法基本相同,但是它并不会在等待过程中响应中断。

singal():方法用于唤醒一个在等待中的线程。相对的 singal All() 方法会唤醒所有在等待中的线程。

九,ReentrankLock 结合 Condition 使用案例

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//用ReentrantLock绑定三个条件实现线程A打印一次1,线程B打印两次2,线程C打印三次3
class ReentrantLockService {
    private int number = 1;//A:1  B:2  C:3
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    //1 判断
    public void print1() {

        lock.lock();

        try {
            //判断
            while (number != 1) {
                c1.await();
            }
            //2 do sth
            for (int i = 0; i < 1; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + number);
            }

            //3 通知
            number = 2;
            c2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //1 判断
    public void print2() {

        lock.lock();

        try {
            //判断
            while (number != 2) {
                c2.await();
            }
            //2 do sth
            for (int i = 0; i < 2; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + number);
            }

            //3 通知
            number = 3;
            c3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //1 判断
    public void print3() {

        lock.lock();

        try {
            //判断
            while (number != 3) {
                c3.await();
            }
            //2 do sth
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + number);
            }

            //3 通知
            number = 1;
            c1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {

        ReentrantLockService resource = new ReentrantLockService();

        new Thread(()->{
            for (int i = 1; i <= 2; i++) {
                resource.print1();
            }
        },"A").start();


        new Thread(()->{
            for (int i = 1; i <= 2; i++) {
                resource.print2();
            }
        },"B").start();


        new Thread(()->{
            for (int i = 1; i <= 2; i++) {
                resource.print3();
            }
        },"C").start();
    }
}

执行结果:

十,Reentranklock 和 synchronization对比

Synchronized :它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

ReentrantLock :它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

用途比较:

Reentranklock 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况;

Reentranklock 锁申请等待限时,避免产生死锁;

Reentranklock 可实现公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好;

Reentranklock 锁可以绑定多个条件,一个ReentrantLock对象可以同时绑定多个 Condition 对象,使用更灵活;

0

评论区