java并发编程学习八——ReentrantLock原理

一、AQS

AQS全称AbstractQueuedSynchronizer,意思是抽象队列同步器。ReentrantLock包含一个成员变量Snyc,ReentrantLock大部分功能依赖Snyc,它继承至AbstractQueuedSynchronizer。必须先弄清楚了AbstractQueuedSynchronizer才能理解ReentrantLock的原理。

1.1 AQS基本原理

AbstractQueuedSynchronizer是阻塞式锁和相关同步工具的框架,本篇主要讨论ReentrantLock,这里就简单介绍AQS基本原理。
AQS特点:

  • 使用state属性来表示共享资源的状态(分为共享式和独占式),子类需要定义如何维护这个状态,控制如何释放锁和获取锁
    • getState 获取state状态
    • setState 设置state状态
    • compareAndSetState 乐观锁机制设置state状态
    • 独占模式只有一个线程能访问共享资源,共享模式可以多个线程访问共享资源
  • 提供了基于FIFO的等待队列,类似于Monitor的EntryList
  • 条件变量来实现等待唤醒机制,类似于Monitor的WaitSet

AQS子类实现方法:

  • tryAcquire获取锁
//如果获取失败
if(!tryAcquire(arg)){
	//进入队列等待  park
}
  • tryRelease释放锁
//如果释放锁成功
if(tryRelease()){
	//唤醒其他线程 unpark
}
  • tryAcquireShare 共享方式获取锁
  • tryReleaseShare 共享方式释放锁
  • isHeldExclusively 是否持有独占锁

1.2 自定义锁

基于AQS自定义实现一个不可重入的独占锁

public class MyLock implements Lock {

    public static void main(String[] args) {
        MyLock lock = new MyLock();
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        },"t1").start();

        new Thread(() -> {
            lock.lock();
            try {
                //lock.lock();再次加锁,当前线程被阻塞
                System.out.println(Thread.currentThread().getName());
            } finally {
                lock.unlock();
            }
        },"t2").start();
    }

    /**
     * 独占锁,不可重入
     */
    class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                //上锁之后,设置锁的所有者为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                System.out.println("成功加锁");
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            setExclusiveOwnerThread(null);
            setState(0);
            //假如先设置状态再设置锁的所有者为空,可能会导致,其他线程获取到锁的一瞬间,所有者还未改变,产生不可预知问题
            //而state变量由volatile修饰,执行顺序不会改变,先设置所有者为空,就不会产生上述问题
            //setExclusiveOwnerThread(null);
            System.out.println("已解锁");
            return true;
        }

        //锁是否被持有
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

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

    private Sync sync = new Sync();

    /**
     * 加锁,不成功的进入等待队列
     */
    @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 {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        //解锁,唤醒正在阻塞的线程
        sync.release(0);
    }

    /**
     * 创建条件变量
     */
    @Override
    public Condition newCondition() {
        return sync.newCcondition();
    }
    
}

二、ReentrantLock

2.1 加锁流程

调用lock.lock()进入lock方法
在这里插入图片描述
进入同步器的lock方法
在这里插入图片描述
NonfairSync是非公平锁实现,可见,当前线程直接去修改state状态,即抢锁。如果此时有其他线程在队列(先进先出,双向链表实现)里面等待,对这些线程来说是不公平的,因此叫非公平锁。加锁成功就结束了,下面看下加锁失败的情况

2.1.1 加锁失败

加锁失败进入acquire(1),看下面代码的执行逻辑
在这里插入图片描述

  • 再次尝试加锁成功,就不再往下执行

  • 再次尝试加锁失败,先将线程加入队列尾部等待,再进入acquireQueued

  • 在这里插入图片描述

  • 加入队列失败,就将当前线程打断

2.2 释放锁流程

调用lock.unlock()
在这里插入图片描述
进入sync.release(1);
在这里插入图片描述
release在AbstractQueuedSynchronizer里面,说明公平与非公平的实现实际上是一样的。解锁流程

  • tryRelease尝试释放锁,会判断当前线程是否是锁持有者,不是就报错
  • 释放锁成功,唤醒头结点的下一个节点,头结点只记录了状态,线程为null,只负责找到下一未取消的节点唤醒
  • 调用unpark恢复线程,从阻塞的 parkAndCheckInterrupt())方法里面继续执行
  • 被唤醒的线程就再次进入循环尝试获取锁
  • 成功获取锁之后,将被唤醒线程的节点指定为头节点,并将节点线程改为null
  • 释放锁失败,返回false,之后由锁的实现类去处理,ReentrantLock没有对返回值进行处理。

2.2.1 可重入原理

从加锁看,再次重入,直接将状态+1

     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");
                //状态加1 
                setState(nextc);
                return true;
            }
            return false;
        }

从解锁看,再次重入,将状态减1,减到0说明解锁完成,再去唤醒队列中等待的线程

  public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
            	//解锁完成,并且都节点不为null,等待状态不为0,唤醒正在等待的线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

进入tryRelease

 protected final boolean tryRelease(int releases) {
 			//状态减1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
            //减为0则返回true,代表解锁完成
                free = true;
                setExclusiveOwnerThread(null);
            }
            //状态不为0,将减小之后的值赋值回去,知道状态减为0 
            setState(c);
            return free;
        }

2.3 条件变量原理

线程在获取到锁之后,可以进入条件对象Condition等待,具体使用请看ReentrantLock使用,下面分析下源码

2.3.1 await

在这里插入图片描述
addConditionWaiter的作用是将当前线程放入等待队列,该等待队列被Condition对象持有。因此,每个条件对象都有自己的等待队列,每个线程可以进入不同的条件等待。
进入fullyRelease
在这里插入图片描述
在调用await之后会释放锁,这块释放锁release不是1,是因为可能发生了锁重入,要一次性将多层锁全部释放。如果此时线程被打断,释放锁失败就会抛出异常,节点状态修改成取消等待。返回之后通过 LockSupport.park(this);进入等待

2.3.2 signal

再来看看唤醒流程
在这里插入图片描述
等待队列的队头有节点,进行唤醒,进入doSignal
在这里插入图片描述

  • (firstWaiter = first.nextWaiter) == null; firstWaiter指向了下一个节点,如果只有一个节点,则firstWaiter ==null为真
  • firstWaiter ==null为真 会进入if块,将lastWaiter赋值为null
  • 之后再执行transferForSignal(first),将线程唤醒,唤醒成功循环结束
  • 如果线程已被打断导致唤醒失败,先将firstWaiter(它指向下一个节点)赋值给first,再判断firstWaiter 是否为空
  • firstWaiter 为空说明没有下一个节点需要唤醒,结束循环
  • firstWaiter 不为空,就继续循环,唤醒first,它已经指向上一次循环中first的下一节点

在transferForSignal方法里面也是调用LockSupport.unpark(node.thread);将线程唤醒

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值