前言
环境
这是至今仍存在的问题,粒度大,只适合单一场景的问题已经随着内部锁的优化逐渐解决。Lock 解决了以上问题,Lock总体如下:
- 支持公平锁与非公平锁
- 支持尝试获取锁
- 支持取消获取共享资源
- 支持多个“条件”
- 支持共享锁和互斥锁
- 支持重入
基础模块
依照管程模型,由 java 实现。具体由 AbstractQueuedSynchronizer 类来实现
可以从以下三个部分来看实现:互斥、线程等待队列、条件变量及条件队列
共享资源状态的实现方式
分析
先思考一下,如何实现互斥?一个线程可以持有多个共享资源,而一个共享资源只可以被一个线程持有。线程与共享资源的关系是一对多。所以我觉得可以这样实现,用一个类来封装共享资源和线程ID,在线程获取共享资源时,先查看该资源是否已经有关联的线程,如果有,则进入等待队列,如果没有,则尝试获取,获取后,将线程 ID 填入。这个资源并不一定是可以指代的实体,比如说代码块,因此需要再将思路优化下,保证互斥就需要在进入共享资源前先进行判断是否有线程持有,因此,可以在共享资源前添加一个方法,让线程执行到共享资源前,先进入这个方法进行判断,可以使用一个变量来判断当前有线程持有该资源,并且判断是哪一个线程(线程切换后,重新来到这里需要判断下是否是自己持有该资源)。Lock 就是这样,用 state 来判断是否有线程持有,用 exclusiveOwnerThread 来记录持有线程
当没有线程持有共享资源时,state 就是 0,如果有线程持有,那么 state > 0,同时设置线程持有者
总结
- Lock 使用 AQS 中的 state 来表示资源是否被线程持有
Lock 使用 AQS 中的 exclusiveOwnerThread 来记录持有该资源的线程
等待队列的实现方式
就如上面管程模型描述的那样,当共享资源被其他线程持有,线程进入等待队列中,等待持有共享资源的线程释放共享资源。
先看下它的节点
prev 和 next ,看起来还是个双向队列呀,waiter就是队列存储的数据咯。status,从注释看,不知道是做什么的;想从方法着手,看看 status 在方法中起到的作用,结果一层一层,跳到最后发现是由 JVM 实现的,无奈,去上网查了查相关的博客,发现 openjdk 14的实现和 openjdk 8的实现还不一样,具体从哪一版本开始变更的,这也不重要,来看一下区别,推断下 status 到底是做什么的OpenJDK 8 的实现方式
严重怀疑,我是不是下了假源码……openjdk 8 的注释好详细啊。回归正题,openjdk 8 将条件,独占,共享等队列的节点用同一个节点进行表示
先看下源码(这里我把源码中的注释删掉了,太多了,感兴趣的可以去看一下) ```java static final class Node {/** Marker to indicate a node is waiting in shared mode */static final Node SHARED = new Node();/** Marker to indicate a node is waiting in exclusive mode */static final Node EXCLUSIVE = null;/** waitStatus value to indicate thread has cancelled */static final int CANCELLED = 1; //取消/** waitStatus value to indicate successor's thread needs unparking */static final int SIGNAL = -1;//释放/** waitStatus value to indicate thread is waiting on condition */static final int CONDITION = -2;//在条件队列中等待/*** waitStatus value to indicate the next acquireShared should* unconditionally propagate*/static final int PROPAGATE = -3;
volatile int waitStatus;volatile Node prev;volatile Node next;volatile Thread thread;Node nextWaiter;final boolean isShared() {return nextWaiter == SHARED;}final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}Node() { // Used to establish initial head or SHARED marker}Node(Thread thread, Node mode) { // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) { // Used by Conditionthis.waitStatus = waitStatus;this.thread = thread;}}
<a name="PIvM2"></a>#### OpenJDK 14 的实现方式openJDK 14 中的 abstract static class,就是队列的基础结构,其余的 条件,独占,共享等节点,都以此为基础进行扩展<br />```java/** CLH Nodes */abstract static class Node {volatile Node prev; // initially attached via casTailvolatile Node next; // visibly nonnull when signallableThread waiter; // visibly nonnull when enqueuedvolatile int status; // written by owner, atomic bit ops by others// methods for atomic operationsfinal boolean casPrev(Node c, Node v) { // for cleanQueuereturn U.weakCompareAndSetReference(this, PREV, c, v);}final boolean casNext(Node c, Node v) { // for cleanQueuereturn U.weakCompareAndSetReference(this, NEXT, c, v);}final int getAndUnsetStatus(int v) { // for signallingreturn U.getAndBitwiseAndInt(this, STATUS, ~v);}final void setPrevRelaxed(Node p) { // for off-queue assignmentU.putReference(this, PREV, p);}final void setStatusRelaxed(int s) { // for off-queue assignmentU.putInt(this, STATUS, s);}final void clearStatus() { // for reducing unneeded signalsU.putIntOpaque(this, STATUS, 0);}private static final long STATUS= U.objectFieldOffset(Node.class, "status");private static final long NEXT= U.objectFieldOffset(Node.class, "next");private static final long PREV= U.objectFieldOffset(Node.class, "prev");}
初始化
入队
出队
“等待”
公平锁与非公平锁
其实就是当一个不在等待队列和条件队列中的线程尝试获取共享资源,如果 state 为 0,即该资源暂时没有线程获取时,是否要判断下等待队列及条件队列中是否有线程在等待。公平锁是,如果有线程在等待,则加入等待队列末尾;如果没有线程等待,则获取该资源。而非公平锁是不判断是否有等待线程,直接获取。代码如下(等待队列及条件队列是按管程模型来讲的,在实现中,这是同一个队列,只是节点属性不同)
公平锁源码
非公平锁源码
可以看到,如果 CAS 获取成功,则直接设为持有线程了,并不判断是否有等待线程
互斥锁
介绍及使用
实现方式
锁定
释放
共享锁
介绍及使用
可以这我的这篇文章 ReadWriteLock
实现方式
锁定
释放
重入
介绍及使用
我觉得引入重入锁,一是为了减少 “锁的重量”,比如当同一个线程多次访问已持有的被锁保护的共享资源,如果是按“持有锁-操作共享资源-释放锁”这一流程,要想重复使用,得先把共享资源释放,然后再竞争,对于需要重复使用的场景,很明显严重拖累了速度。二是为了避免死锁,当锁定的方法需要调用另一个同一个锁锁定的方法时,如果不支持重入,那么就会陷入死锁。
实现方式
锁定
进入lock 后,调用 sync.lock 方法,sync 是 AQS 子类的实例
解锁
验证
验证代码
代码如下:
package cn.zjm404.stu.thread.lock.explicition;import java.util.concurrent.locks.ReentrantLock;/*** @author ZJM*/public class ReentrantLockTest {private final ReentrantLock lock = new ReentrantLock();public void method1(){lock.lock();try{//...执行任务System.out.println("hello world");}finally {lock.unlock();}}public void method2(){lock.lock();try{method1();}finally {lock.unlock();}}}
package cn.zjm404.stu.thread.lock.explicition;public class Client {public static void main(String[] args) {ReentrantLockTest rt = new ReentrantLockTest();rt.method2();}}
