Java锁之可重入锁

一般遇到并发问题,都会想到synchronizedReentrantLocksynchronized一样都是实现线程同步,但是像比synchronized它更加灵活、强大、增加了轮询、超时、中断等高级功能,可以更加精细化的控制线程同步,它是基于AQS实现的锁,他支持公平锁和非公平锁,同时他也是可重入锁和自旋锁。

目录

一、初识ReentrantLock

二、简单使用

1、锁的过程

①Lock加锁

-非公平锁

-公平锁

②UnLock释放锁

③锁中断

三、总结


一、初识ReentrantLock

ReentrantLock就是可重入的锁,同一个线程可以多次获得同一把锁

在使用Synchronized,会存在以下几个问题:

  • 不可中断锁,需要线程执行完才会释放锁(synchronized的获取和释放锁由jvm实现)

  • 非公平锁

  • Synchronized引入了偏向锁,轻量级锁(自旋锁)后,性能有所提升


Synchronized

  1. 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制

  2. 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待

ReentrantLock

  1. 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁

  2. 可中断锁:可中断锁时子线程在获取锁的过程中,是否可以相应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的

  3. 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁


ReentrantLock位于java.util.concurrent.locks包下,实现了Lock接口和Serializable接口

 public class ReentrantLock implements Lock, java.io.Serializable {……}

synchronized的细粒度和灵活度不够好

ReentrantLock是一把可重入锁互斥锁,它具有与 Synchronized 关键字相同的含有隐式监视器锁(monitor)的基本行为和语义,但是它比 Synchronized 具有更多的方法和功能

二、简单使用

 
public class ReentrantLockTest {
     private static final Lock lock = new ReentrantLock();
 ​
 ​
     public static void test() {
         try {
             //获取锁
             lock.lock();
             System.out.println(Thread.currentThread().getName() + "获取到锁了");
             //业务代码,使用部分花费100毫秒
             Thread.sleep(100);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             //释放锁放在finally中。
             lock.unlock();
             System.out.println(Thread.currentThread().getName() + "释放了锁");
         }
     }
 ​
     public static void main(String[] args) {
         new Thread(() -> {
             test();
         }, "线程1").start();
 ​
         new Thread(() -> {
             test();
         }, "线程2").start();
 ​
     }
 }
 ​

运行结果:

效果和Synchronized的一样,线程1获取到锁了,线程2需要等待线程1释放锁后才可以获取锁

注意:为了防止锁不被释放,从而造成死锁,强烈建议把锁的释放lock.unlock()放在finally模块中

进入java.util.concurrent.locks.ReentrantLock中,发现ReentrantLock有三个内部类

ReentrantLock

—Sync

NofairSync

FairSync

更详细的图

Lock接口定义如下:

 public interface Lock {
 ​
     /**
      * 获取锁
      */
     void lock();
 ​
     /**
      * 获取锁-响应中断 
      */
     void lockInterruptibly() throws InterruptedException;
 ​
     /**
      * 返回获取锁是否成功状态
      */
     boolean tryLock();
 ​
     /**
      * 返回获取锁是否成功状态-响应中断 
      */
     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
 ​
     /**
      * 释放锁
      */
     void unlock();
 ​
     /**
      * 创建条件变量
      */
     Condition newCondition();
 }
对应UML图:

看到了一个熟悉的身影java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS同步队列器)

先简单的说下AQS(AbstractQueuedSynchronizer)流程,AQS为加锁和解锁过程提供了统一的模板函数,加锁与解锁的模板流程是,获取锁失败的线程,会进入CLH队列阻塞,其他线程解锁会唤醒CLH队列线程,如下图所示(简化流程)

线程释放锁时,会唤醒CLH队列阻塞的线程,重新竞争锁

注意:此时可能还有CLH队列的线程参与竞争

非公平策略

所以非公平就体现在这里,CLH队列线程与CLH队列线程竞争,各凭本事,不会因为你是CLH队列的线程,排了很久的队,就把锁让给你

1、锁的过程

①Lock加锁
-非公平锁

非公平锁获取锁时不会判断阻塞队列是否有线程再等待,所以对于已经在等待的线程来说是不公平的,但如果是因为其它原因没有竞争到锁,它也会加入阻塞队列

进入阻塞队列的线程,竞争锁时都是公平的,应为队列为先进先出(FIFO)

可以从源码中看出ReentrantLock默认为非公平锁,当fair参数为true为公平锁,但是系统内部肯定需要维护一个有序队列,因此公平锁的实现成本比较高,性能相对于非公平锁来说相对低一些

 ReentrantLock fairLock = new ReentrantLock();     //默认非公平锁
 ReentrantLock fairLock = new ReentrantLock(true); //公平锁

