仅以此记录学习的过程,有些是借鉴的,有错误望指出!
Lock接口锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序依靠synchronized关键字实现锁功能,而Java5之后,并发包(java.concurrent.util)新增了Lock接口(及相关实现类)来实现锁功能。它提供了与synchronized类似的同步功能,只是是显示的获取和释放锁(而synchronized是隐式的获取和释放锁)。也拥有了更多的synchronized不具备的同步特性,如锁的获取与释放可以多个操作、可中断的获取锁、超时的获取锁等。
synchronized是隐式的获取和释放锁,简化了同步的管理,但扩展性比较差,因为它固化了锁的获取和释放,必须先获取再释放。
如:
Lock lock = new ReentrantLock(); lock.lock(); try{ }finally{ lock.unlock(); }
特性 |
描述 |
|
尝试非阻塞地获取锁 |
当现场尝试获取锁时,如果这一时刻锁没有其他线程占有,则成功获取并持有锁 |
|
能被中断地获取锁 |
与synchronized不同的是,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,且同时会释放锁lock.lockInterruptibly() |
|
能超时获取锁 |
在指定的截止时间之前获取锁,如果截止时间到了仍然无法获取锁,则返回false lock.tryLock(timeout,TimeUnit) |
队列同步器AQS
参考:
https://ifeve.com/java%e5%b9%b6%e5%8f%91%e4%b9%8baqs%e8%af%a6%e8%a7%a3/
简介
队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包(java.concurrent.util)的作者Doug Lea。比如等待队列、条件队列、锁的独占和共享等,都是基于AQS,其定义了一套多线程访问共享资源的同步器框架。
AQS的设计理念
前面提到过:同步器的设计一般包含几个方面:状态变量设计(同步器内部状态),访问条件设定,状态更新,等待方式,通知策略。用Java实现的队列同步器AQS也是如此。
同步器的设计是基于模版方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模版方法,而这些模版方法将会调用使用者重写的方法。
与synchronized对比而言,就Lock接口多了一些特性,如锁的获取与释放可以多个操作、可中断的获取锁、超时的获取锁等。AQS也包括这些特性,以及实现了锁的公平性、非公平性,当然还有可重入性。
大概猜想: Lock lock = new 锁; lock.lock();//加锁 for(;;){ if(CAS操作){//加锁成功就跳出 break; } //否则需要等待,用什么数据结构保存? //即公平性与非公平性,公平即保证进来的顺序排序,非公平即不保证顺序, //总结只能用队列 queue.put(thread);//存当前线程信息 //然后阻塞 LockSupport.park(thread); } //TODO处理业务逻辑 lock.unlock();//解锁 queue.pop();//移出队列,即下一线程为队首 LockSupport.unpark(thread);//唤醒下一线程
ReentrantLock
ReentrantLock
ReentrantLock lock = new ReentrantLock(); lock.lock(); try{ }finally{ lock.unlock(); }
AQS的接口框架
AQS它内部维护volatile int state(代表共享资源的可用状态)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
state的三种访问方式:
- getState():返回当前的同步状态值
- setState(int):设置当前的同步状态值
- compareAndSetState(int expect, int update):原子操作替换状态值
AQS定义了两种队列
- 同步等待队列(CLH队列)
- 条件等待队列
AQS定义两种资源共享方式:
- Exclusive(独占,只有一个线程能执行,如ReentrantLock)
- Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
AQS的实现与部分源码理解
从实现角度分析同步器是如何完成线程同步的,主要包括:同步队列、独占式锁的获取与释放、共享式锁的获取与释放等同步器的核心数据结构与模版方法。
同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)又名CLH队列(由Craig、Landin、Hagersten三人发行的基于双向链表的队列)来完成同步状态的管理。
当前线程获取同步状态失败时,同步器就会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会堵塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
Node的属性说明
属性 |
说明描述 |
int waitStatus |
表示节点的状态。其中包含的状态有:
|
Node prev |
前驱节点,比如当前节点被取消,那就需要前驱节点和后继节点来完成连接。 |
Node next |
后继节点。 |
Node nextWaiter |
存储condition队列(条件等待队列)中的后继节点。如果当前节点是共享的,那这个字段将是一个SHARED常量,即节点类型(独占和共享)和条件等待队列中的后继节点共用同一个字段。 |
Thread thread |
入队时获取同步状态的线程 |
Node成为sync队列和condition队列构建的基础,在同步器中就包含了sync队列。同步器拥有首节点和尾节点,没成功获取的线程就会加入该队列的尾部。同步队列基本结构如下:
1.当一个线程成功获取了同步状态(锁),其他线程将无法获取到同步状态,转而被构造成了一个Node并加入到同步队列中,而这个过程必须保证线程安全性,为此,同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect, Node update)
2.当首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,由于只有一个线程能成功获取到同步状态,因此不需要CAS操作,只需将head的next节点去掉原来的首节点并设置原首节点的后继节点为新的首节点即可。
独占式锁的获取
API说明:
方法名称 |
描述 |
protected boolean tryAcquire(int arg) |
排它的获取这个状态。这个方法的实现需要查询当前状态是否允许获取,然后再进行获取(使用compareAndSetState来做)状态。 |
protected boolean isHeldExclusively() |
在排它模式下,状态是否被占用。 |
acquire()
此方法是独占模式下线程获取共享资源的顶层入口。
如果获取到资源,线程直接返回,否则进入同步等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
函数流程如下:
- tryAcquire()尝试直接去获取资源,如果成功则直接返回true;
- 如果获取失败,addWaiter()将该线程加入同步等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程在同步等待队列中获取资源:
- 获取成功,节点出队,并且head去掉原首节点,且设置next为原首节点的后继节点,即当前节点
- 获取失败,阻塞等待被唤醒。(相关signal状态值会更改)
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
tryAcquire(arg)
尝试直接去获取资源,如果成功则直接返回true,否则返回false。就像Lock接口提供的方法tryLock()方法的语义一样。
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现,具体的堵塞、重入等特性需要自行定义。
非abstract,可自行选择模式(独占或共享)开发接口。
addWaiter
获取失败,addWaiter()将该线程加入同步等待队列的尾部,并标记为独占模式。
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; } } } } private Node addWaiter(Node mode) { //指定模式构造节点 独占或共享式 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure //快速入队 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
acquireQueued
前几步已经表明当前线程获取锁失败,且加入到队列尾部了。那么接下来就需要当前线程进入休眠等待期,等待被唤醒,才能正常的获取资源。
final boolean acquireQueued(final Node node, int arg) { //线程是否获取到锁 boolean failed = true; try { //是否被中断 boolean interrupted = false; //自旋 for (;;) { //获取当前节点的 前驱节点 final Node p = node.predecessor(); //如果前驱节点是头节点,代表当前线程所在的node在队列第一个,有资格尝试获取锁 if (p == head && tryAcquire(arg)) { //获取成功,则需要将当前线程的node设置为head节点 setHead(node); //将原head节点的next置null,代表原来的头节点脱离队列 p.next = null; // help GC failed = false; return interrupted; } //获取失败,阻塞等待,直到被唤醒unpark //1进入等待 //2判断是否被中断唤醒,且标记被中断过 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { //获取失败,取消尝试获取 if (failed) cancelAcquire(node); } } private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //前驱节点状态是signal(处理完后会通知自己),可以安全的继续等待 return true; if (ws > 0) { do { //去掉取消了的节点,往后找最近一个状态正常的节点 node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //当前驱节点waitStatus是0或者propagate时,将它设置为signal,然后才能安全地等待。 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } private final boolean parkAndCheckInterrupt() { //阻塞,底层调用系统内核功能 LockSupport.park(this); //返回当前线程的中断状态 return Thread.interrupted(); }
park()会让当前线程进入waiting状态。
在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
LockSupport.park(this);//如果被中断,后面的会一直堵塞
LockSupport.park();//如果被中断,后面的不会堵塞
这时引入一个释放的问题,也就是说使睡眠中的Node或者说线程获得通知的关键,就是前驱节点的通知,而这一个过程就是释放,释放会通知它的后继节点从睡眠中返回准备运行。
独占式锁的释放
方法名称 |
描述 |
protected boolean tryRelease(int arg) |
释放状态。 |
release(int arg)
acquire方法是保证能够获取到锁,即修改锁的状态。相反,release就是将锁状态设置回去,即释放锁,释放资源。而且此方法是独占模式下线程释放共享资源的顶层入口(各自行实现同步器下)
public final boolean release(int arg) { //释放一次锁成功?成功->原子化的将状态设置回去 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) //唤醒后继节点 unparkSuccessor(h); return true; } return false; }
tryRelease(int arg)
//释放锁资源,具体在子类中实现
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
unparkSuccessor(Node node)
private void unparkSuccessor(Node node) { //当前线程对应node在队列中的状态 int ws = node.waitStatus; if (ws < 0) //释放锁,设置状态为0,保证以后还能尝试继续访问一次 compareAndSetWaitStatus(node, ws, 0); //若持有锁当前线程的后继节点为空,或者状态为取消时,需要从队列尾部开始往前遍历寻找状态 //正常的节点线程 Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; //为什么从尾部开始遍历? //场景:当方法执行到此处时,可能确实没有后继节点,但也就是在此时,别的线程addWaiter进来 //了队列尾部,那tail!=null,防止以为没有后继节点,导致唤醒失败 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } //唤醒s线程,即状态ok的线程 if (s != null) LockSupport.unpark(s.thread); }
共享式锁的获取
API说明:
方法名称 |
描述 |
protected int tryAcquireShared(int arg) |
共享的模式下获取状态。 |
acquireShared(int arg)
此方法是共享模式下线程获取共享资源的顶层入口。
共享模式和独占模式是有所区别的:如文件的读写操作,当某个文件,被线程1读取的时候,其它线程也是可以读取的,但是当某个线程在写文件操作的时候,在这一时刻别的线程的读写操作都会被堵塞,直到写操作完成。
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
大概逻辑:
1.调用tryAcquireShared方法尝试获取共享锁,如果成功则直接返回,处理自己的逻辑。
2.调用tryAcquireShared方法失败会小于0,则调用doAcquireShared以共享模式加入同步队列中,并等待,拿到资源才返回。
tryAcquireShared(int arg)
与tryAcquire方法类似,都需要子类自己实现
//1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取; //2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了; //3.当返回值小于0时,表示获取同步状态失败。
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
doAcquireShared(int arg)
private void doAcquireShared(int arg) { //以共享模式加入同步队列中 final Node node = addWaiter(Node.SHARED); //标记获取锁成功与否的状态 boolean failed = true; try { //中断状态 boolean interrupted = false; //自旋 for (;;) { //获取前驱节点 final Node p = node.predecessor(); //前驱为头节点时, if (p == head) { //则可以再一次尝试获取共享锁 int r = tryAcquireShared(arg); //大于0说明还有同步状态可以选择 if (r >= 0) { //设置node为头节点,如果有下一节并没到安全点时,需设置为传播模式 setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } //达到安全点等待,见上面分析 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) { //存放旧的头节点 Node h = head; //设置node为新的头节点 setHead(node); //1。propagate > 0表示调用的线程指明了后继节点需要被唤醒才行 //2。头节点的后继节点需要被唤醒,不论是旧的还是新的头节点。 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //node是尾节点,或者 node的后继节点是共享模式的节点 if (s == null || s.isShared()) doReleaseShared(); } } final boolean isShared() { return nextWaiter == SHARED; }
doReleaseShared()
似懂非懂!!!
private void doReleaseShared() { //自旋 for (;;) { Node h = head; //一定有后继节点 if (h != null && h != tail) { int ws = h.waitStatus; //head状态为signal时,会重置为0,不会直接设置PROPAGATE,因为有两个地方可以 //进行unpark,当前方法setHeadAndPropagate和release方法,避免重复唤醒 // if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue;//设置失败,重新走循环 //设置成功,唤醒head.next节点的线程,此时锁如果被head.next获取,则head会指向 //当前获取锁的node,即head变了。 unparkSuccessor(h); } // 如果head节点的状态为0,需要设置为PROPAGATE,表示将状态向后继节点传播。 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // head变了 重新循环 break; } }
共享式锁的释放
方法名称 |
描述 |
protected boolean tryReleaseShared(int arg) |
共享的模式下释放状态。 |
releaseShared(int arg)
释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
tryReleaseShared(int arg)
尝试去释放指定量的共享锁,也是子类自行实现逻辑。
protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
doReleaseShared()在共享锁的获取时讲过,可以看出,共享锁的获取和释放都会涉及到doReleaseShared,也就是后继线程的唤醒。