1.管程 Java同步的设计思想
管程:指的是管理共享变量以及对共享变量的操作过程,让他们支持并发;
互斥:同一时刻,只允许一个线程访问资源;
同步:线程之间的通信与协作
2.MESA 模型:
MESA 模型(MESA-Model)是一个用于描述计算机系统中并发编程的模型,Java中现在的并发库中很多的类,都是依据这个模型进行设计的
上图就是MESA 的模型,并发中主要存在的问题就是上述所说的互斥和共享功能,这个模型中的入口处有个等待队列就是为了解决互斥功能的,里面的条件等待队列就是解决共享功能的
3.AQS
什么是AQS ?
AQS 是 Java 中的一个重要的同步器框架,"AbstractQueuedSynchronizer" 的缩写,AQS 提供了一种用于实现各种同步器的基础框架,例如锁(Lock)、信号量(Semaphore)、倒计数器(CountDownLatch)等等
如何通过AQS实现一把自己的独占锁?
代码:
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class HuangLock extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());//设置当前正在用的线程
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);//将当前正在使用的线程设为空
setState(0);
return true;
}
public void lock() { acquire(1);}
public boolean tryLock() { return tryAcquire(1);}
public void unlock() { release(1);}
public boolean isLocked() { return isHeldExclusively(); }
}
其实很简单就是继承之后,新增加锁 解锁 , 等常用方法,这里解释下两个override 的方法,这两个方法AQS中都没有实现,需要我们自己实现,即通过CAS的方法针对 state 的值进行获取 和 对state 值的替换。
4.源码分析
其实源码可以通过几个需要知道的知识点去看,比如如何实现互斥,即独占锁,还有公平与非公平
等待中的线程如何进入等待队列的,等待队列的结构是什么,以及线程释放锁之后的出队以及对下
一个线程的唤醒,这都是我们需要去了解的;
在IDEA 中,右击选择Thread 我们就能在多线程中进行debug ,能帮助我们更好的去debug阅读源
码。
下面我们通过 ReentrantLock 进行源码的解读:
我们发现Java中这些同步类基本的套路都是,通过一个Sync 来继承AQS 然后通过这个内部类将AQS的所有调用都映射到 Sync 方法;我个人的思路是从lock()方法入手去阅读源码,我们假设是第一个线程进入lock()方法:
图一是非公平锁的lock()方法,图二是公平锁的lock()方法;显而易见,非公平锁在获取锁资源的时候会进行一个CAS 操作,来判段我们是否能获取锁,如果获取不到再排队,acquire() 中的参数1 大家可能不理解,其实就是获取几把锁,用于可重入锁的次数累加,到时候释放锁的时候把这个参数减到0就是全部释放了,后面如果看到相关源码会说;好我们现在进入acquire () 方法
这段代码呢,看字面意思就是获取不到,这个线程就挂起,现成我们来看看,tryAcquire()方法
这个是非公平的,公平的和它公用这一个,我们发现这个state 变量,就是判断我们能不能获取锁的关键,如果 state = 0 那么就能够获取到锁,如果 state != 0 ,那就代表获取不到;这里就能看到上面参数arg的用处了,int nextc = c + acquire ;就是为了实现可重入;
如果我们是第一个线程那么肯定是可以获取到锁的 所以我们的第一个线程很简单,就这个轻而易举的获取到了锁,然后设置了当前锁是由哪个线程独占的; setExclusiveOwnerThread(Thread.currentThread());
接下来我们是第二个线程再次进入lock() 这个方法;
我们会发现我们获取不到锁,并且会执行
acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 这个方法我们进入看看:
我们先看看addWaiter() 方法 这里的入参是个空的node 节点;我们来看下这个node 是什么样的
通过它的pre 和 next 属性,我们不难发现,由多个 Node 构成的是一种双向链表的队列结构;我们继续往下看,当第二个线程进来后,它的 tail 节点肯定是 NULL ,因为这个队列还没创建,所以它会直接走enq() 方法;
因为线程二需要入队,所以线程二在到这一步的时候,需要创建一个队列并进行入队操作,我们看到源码是利用for 循环来进行这种操作的;当我们的尾结点为空时,它会通过CAS 将一个空节点设置为我们的head 节点,由于是双向链表且只有一个节点,所以这个节点既是头节点也是也是尾节点;当队列创建成功后,就会把线程二这个node 的前驱节点设置为上一个的尾结点,然后通过CAS 将现在的节点设置为新的尾结点,然后再把上次一的节点指向我们现在的尾节点,我们画个图就清楚了如下图:
接下来我们跳出来进入acquireQueued()方法:
这个方法主要是用来挂起线程的,首先它会获取当前节点的前面一个节点P ,判断是不是头节点,如果是头节点那么它会尝试去获取锁,因为中断需要上下文切换比较消耗性能,然后它会判断是不是该把线程二挂起,挂起前它会把上个节点的waitStatus 改成 -1 ,这个意思是代表它可以唤醒下个节点的线程;
当准备工作做好,它就会挂起线程二
线程三入队的情况和二一样,如下图:
上面这些就l是ock()方法所涉及的相关源码;接下来我们看下 unlock() 的相源码:
通过上面的截图可知主要就是把state 设置为0 , 再把当前独占锁的线程置空; int c = getState() - releases ; 就是之前说的可重入会对 state 进行累加;然后回到release() 调用 unparkSuccessor(h)
这里直接把线程二给唤醒了,
如上图所示,线程二现在去获取锁,可以成功获取,通过setHead 方法和 p.next = null ; 就把第一个节点给断开了,线程一就出去了
线程一出队之后如下图所示:线程二变成了头节点
以上就是AQS相关源码的分析