ReentrantLock 底层原理

目录

一、ReentrantLock入门

二、AQS原理

1、AQS介绍

2、自定义锁

三、ReentrantLock实现原理

1、非公平锁的实现

加锁流程

释放锁流程

2、可重入原理

3、可打断原理

4、公平锁原理

5、条件变量原理

await流程

signal流程


一、ReentrantLock入门

相对于synchronized它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与synchronized一样,都支持可重入

基本语法

//获取锁
reetrantLock.lock();
try{
    //临界区
}finally{
    //释放锁
    reentrantLock.unlock();
}

可重入

可重入指一个线程如果首次获取这把锁,他就是这把锁的拥有者,有权再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可打断

如果用的lock.lockInterruptibly()这个方法上锁的话,别的线程是可以通过interrupt方法打断的。比如:我的a线程正在尝试获取锁,但这个lock已经被b线程拿了,b可以如果执行Interrupt就可以把a线程正在尝试的打断直接获取失败不等待。就是一种防止无限制等待的机制,避免死等,减少死锁的产生

锁超时

有个lock.tryLock()方法,返回boolean,获取到就返回true,获取不到锁就返回false,这个方法的可以传入两个参数超时时间,第一个参数数字代表时间,第二个是单位。代表他去tryLock()尝试获取锁的时候最多等待的实践,如果是1和秒就是最多尝试等待一秒,还没拿到就返回false。也是一直超时机制,防止死锁。

公平锁

syn就是非公平的,重量级的monitor的堵塞队列就是非公平的。

ReentrantLock默认是不公平,但是我们可以通过构造方法改成公平的,传的是boolean值

条件变量

syn不满足条件的线程都在一个休息室

而ReentranLock支持多休息室,唤醒的时候也是按照休息室来唤醒

使用流程:

  • await进行等待(前需要获得锁)
  • await执行后,会释放锁,进入conditionObject等待
  • await的线程被唤醒(或打断或超时)去重新竞争lock锁
  • 竞争lock锁成功后,从await后继续执行

在new完ReentranLock之后可以用newCondition()方法创建休息室,然后就可以用new出来的condition调用await方法进入休息室等待,唤醒的话是signal()方法

二、AQS原理

1、AQS介绍

Abstract Queued Synchronizer,抽象队列同步器,是阻塞式锁和相关的同步器工具框架,主要是继承他,然后重用他的功能

特点:

  • state属性用来表示资源的状态(分独占模式和共享模型)子类需要定义如何维护这个状态,控制如何获取锁和释放锁
    • getState 获取状态
    • setState 设置状态
    • compareAndSetState -cas机制设置state状态(cas防止多个线程修改state时线程安全)
    • 独占模式是只有一个线程能够访问资源,共享模式允许多个线程访问资源
  • 提供了基于FIFO的等待队列,类似于Monitor的EntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似与monitor的waitSet

获取锁:tryAcquire(arg)返回boolean,aqs里面暂停和恢复用park和unpark

释放锁:tryRelease(arg)返回boolean

2、自定义锁

自定义的锁的方法基本上都是通过AQS来进行实现的

  • tryAcquire给state进行修改状态,这次使用的是独享锁,给AQS对象锁设置owner
  • 然后就是tryRelease,主要就是设置state为0,解锁,释放owner
  • isHeldExcusively:判断是不是有对象在占用锁
  • newCondition实际上还是AQS里面的ConditionObject对象,也就是条件变量的创建

最后就是实现锁的方法,基本上都是间接调用同步器的方法来执行

class MyLock implements Lock {

    //先实现同步器,实现AQS抽象类,他已经帮我们写了大多数方法,我们只需要自定义4个方法
    class MySync extends AbstractQueuedSynchronizer{

        @Override
        protected boolean tryAcquire(int arg) {
             //尝试加锁
             if(compareAndSetState(0,1)){
                 //这个用的是CAS的无锁机制加锁防止多线程安全问题
                 setExclusiveOwnerThread(Thread.currentThread());
                 return true;
             }
             return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            //尝试释放锁
            setExclusiveOwnerThread(null);
            //这个state是volatile修饰,防止指令重排
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            //锁是不是被线程持有
            return getState()==1;

        }

        public Condition newCondition(){
            return new ConditionObject();
        }
    }

    private MySync sync=new MySync();

    @Override//加锁
    public void lock() {
        sync.acquire(1);
    }

    @Override//加锁,可以被中断
    public void lockInterruptibly() throws InterruptedException {
           sync.acquireInterruptibly(1);
    }

    @Override//尝试加锁,不成功就放弃
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    //尝试加锁,超时就进入队列
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        long waitTime = unit.toNanos(time);
        return sync.tryAcquireNanos(1,waitTime);
    }

    @Override
    public void unlock() {
        //他这个释放锁,调用的是release方法,这个方法还会唤醒堵塞队列中的一个线程
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
//        sync.newCondition();
        return sync.newCondition();
    }
}

三、ReentrantLock实现原理

ReentrantLock 底层原理_第1张图片

他底层也是有个抽象Sync类继承了AQS,他有两个实现NonfairSync非公平锁FairSync公平锁

1、非公平锁的实现

加锁流程

他先用cas的compareAndSetState尝试加锁,当没有竞争时,如果上锁成功就setExclusiveOwnerThread改成当前线程

