java并发-AQS总结-原理

AQS是Java多线程编程的重入锁,管程,工具类的基础类,是必须要掌握的。不掌握这个类,根本不能称之为合格的Java程序员。

即使是把这个类所有的代码都背会,也是值得的。

如何标识已经有线程在执行呢?

有两个变量,一个state变量,一个exclusiveOwnerThread变量,为什么需要两个变量呢?用一个exclusiveOwnerThread变量不也可以吗?

state变量用来支持重入,exclusiveOwnerThread变量用来支持互斥。

state变量标识是否已经有线程获得锁。但是这里为什么只是用cas尝试设置一次呢?如下代码所示:

复制代码 static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

复制代码 如果说这里使用CAS尝试修改无限循环的设置,那么能够就能简单的实现锁了呢?如下代码所示:

final void lock() { int state = getState(); while (!compareAndSetState(state, 1)){

        }
    }        

这里使用CAS设置state成员变量明显不行,那能够设置当前线程呢?这里是不行的,CAS只能保证多个线程并发修改数据的线程安全特性,只有一个线程修改成功,但无法保证这类数据被一个线程锁占用时,另一个线程的CAS操作会失败的情况!!!所以,这里只能尝试设置一次状态,这里判断的用意是看当前线程执行到这里,有没有其他的线程已经设置过状态,若设置过,那么就只能加入到队列,否则,自己就直接加锁不就得了。

acquire方法为何最后要自我打断?

分析情况应该分多钟情况,这样比较好分析。

情景1:节点类型

1.节点全部都是独占模式的情景模拟

2.节点全部都是共享模式的情景模拟

3.节点既有独占模式,又有共享模式的情景模拟。

情景2:

1.当有一个线程加入队列时,此时持有锁的线程释放。。。

2.当有2个线程加入队列时,此时持有锁的线程释放。。。

为什么会同时存在addWaiter方法和enq方法呢?只使用addWatier方法不就行了吗?

addWatier方法适用于只有一个线程加入队列,此时没有竞争,很快就能加入队列。

enq方法同时考虑了两种情况:

1.多个线程都要加入队列

此时为了提高效率,先尝试一次CAS设置,至此有一个线程成功设置,让其他线程后进入enq方法,让所有要加入队列的线程自旋CAS加入队列。

2.队列为空

此时需要初始化一个头结点,而且,为了只能设置一个头结点,运用CAS来操作。

复制代码 private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } 复制代码 这里比较经典,只有一个线程会走Must initialize,而且只会发生一次!!!然后,大家都会走下面的分支来依次添加到队尾。思考,这个Head什么都没有,如何处置他呢?

我感觉下面会竞争执行权。

结点状态深入理解!!!结点有好几种状态,必须理解一下。

节点状态有:

1.被取消

-1 等待通知

-2 等待条件

-3 ???

什么时候会出现被取消的节点呢?

当调用tryLock(TimeUinit)这样的方法时,如果尝试获得锁失败,则会将节点变成取消状态。

什么时候会出现取消的节点呢?

1.当超过等待时间依然没有获得锁,则退出方法时,会设置成取消状态

2.当等待过程过,出现打断异常,则退出方法时,会设置取消状态

为什么会保留这样的取消的节点呢?

因为如果要线程自己移除这个取消的节点,还是要保证线程安全,花费的时间是不确定的,所以,不如简单的设置一下已取消状态,让其他的线程帮自己移除,而自己尽快返回,不浪费时间。

使用AQS组件的锁的线程什么时候会自旋?

感觉没有地方会自旋。

为什么要用双向链表存储被阻塞的线程,而不是使用单链表?

1.节省寻找前驱节点的时间

为何ConditionObject是使用单链表,而不是双向链表呢?

够用就好

ReentantLock是否是公平锁?如果是,如何实现公平锁的?

通过一个boolean类型参数的构造函数来决定是否公平,公平锁保证等待的所有线程都有平等的机会获执行权,实现方式就是有一个线程获得锁之后,在这个线程没有获得锁之前,其他线程一直自旋等待加入队列,公平锁比较浪费CPU计算资源。此时等待队列就相当于退化到了只有一个节点的队列。。。。

非公平锁,是先来先服务的锁,谁先能够加入的等待队列,则谁就先被通知执行,非公平锁是阻塞锁,相较于公平锁,节省CPU计算资源。

针对这个类的每个方法,都仔细思考思考,切实理解每个方法的实现思路和原因?为什么要这样做比这个方法做了什么更重要。

每个方法的原因###############

释放互斥锁的操作

release方法,内部调用tryRelease方法和unpakcSuccessor(唤醒后继节点)方法。

tryRelease方法返回true说明完全释放了锁,所以其他等待的线程将会调用acquire方法获得锁。(其实是正在等待和正在调用acquire方法的线程)

unpackSuccessor方法唤醒后继节点,这里比较的有意思。

这个方法先找到第一个,是从双链表的尾部项头部遍历,找到哨兵节点的后继节点,然后unpack操作。

思考,谁把当前的这个节点从链表当中移除的呢?因为此时unpack操作,一定唤醒的是在睡眠的线程,所以,应该是要看做pack操作的后面的代码,因为此时会从这里被唤醒然后继续执行!!!

从acquireQueued方法可以看出,是线程自己把自己设置为头结点???哨兵节点不是头结点吗?

自己设置为头结点之后,就放弃了成为等待节点的能力,因为setHead方法会设置必要的字段为空,这样就成为哨兵节点了。此时,是放弃了之前的哨兵节点了!!!

获得互斥锁的操作

可中断的获得互斥锁的操作

超时获得互斥锁的操作

尝试一次获得互斥锁的操作

注意:哨兵节点的加入,其实也是惰性的,只有当有线程需要加入阻塞队列的时候才会创建哨兵节点,

哨兵节点来自3个操作:

1.第一个要加入阻塞队列的线程追加的

2.当获得互斥锁的线程要释放互斥锁,那么就会unpack后继节点,后继节点线程醒来,就会设置自己所在的节点为哨兵节点,那么,此时,哨兵节点的status字段就标识了当前线程之前所处的状态了。

3.释放共享锁,跟释放互斥锁类似。

#######################################################################################

释放共享锁的操作

可中断获得共享锁的操作

获得共享锁的操作

超时获得共享锁的操作

尝试一次获得互斥锁的操作

##########condition对象

为什么condition对象的wait和single类方法使用之前需要加锁,使用之后需要解锁?

因为这类方法都不是线程安全的方法,需要借助加锁来保证线程安全,保证自己追加Waiter能够线程安全。

包括增加等待节点,取消等待节点。

这里不是线程安全的方法,正好可以借助ReentantLock来保证线程安全。这样做,简化了Condition接口实现类的写法,比较容易理解。

conditionObject是condition的实现类,作为一个内部类,用意是能够访问外部类对象AQS的一些属性。

condition对象的本质依然是对等待对列的操作,两种操作,入队,出队,提供等待和通知两种比较实用的方法的工具组件。

可以实现管程。

condition对象的singleAll是一个一个通知的。先来先通知。

condition构建自己的等待队列不久OK了吗?为何还要让描述自己的节点加入AQS的同步队列呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章