然后进入到NofairSync的lock()中,使用final修饰,该方法不能被重写

通过CAS操作来修改state的状态,表示争抢锁的操作,setExclusiveOwnerThread()设置当前获得锁状态的线程,然后acquire(1)尝试去获取锁

CAS 方法更新的要求涉及三个变量,currentValue(当前线程的值)expectedValue(期望更新的值)updateValue(更新的值)

if(currentValue == expectedValue){
  currentValue = updateValue
}

Sync还定义了一个nonfairTryAcquire函数,这个函数是专门给NonfairSync使用的,FairSync却没有这种待遇,所以说Sync偏心

以下三个方法都是AQS中的

compareAndSetState();//通过cas操作来修改state状态,表示争抢锁的操作
setExclusiveOwnerThread();//设置当前获得锁状态的线程
acquire();//尝试去获取锁

如果当前线程是这个锁的持有者,那么持有计数将减少。如果保持计数现在为零,则释放锁

由于公平锁和非公平锁都是继承自java.util.concurrent.locks.AbstractQueuedSynchronizer。所以我们不得不先说说AQS,先把AQS搞清楚了上面的ReentrantLock方可继续

acquireAQS中的方法,总共有4个方法

tryAcquire()

tryAcquire进入非公平锁获取锁nonfairTryAcquire方法,对锁状态进行了判断,并没有把锁加入同步队列中

如果既没有排队的线程而且使用 CAS 方法成功的把 0 -> 1 (偏向锁),那么当前线程就会获得偏向锁,记录获取锁的线程为当前线程

acquire 方法会先查看同步状态是否获取成功,如果成功则方法结束返回,也就是 !tryAcquire == false ,若失败则先调用 addWaiter 方法再调用 acquireQueued 方法

上面的代码,逻辑大致为:

  1. 获取当前线程

  2. 获取当前的state,因为state是volatile修饰,所以不用考虑线程的可见性

  3. 判断state==0,表示锁没有被持有,把state设置为1,把锁持有的线程设置成当前线程,然后true则表示可以获取锁

  4. state!=0,则判断当前线程是不是获取锁的线程程

  5. 是当前线程,则state=state+1,然后true表示获取锁成功

非公平锁加锁流程:

🔴小贴士:在ReentrantLock中,它对AbstractQueuedSynchronizerstate状态值定义为线程获取该锁的重入次数,state状态值为0表示当前没有被任何线程持有,state状态值为1表示被其他线程持有,因为支持可重入,如果是持有锁的线程,再次获取同一把锁,直接成功,并且state状态值+1,线程释放锁state状态值-1,同理重入多次锁的线程,需要释放相应的次数

addWaiter()

线程1把锁持有了,把state设置成1,这时线程2来执行nonfairTryAcquire()就会返回false,那么这时候就会执行addWaiter()

public final void acquire(int arg) {
     //如果当前线程尝试获取锁失败并且 加入把当前线程加入了等待队列 
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
       //先中断当前线程
       selfInterrupt();
    }
}

addWaiter(Node.EXCLUSIVE),再执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。下面用一个时序图来看看当前到哪一步了

首先当把当前线程和Node的节点进行封装,Node 节点的类型有两种,EXCLUSIVESHARED,前者为独占模式,后者为共享模式,具体的区别我们会在 AQS 源码讨论

首先会判断tail尾节点是否为null,其实头节点也相当于没有尾节点

  • 如果有尾节点,就会原子性的将当前节点插入同步队列,再执行 enq入队操作,入队操作相当于原子性的把节点插入队列中

  • 如果当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程

主要对上面刚加入队列的Node不断尝试以下两种操作之一。

  • 在前驱节点就是head节点的时候,继续尝试获取锁

  • 将当前线程挂起,使CPU不再调度它

该线程获取资源失败,已经被放入等待队列尾部了,线程下一步进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了

-公平锁

公平锁和非公平锁的主要区别:公平锁会考虑前面有没有线程在等待队列里,就是前面有没有线程先进来,先来先到

公平锁会获取锁时会判断阻塞队列里是否有线程再等待,若有获取锁就会失败,并且会加入阻塞队列

公平策略:

严格按照CLH队列顺序获取锁,线程释放锁时,会唤醒CLH队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH队列的线程参与竞争

为了保证公平,一定会让CLH队列线程竞争成功,如果非CLH队列线程一直占用时间片,那就一直失败(构建成节点插入到CLH队尾,由ASQ模板流程执行),直到时间片轮到CLH队列线程为止,所以公平策略的性能会更差

