并发编程07-线程安全解决方案之ReentrantLock锁

线程安全解决方案之ReentrantLock

Lock接口介绍

Lock是J.U.C包下面提供的一个接口,也是用来实现线程同步的一种解决方案,他提供了一个规范,定义了关于一个锁应该具备的能力,定义了加锁、解锁等基本的方法。实现类有ReentrantLockReadLock(在ReentrantReadWriteLock中做读锁)、WriteLock(在ReentrantReadWriteLock中做写锁)、Segment(在ConcurrentHashMap)中做分段锁。当然我们也可以通过实现这个接口去自定义我们的锁。因此,我们在来理解一下接口的意义,其实就是定义规范,定义如果你要实现一个锁,则必须按照我的规范来。

我们一起看下Lock接口定义了那些方法要我们去实现:

public interface Lock {
    
    /**
     * 线程阻塞的一种加锁方案,调用该方法用来获取锁,如果获取不到,线程会进入休眠状态,直到获取到锁为止
     */
    void lock();
    
    /**
     * 在上面方法的基础上加了中断机制
     */
    void lockInterruptibly() throws InterruptedException;
    
    /**
     * 尝试获取锁,如果锁可以用,立即获取锁,返回true,如果锁不可用,返回false
     */
    boolean tryLock();
    
    /**
     * 在上面方法的基础上加了超时机制,就是说如果不能立即获取到锁,允许多尝试几次,
     * 如果在指定的时间内获取到锁就返回true,如果时间到了还没获取到,在返回false
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    /**
     * 解锁,释放锁资源
     */
    void unlock();
       
    /**
     * 为锁绑定一个条件,这个可以用来实现wait/notify类似的线程通信的效果,后续展开
     */
    Condition newCondition();
}

ReentrantLock如何使用

ReentrantLock是我们最常用的Lock接口的一种实现,Reentrant是可重入的概念(后面展开)。他和synchroized关键字都是悲观锁。我们一起看下如何使用:

lock() and unlock()

Lock lock = new ReentrantLock();

