Java并发编程总结 [6] 并发包中的并发锁 原理剖析

一、把我能想到的写下来

1.LockSupport 类

    JUC中有个很重要的工具类 LockSupport 类,每个调用这个方法的线程会对应一个许可证。默认情况下,线程是没有这个许可证的。它提供 park() 和 unpark() 方法,在并发包中用于对线程进行阻塞和唤醒。
    park()方法:如果线程有许可证,就直接返回,否则会将线程阻塞挂起。

(补:
因调用 park() 方法而被阻塞的线程被其他线程中断而返回时并不会抛出 InterruptedException 异常。)

    unpark()方法:给线程许可证。如果之前线程因为 park() 方法被挂起,就会返回。

(补:
    当一个线程调用 unpark 时,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有,如果 thread 之前因为调用 park() 方法而被挂起,则调用 unpark() 方法后,该线程被唤醒。如果 thread 之前没有调用 park ,则调用 unpark 方法后,再调用 park() 方法,会立刻返回。)

    park(long nanos)带参数时,是在nanos 时间后,线程会自动唤醒,不再阻塞。

    还有 park(Blocker blocker)方法,通过 jstatck ??? 可以在控制台查看阻塞原因。

(答:
    通过 jstack pid 命令查看线程堆栈。

(改:
    是 park(Object blocker) 。
    使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用 getBlocker (Thread) 方法,来获取 blocker 对象)。

有无实现类???
(答:无。)
    

2.同步队列AQS: AbstractQueueSynchronzier

    这是个抽象类,它的实现类有 ReetrantLock(可重入锁)、ReadWriteLock(读写锁)等。数据结构是双向队列(链表形式,有 prev 和 next ),有内部类 Node和ConditionObject ,Node节点类型的变量 head指向队头, tail 指向队尾 。通过访问资源是独占的或是共享的,节点可分为 独占式 : exclusive 和 共享式 : shared ,同步队列中的节点类型有 :cancelled 已取消的、condition 在条件队列中等待的、waiting 需要被唤醒的。
    类图:
在这里插入图片描述

(改:
    没有”WAITING“的状态,
    应该是 ”SIGNAL“线程需要被唤醒,和 ”PROPAGATE“释放共享资源时需要通知其他节点。

在这里插入图片描述

    AQS 类中维护的单一的状态变量 state 变量十分重要,(是不是用来保证同步???)

(答: √。
    对于 AQS 来说,线程同步的关键是对状态值 state 进行操作。
获取/释放锁都是提供设置 state 的值。)

    在不同子类中意义不同,比如在 独占锁 ReentrantLock 中,如果当前资源没有被任何线程持有,第一个线程访问资源,会标记这个线程,并且将 state 由 0 置为 1 ,如果这个线程还访问同步块,那么就会给 state +1 。

(补:
    对于读写锁 ReentReadWriteLock 来说,state 的高 16 位表示 读状态,即获取读锁的次数,低 16 位表示写状态。
    对于 semaphore 来说,state 表示当前可用信号的个数。
    对于 CountDownlatch 来说, state 表示计数器当前的值。)

    如果用的是独占性资源,调用 acquire() 方法,首先会去调用 tryAcquire() 方法,修改 state 值,如果成功 ,就返回资源,否则,就生成一个 WAITING 类型的节点,加到同步队列队尾,并调用 LockSupport类的 park()方法将自己挂起。

(改:否则,将当前线程封装为类型为 Node.EXCLUSIVE 的 Node 节点后插入到 AQS 阻塞队列的尾部。)

    如果是共享性资源,调用的是acuqireShared() 方法。

(Node.SHARE)

    如果用的是独占性资源,调用 release()方法,首先会释放锁(???),那么这时需要个线程来用资源,所以会调用 tryAcquire() 方法,从同步队列【需要被唤醒的节点们】中出队一个节点,调用 LockSupport 类的 unpark() 方法,使该线程拥有许可证。

(答:×。
    当一个线程调用 release(int arg)方法时会尝试使用 tryRealse释放资源,这里是设置 state 的值,然后调用 LockSupport.unpark(thread)方法激活 AQS队列里的被阻塞的一个线程(thread)。被激活的线程则使用 tryAquire 尝试,看当前状态变量 state 的值是否满足自己的需要,满足则激活线程,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。 )

    子类需要覆写 tryAcquire 方法。

2.ConditionObject条件变量

    LockSupport 类的内部类 ConditionObject 是用来实现同步的,通过 AQS.Condition() ,可以产生一个 ConditionObject 对象,一个 AQS 对象可以对应很多个 ConditionObject 对象,而且 每个 ConditionObject 都对应一个 等待队列,是单向的,节点:Node 类型,由 firstWaier 和 lastWaiter 分别表示 队头、队尾 。

(改:
    lock.newCondition() 其实是 new 了一个在 AQS 内部声明的 C0nditionObject 对象。C0nditionObject 是 AQS的内部类。

(补:
    ConditionObject 是条件变量,每个条件变量对应一个条件队列,用来存放 调用条件变量的 await 方法后被阻塞的线程。当线程调用 await() 方法,(之前必须先调用锁的 lock() 方法)会在内部构造一个类型为 NODE.CONDITION 的 node 节点,将该节点插入到条件队列末尾,之后当前线程会释放获取锁(也就是会操作锁对应的 state变量的 值)并被阻塞挂起。

    lock() + await() + signal() / signalAll() + unlock() 就相当于 之前 synchronized + Object类的 wait() + notify/ notify 。 调用lock() 相当于进入同步块, await() 相当于阻塞线程,在另一个线程中可以通过 signal() 唤醒线程。

二、补充

1.独占方式和共享方式

    根据 state 是否属于一个线程,操作 state 的方式分为 独占方式 和 共享方式。
    使用独占方式获取的资源是与具体线程绑定的,就是说 如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作 state 就会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。比如,独占锁 ReentrantLock 的实现,当一个线程获取了 ReentrantLock 的锁后,在 AQS 内部会首先使用 CAS 操作把 state 的值从 0 变为1 然后设置当前锁的持有者为当前线程,当该线程再一次获取锁时发现它就是锁的持有者,则会把状态值从 1 变为 2,也就是设置可重入数,而当另一个线程获取锁时,发现自己并不是该锁的持有者就会被放入 AQS 阻塞队列后挂起。
    对应共享方式的资源 与 具体线程是不相关的,当多个线程去请求资源时通过 CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再去获取时如果当前资源还能满足它的需要,则当前线程只需要使用 CAS 方式进行获取许可。比如 Semaphore 信号量,当一个线程通过 acquire() 方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋 CAS 获取信号量。

2.如何维护 AQS 提供的队列

    入队操作:当一个线程获取锁失败后,该线程会被转换为 Node 节点,然后会使用 enq 方法将该节点插入到 AQS 的阻塞队列中:

  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;
                }
            }
        }
    }

