Java多线程3:Lock锁--AQS,ReentrantLock

27 篇文章 0 订阅

Lock接口是JDK1.5以后新增的,需要显式加锁,拥有了获取和释放锁的可操作性,可中断地获取锁,可超时获取锁等synchronized关键字所不具备的同步特性。
通过子类进行实例化
Lock的使用:

Lock lock=new ReentrantLock();
        lock.lock();
        try{
            
        }finally {
            lock.unlock();
        }

synchronized同步块执行完成或者遇到异常时会自动释放锁,而lock必须要调用unLock方法来释放锁,放在finally块中,保证一定会解锁。
ReentrantLock 可重入锁:查看源码可以发现所有的方法都调用了静态内部类Sync中的方法,Sync继承了AbstractQueuedSynchronizer(AQS–同步器)

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        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");
                setState(nextc);
                return true;
            }
            return false;
        }

ReentrantLock支持重入性,支持对共享资源重复加锁,也就是当前线程获取该锁再次获取不会被阻塞。在Java中关键字synchronized隐式支持可重入,synchronized是通过获取自增释放自减的方式实现重入。ReentrantLock支持公平锁和非公平锁两种方式。
重入性的实现原理:
1.在线程获取锁的过程中,如果获取锁的线程是当前持有锁的线程则直接再次获取成功;
2.由于支持可重入,就可能进行n次加锁,那么只有锁只有在被释放n次之后才算是释放成功。

ReentrantLock支持公平锁和非公平锁,构造方法传入true时为公平锁,false时非公平锁,默认为非公平锁,保证系统更大吞吐量

AQS是一个抽象类,实现了序列化接口,AQS是用来构建锁和其他同步组件的基础框架,依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成的等待队列。它的子类必须重写AQS的几个用protected修饰的用来改变同步状态的几个方法(【状态的更新】如getState、SetState和compareAndSetState),其他方法主要实现了排队和阻塞机制。

AQS本身没有实现任何同步接口,仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以实现不同类型的同步组件。在锁的实现中聚合同步器,利用同步器实现锁的语义。锁是面向使用者,定义了使用者与锁交互的接口,封装了实现细节;而同步器是面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态的管理、线程阻塞等待唤醒的一些底层操作。

AQS的模板方法设计模式:将一些方法开放给子类进行重写,而同步器给同步组件所提供的模板方法又会重新调用被子类所重写的方法。
Acquire方法

public final void acquire(int arg) {
//再次使用CAS尝试,成功返回,并将当前线程置为持有锁线程
//如果失败就调用addWaiter方法将当前线程封装为节点入队(尾插)进入同步队列
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

此时当继承AQS的NonfairSync调用模板方法时就会调用已经覆写过的tryAcquire方法。
AQS中模板方法源码分析
AQS提供的模板方法可以分为三类:

  1. 独占式获取与释放锁
  2. 共享式获取和释放锁
  3. 查询同步队列中等待线程的情情况
    同步组件通过AQS提供的模板方法实现自己的同步语义
    新建一个同步组件:1)实现时推荐定义继承AQS的静态内部类,并且覆写需要的protected修饰的方法;2)同步组件语义依赖于AQS的模板方法,而AQS的模板方法依赖于被AQS子类所重写的方法。

AQS中的同步队列是使用链表来实现的(双向队列),通过头尾指针来管理同步队列;静态内部类Node,是我们同步队列的每个具体节点,在这个类中的属性有:
volatile int waitStatus;节点的状态
节点的状态有:0—初试状态;1—当前节点从同步队列中取消;-1—后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继结点继续运行;-2—节点在等待队列中,节点线程在Condition上,其他线程对Condition调用了signal()方法后,该节点会从等待队列中转移到同步队列中,加入到对同步状态的获取;-3----下一次共享式同步状态获取将会无条件传播下去。

调用lock()方法是获取独占锁,获取失败就将当前线程加入同步队列,成功则线程执行。
入队:addWaiter(Node mode)将当前线程封装为节点,当前尾节点不为空,将当前线程尾插入同步队列中,为空或者CAS失败就进入enq()方法,在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法完成链式队列的头结点的初始化;自旋不断尝试CAS尾插入节点直到成功为止。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋
            for (;;) {
            //获取当前节点的前驱节点
                final Node p = node.predecessor();
                //当前节点能否获取独占式锁
                //当前节点的前驱节点是否是头结点
                if (p == head && tryAcquire(arg)) {
                //队列头指针指向当前节点
                    setHead(node);
                    //释放前驱节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //获取同步状态失败,线程进入等待状态等待获取独占锁
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

获取锁成功进行出队操作:队列头节点的引用指向当前节点,释放前驱节点;无任何引用方便GC时进行回收;获取失败就会调用shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法:使用CAS将节点状态由INITIAL设置为SIGNAL,表示当前线程阻塞。shouldParkAfterFailedAcquire()返回true时才执行parkAndCheckInterrupt(),该方法调用LookSupport.park()方法。
acquireQueued()在自旋中完成了:1.如果当前节点的前驱节点是头结点并且当前节点能够获得同步状态的话,当前线程能获取到锁该方法执行结束退出;2.获取锁失败的话就调用LookSupport.park()方法阻塞当前线程。

独占锁的释放release()方法调用AQS中的release方法;同步状态释放成功(tryRelease返回true);每一次锁释放后就会唤醒同步队列中该节点的手机节点所引用的线程,从而进一步可以证明获得锁的过程是一个FIFO的过程。

总结:
1.线程获取锁失败,被封装为Node节点进行尾插入队操作,核心方法在于addWaiter和enq方法,同时enq完成对同步队列的头结点初始化工作以及CAS操作失败的重试。
2.线程获取锁的过程是一个自旋的过程,当且仅当当前线程的前驱节点是头结点并且成功获得同步状态时,节点出队(也就是当前线程成功获取到锁),失败则调用LookSupport.park()方法使得线程阻塞。
3.释放锁的时候会唤醒后集结点

独占式锁特性:可中断式获取锁;使用lock.lockInterruptibly();该方法底层会调用AQS的acquireInterruptibly方法,parkAndCheakInterrupt返回true时即线程阻塞时该线程被中断,抛出中断异常。
超时等待获取锁:tryAcquireNanos()方法通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,返回:超时时间内成功获取到锁;当前线程在超时时间内被中断;超时时间结束,仍未获得锁返回false
会在获取锁失败之后,计算出按照现在时间和超时时间计算出理论截止的时间,如果超时,返回false,如果还没有超时就会执行LookSupport.partNanos使得当前线程阻塞,然后对中断检测,若检测出被中断直接抛出中断异常。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值