public void sellTicket() {
    try {
        lock.lock();
        if (totalTicket > 0) {
            System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
            Thread.sleep(1000);
            totalTicket--;
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 切记切记,这里一定要记得在finally里释放锁,否则会导致锁资源被占用,其他线程无法始终无法获取锁,永远被阻塞
        lock.unlock();
    }

}

错误写法1:如果代码在第2行到第6行之间报了错,抛出异常,会导致无法走到第8行,锁无法释放

lock.lock();
if (totalTicket > 0) {
    System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
    Thread.sleep(1000);
    totalTicket--;
}
lock.unlock();

错误写法2:多个线程会加载自己私有的线程栈,lock作为局部变量是每个线程栈内部私有的,不共享,会导致有多少个线程访问,就有多少锁,没办法起到互斥的作用,如果不理解那些是线程私有的,可以参考:并发编程02-什么是线程安全以及Java虚拟机中哪些数据是线程共享的,那些是线程私有的

public void sellTicket() {
    Lock lock = new ReentrantLock();
    try {
        lock.lock();
        if (totalTicket > 0) {
            System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
            Thread.sleep(1000);
            totalTicket--;
        }
        lock.unlock();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

tryLock()

tryLock()尝试获取锁,如果获取不到,线程就不进行处理。之前做一个定时任务的需求,由于项目中没有分布式任务调度框架,故用了spring自带的定时任务。这就导致如果你的应用部署在多个节点上,那么到了设定的时间,三个节点会同时触发定时任务,实际上只需要触发一次即可。当时用数据库做分布式锁实现了Lock接口,当定时任务触发的时候,先调用tryLock()方法获取锁,获取到则触发定时任务,否则什么都不做。

public void sellTicket() {
    if (lock.tryLock()) {
        try {
            // 获取到锁的分支
        } finally {
            lock.unlock();
        }
    } else {
        // 获取不到锁的分支
    }
}

错误写法:如果有多个线程访问这个方法,同一时刻只有线程1获取到了锁,线程2没有获取到锁,那么线程2会走到第5行,尝试释放锁,而实际线程2并没有获取到锁,导致代码报错。

public void sellTicket() {
    if (lock.tryLock()) {
        // 执行获取到锁的业务逻辑
    }
    lock.unlock();
}

简单说明ReentrantLock如何实现加锁/解锁

Lock是一个接口,定义了锁的规范,提供了加锁/解锁的基本方法。而具体的核心逻辑是在J.U.C包下面有一个抽象类AbstractQueuedSynchronizerAQS),在这个抽象类里面结合模板方法设计模式实现了lockunlock()等基础功能,其他锁实现只需要继承这个抽象类,在做一些自定义的实现即可。通过这个设计可以进一步理解接口和抽象类的区别:接口定义规范(can-do),抽象类提取子类的公共逻辑进行实现,减少冗余代码(is-a)。关于AbstractQueuedSynchronizer的细节可以参考:通过ReentrantLock和Semaphore看AQS独占锁和共享锁的源码。这里我们在简单提一下基本加锁流程:

  1. AQS中定义了一个全局变量:state,初始化为0,如果有线程获取锁资源,则通过CAS的原子操作对state加一,表示加锁成功
  2. 当线程释放锁资源的时候,将state重新设置为0,表示解锁成功
  3. 如果同时有多个线程争抢锁资源,同时只有一个线程获取到了锁,其他的线程如何处理?AQS提供了一个用双向链表实现的同步队列,让其他线程去这个队列里面排队,当锁被释放后,从队列里面取出一个线程获取锁
  4. 假设线程2来获取锁资源,此时线程1刚好释放了锁,他则直接加锁成功,而没有去上面说的队列中排队等待,那么这种锁我们叫做非公平锁,通过new ReentrantLock(false)定义非公平锁,默认非公平
  5. 假如线程2来获取锁资源没有插队,而是乖乖去队列里面排队等候,那么这种锁我们叫做公平锁,通过new ReentrantLock(true)定义公平锁

在这里插入图片描述

如何理解Reentrant的概念

Reentrant,英语是可重入的意思,ReentrantLock即可重入锁,synchroized也是可重入锁(用法参考:并发编程04-线程安全解决方案之如何正确使用synchroized关键字)。什么是可重入锁呢,以下面的代码为例,如果某一个线程在调用method1的时候获取到了锁,那么在调用method2的时候也会自动获取锁,即可重入。假设不会自动获取,也就是不可重入,那么下面这两段代码就会这样运行:线程1执行method方法获取了锁,接着调用method2,又需要获取锁,那么此时锁被谁占用呢?线程1,因此他会等线程1释放锁,而线程1能释放锁嘛,不能,因为他还没有执行完,他在method2中等待获取锁。这就卡住了一个bug,导致了死锁。因此我认为可重入锁在一定程度上解决了死锁问题。

Lock lock = new ReentrantLock(true);

public void method1() {

    try {
        lock.lock();
        method2();
    } finally {
        lock.unlock();
    }

}

public void method2() {
    try {
        lock.lock();
        // 方法2的逻辑
    } finally {
        lock.unlock();
    }
}
public synchronized void method1() {
    method2();
}

public synchronized void method2() {

}

Lock锁和synchroized锁的区别

相同点:

  1. 他们都可以实现线程同步,都是悲观锁
  2. 都是可重入锁

不同点:

  1. synchroized是JVM层面实现的,lock是基于AQS框架实现的
  2. lock支持公平锁和非公平锁两种,synchroized是非公平的
  3. synchroized加锁解锁是自动的,lock需要手动去做,因为手动,则更加灵活,你可以在任意地方加锁解锁,但是一定要注意合理正确的释放锁,否则会造成死锁
  4. lock提供了tryLock()方法,这个是synchroized做不到的
  5. synchroized可以结合wait/notify机制实现线程通信,而lock锁可以用Condition实现线程通信
  6. lock接口下有读锁、写锁的实现类,用来为读写锁实现高性能的读写分离的锁,synchroized是做不到的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半__夏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值