公平锁代码和nonfairTryAcquire唯一的不同在于增加了hasQueuedPredecessors方法的判断

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

       protected final boolean tryAcquire(int acquires) {
           //获取当前线程
            final Thread current = Thread.currentThread();
            int c = getState();
            //判断当前对象是否被持有
            if (c == 0) {
                //如果等待队列为空 并且使用CAS获取锁成功   否则返回false然后从队列中获取节点
                if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {
                    //把当前线程持有
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
           //若被持有 判断锁是否是当前线程  可重入锁的关键代码
            else if (current == getExclusiveOwnerThread()) {
                //计数加1 返回
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //不是当前线程持有 执行
            return false;
        }
    }

hasQueuedPredecessors判断等待队列是都有其他节点

公平锁加锁流程图

UnLock释放锁

ReentrantLock中的unlock方法了,直接都是从AQS里开始的

此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()

释放锁和锁的公平性就没关系了,继续在ReentrantLock中的unlock方法

private final Sync sync;
public ReentrantLock() {
        sync = new NonfairSync();
}
public void unlock() {
        sync.release(1);
}

然后进入到Release(),这里的release方法是AQS中的release方法,此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()

空方法留给子类去执行

Syn中实现tryRelease()

//释放当前线程占用的锁 releases=1
protected final boolean tryRelease(int releases) {
    //计算state=state-1
    int c = getState() - releases;
    //判断持有锁的线程是不是当前线程
    //不是抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
    boolean free = false;
    //如果state==0证明本次锁释放成功
    if (c == 0) {
        free = true;
        //锁持有线程设置成null
        setExclusiveOwnerThread(null);
    }
    //把state设置成0
    setState(c);
    return free;
}

流程图:

小结:

可重入性实现原理

在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功

由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功

③锁中断

tryLock()

ReentrantLock获取锁的过程是可中断的

对于Synchronized关键字来说,如果一个线程在等待锁,最终只有2种结果:

  1. 要么获取到锁然后继续后面的操作

  2. 要么一直等待,直到其他线程释放锁为止

而ReentrantLock提供了另外一种可能,就是在等的获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求

tryLock()会尝试获取锁,会立即返回,返回值表示是否获取成功

ReetrantLocktryLock(long timeout, TimeUnit unit)提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放,该方法会响应线程的中断

方法tryAcquireNanosAQS中的方法

总结

如果超时时间设置小于等于0,则直接返回获取失败。线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。

很多人可能会问:这里为什么还需要循环呢?

因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试

三、总结

  1. ReentrantLock可以实现公平锁和非公平锁

  2. ReentrantLock默认实现的是非公平锁

  3. ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次

  4. 释放锁的操作必须放在finally中执行

  5. 实例方法tryLock()会尝试获取锁,会立即返回,返回值表示是否获取成功

  6. 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 中,可重入锁是指在同一个线程中,可以重复获取而不会导致死Java 中提供了两种可重入锁的实现:synchronized 和 ReentrantLock。 1. synchronized synchronized 是 Java 中最基本的互斥同步手段,用于保证多个线程访问共享资源的互斥性。synchronized 是可重入锁,当一个线程持有时,可以再次获取该而不会导致死。 例如,下面的代码演示了 synchronized 的可重入特性: ```java public class ReentrantDemo { public synchronized void method1() { System.out.println("method1"); method2(); } public synchronized void method2() { System.out.println("method2"); } public static void main(String[] args) { ReentrantDemo demo = new ReentrantDemo(); demo.method1(); } } ``` 输出结果为: ``` method1 method2 ``` 2. ReentrantLock ReentrantLock 是 Java 提供的另一种可重入锁实现方式,相对于 synchronized 更加灵活。与 synchronized 不同,ReentrantLock 可以手动控制的获取和释放,并且提供了更多的高级特性,例如可中断、公平、多条件变量等。与 synchronized 类似,ReentrantLock 也是可重入锁。 例如,下面的代码演示了 ReentrantLock 的可重入特性: ```java public class ReentrantLockDemo { private final ReentrantLock lock = new ReentrantLock(); public void method1() { lock.lock(); try { System.out.println("method1"); method2(); } finally { lock.unlock(); } } public void method2() { lock.lock(); try { System.out.println("method2"); } finally { lock.unlock(); } } public static void main(String[] args) { ReentrantLockDemo demo = new ReentrantLockDemo(); demo.method1(); } } ``` 输出结果为: ``` method1 method2 ``` 以上就是 Java可重入锁的两种实现方式。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值