3.ConditionObject 类的 await() 和 signal() 方法

    Object在调用 wait() 和 notify() 之前需要先获取资源对象的监视器锁,ConditionObject 在调用 await() 和 signal() 方法前需要先调用锁的 lock() 方法获取锁。
    当线程调用 await() 方法,会在内部构造一个类型为 NODE.CONDITION 的 node 节点,将该节点插入到条件队列末尾,之后当前线程会释放获取的锁(也就是会操作锁对应的 state变量的 值)并被阻塞挂起。来看 await() 源码:

 public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();

//创建新的 node节点,并插入到条件队列末尾
            Node node = addConditionWaiter();

//释放当前线程获取的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
            
//调用 park 方法把自己阻塞挂起
                LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
if (acquireQueued(node, savedState) &&
 interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) 
            // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

    调用条件变量的 signal() 方法,在内部会把条件队列里面等待时间最长的线程节点(也就是头节点)移除并放入 AQS 同步队列里,然后激活这个线程,使之有机会获得锁。来看 signal() 方法源码:

  public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)

//将sate设置为0:初始状态
                doSignal(first);
        }

4.生产者-消费者模型

    通过容器解决生产者与消费者的强耦合问题,生产者和消费者之间不直接通信,而通过阻塞队列来进行通讯。
(后续补上 p133)

三、总结

(1)LockSupport 工具类用于挂起和唤醒线程。
(2)AQS是个抽象类,同步是通过状态变量 state 的值(获得锁/释放锁都是改变 state 的值)。
    获取锁失败(即设置 state值失败)的线程会被放入 AQS 的阻塞队列中。
AQS的内部类 ConditionObject 是条件变量,内部维护了一个条件队列,调用 await() 方法就会把线程放入条件队列中。
    一个锁对应一个 AQS 阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值