ReentrantLock 底层原理_第2张图片

第一个竞争出现时,他会自己tryAcquire重试一次,如果成功就加锁成功。

ReentrantLock 底层原理_第3张图片

如果还是加锁失败就会构建node队列,head指向的是第一个节点称为dummy哑元或哨兵,是占位的,并不关联线程

ReentrantLock 底层原理_第4张图片

ReentrantLock 底层原理_第5张图片

然后回进入一个死循环汇总不断尝试获取锁,失败后进入park阻塞

如果自己是紧邻head的第二位,那么可以再次tryAcquire尝试获取锁,当仍然state为1,失败

将前驱的node,即head的waitStatus改为-1(表示要堵塞了,head是status-1就可以后面有责任唤醒它),这次返回false

这个时候还会再循环一遍,进入方法因为前驱节点为-1了就回返回true,然后就会调用park方法堵塞当前线程

ReentrantLock 底层原理_第6张图片

当有多个节点都park堵塞的时候就会一个个进入,然后每个state上面的小三角都是-1表示前驱节点有责唤醒它

ReentrantLock 底层原理_第7张图片

释放锁流程

会tryRelease,如果成功直接设置exclusiveOwnerThread为null,然后state为0

当前队列不为null时,并且head的waitStatus为-1,他就会找到队列中里head最近的node,unpark恢复他的运行,然后就会执行上面加锁的方法。

但是这个是非公平锁,假如这个时候突然来了个不在队列里面的线程4,就会跟刚刚唤醒的线程竞争,如果4先拿到锁设置为exclusiveOwnerThread,那么1将会重新park进入阻塞

ReentrantLock 底层原理_第8张图片

2、可重入原理

以非公平锁为例,获取锁会调用nonfairTryAcquire

他会先判断state状态,如果为0说明没有加锁,直接加锁

如果不为0说明上锁了就会判断当前线程是不是和exclusiveOwnerThread里面的一样,如果一样的话就说明是重入了,直接s 

释放的时候就会调用tryRelease方法,调用完就会state--,如果当减到0的时候,就会让锁释放掉,设置exclusiveOwnerThread为null,让state为0

3、可打断原理

不可打断模式

默认情况是不可打断的,线程在没变法立刻获得锁的时候,就会不断循环尝试获取锁,仍然不成功会进入park,进入park是可以被其他线程被唤醒。有线程来打断他,打断标记默认值是false,一旦被打断就会色设置为true,但是他就是改个这个标记,然后又重新进入循环park了。就是唤醒了但是没有拿到锁还是继续阻塞,只是有一个true标记

不可打断模式下,即使被打断,仍会驻留在AQS队列里面,等获得锁后才能继续执行,在获得锁以后才知道其他线程打断我了(打断标记被设置为true)

可打断模式

当调用doAcquireInterruptibly,也是去获取锁,然后park进入队列,这个时候别的线程唤醒它继续执行,直接是抛出InterruptedException异常就直接不用循环等待了,实现了可打断

4、公平锁原理

非公平锁是nonfairTryAcquire去获取锁,获取锁的时候如果为state为0说明没有人拿锁,他会直接去抢锁,没有任何判断

公平锁会多个判断条件,如果堵塞队列还有线程就不会去拿锁

5、条件变量原理

await流程

  • 首先就是把线程放入到对应的await的condition队列(相当于就是休息室)
  • 就是清空锁,防止可重入和获取了别的锁。然后唤醒Sync的队列的线程
  • 然后就是进入condition阻塞

总结:进入condition队列,清空锁并且唤醒线程,最后就是使用park进行阻塞。

ReentrantLock 底层原理_第9张图片

signal流程

  • 首先检查线程是不是获取了锁,然后就是获取队列的头结点
  • 接着就是调用doSignal唤醒first加入到Sync的队列(加入到Sync才能够进入到owner竞争锁执行),类似wait之后进入休息室,唤醒后还是要进入队列竞争
  • 接着就是获取下一个节点(也就是真正的条件队列节点),获取如果是null,那么最后的节点设置为空
  • 如果不为空,节点被取出来并且next节点设置为空(已经保存到firstWaiter中)
  • if ( (firstWaiter = first.nextWaiter) == null)
  • lastWaiter = null;
  • first.nextWaiter = null;
  • 首先需要知道这是一个单向链表,而且有指向的队首和指向队尾的节点,可以看下面的addConditionWaiter方法,很明显每次都是直接通过队尾下一个节点指向新节点,然后队尾=新节点。这样子的移动。那么这里的first.nextWaiter就是断开first而已,并没有把firstWaiter设置为null。只是指针没有指向下一个节点。每次相当于就是把firstWaiter队首往后面移动,然后把first节点弄到Sync队列上面去等待。
  • 接着就是到把first节点转移通过方法transferForSignal,并且把节点的状态设置为0
  • Node p = enq(node);最后就是把节点拼接上Sync队列,并且返回前驱节点
  • 修改前驱节点状态-1。结束了signal唤醒

总结:signal完成了条件队列清除(单项链表清除),然后就是把对应的节点全部送去Sync队列。如果失败可能就是队列满了或者是超时了。最后就是取出前驱节点修改状态。

ReentrantLock 底层原理_第10张图片

你可能感兴趣的:(JUC,java,juc,并发编程,aqs,锁)