深入理解并发编程之Lock源码分析
一、Lock简要分析
Java中我们常用的锁就是Synchronized关键字和Lock锁,Synchronized是由C++写的,我们在前面已经分析了,Lock锁是使用Java代码写的我们可以直接点击进去看代码分析,看代码我们知道:
Lock是通过AQS + LockSupport + CAS实现
AQS
AQS全称为AbstractQueuedSynchronizer 是一个抽象同步队列,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件。简单来说AQS就是一个队列,在Lock锁中的作用是用来存放阻塞线程的。
LockSupport
LockSupport是一个工具类,主要功能是阻塞和唤醒线程,有两个核心方法:
- park():用来阻塞线程,可以不传参标识阻塞当前线程,也可以用线程做参数表示阻塞传递的线程。
- unpark():用来唤醒线程,用线程做参数表示唤醒传递的线程。
看一个简单使用代码:
public class Test001 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("start");
LockSupport.park(Thread.currentThread()); // 让当前线程变为阻塞状态,也可以不传参数默认当前线程
System.out.println("end" + Thread.currentThread().isInterrupted());
});
t1.start();
try {
Thread.sleep(1000); //防止代码执行过快先打印唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("唤醒阻塞的线程");
LockSupport.unpark(t1); //唤醒刚才阻塞的thread线程
}
}
看一下执行结果,符合预期(先执行打印唤醒然后执行打印end):
CAS
CAS我们在前面已经讲过了,比较交换,V(共享变量)、E(旧值)、N(新值)三个参数,先比较V==E,如果相等则把N新值赋值给V。
二、Lock源码分析
这里不细翻代码了,直接使用通过抽取Lock核心源码写的简要Lock锁来分析(当然这只是个简单的源代码示意,实际代码量是比较多的,考虑的问题比较全面操作也比较细,这个简单代码在并发量较大的情况下还是有不少漏洞的,这是做原理分析简单测试没问题的):
public class MyLock {
/**
* 获取锁的状态 0当前线程没有获取该锁,1 已经有线程获取到该锁
*/
AtomicInteger state= new AtomicInteger(0);
// 记录锁被那个线程持有
private transient Thread exclusiveOwnerThread;
// 存放没有获取锁到的线程
private ConcurrentLinkedDeque<Thread> waitThreads = new ConcurrentLinkedDeque<>();
/**
* 获取锁
*/
public void lock() {
// 底层使用cas 修改锁的状态从0变为1 硬件层面帮助我们实现
if (acquire()) {
return;
}
// 使用cas 修改锁的状态失败 设计重试次数
Thread currentThread = Thread.currentThread();
// 如果该线程已经存在的情况下
waitThreads.add(currentThread);
for (; ; ) {
//短暂重试
if (acquire()) {
// 移除队列
waitThreads.push(currentThread);
return;
}
// 重试一次还是没有获取到锁,将当前的这个线程变为阻塞状态
LockSupport.park();
}
}
/**
* 释放锁
*/
public void unLock() {
if (exclusiveOwnerThread != Thread.currentThread()) {
throw new RuntimeException("不是当前线程在释放锁");
}
// 释放锁
if (compareAndSetState(1, 0)) {
this.exclusiveOwnerThread = null;
// 取出阻塞的线程 唤醒
Thread pollThread = waitThreads.poll();
if (pollThread != null)
// 唤醒刚才阻塞的线程
LockSupport.unpark(pollThread);
}
}
/**
* 通过CAS修改锁的状态
* @return
*/
private boolean acquire() {
if (compareAndSetState(0, 1)) { //如果预期是0,则修改状态为1表示当前线程获取到锁
//当前线程获取到锁
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* CAS 三个参数 V E
*
* @param expect
* @param update
* @return
*/
private boolean compareAndSetState(int expect, int update) {
return state.compareAndSet(expect, update);
}
/**
* 设置获取锁的为当前线程
* @param exclusiveOwnerThread
*/
public void setExclusiveOwnerThread(Thread exclusiveOwnerThread) {
this.exclusiveOwnerThread = exclusiveOwnerThread;
}
}
首先对三个属性进行讲解:
- state:这是标识符,用来标识当前线程是否获取到锁,0表示没有获取到锁;1表示获取到锁,源码中使用的private volatile int state,我们就简单的使用原子类来用一下。
- exclusiveOwnerThread:用来记录当前持有锁的线程。
- waitThreads:这是一个队列,用来存放没有获取到锁进入等待状态的线程,源码中使用的双向链表操作比较复杂,暂时用个队列类来代替一下。
下面说一些几个方法:
- lock()/unLock():这两个方法不用说了,上锁和解锁。
- acquire():这是CAS比较方法,里面核心compareAndSetState(0,1)有两个参数,预期值和更新值,意思是如果预期是0,我们修改为1。并且返回操作是否成功。
- setExclusiveOwnerThread():设置持有锁标记的线程。
下面重点对lock()和unlock()进行下讲解:
lock():
- 进入方法首先使用CAS操作来修改锁的状态,如果修改成功,则设置当前当前线程持有锁标记;
- 如果修改失败,说明锁标记可能被其他线程持有,将当前线程放入等待池,然后再进行CAS判断重试。
- 如果几次(这里for循环可以自己设置重试次数)修改后还是失败,阻塞当前线程,重试几次中有一次操作成功,则设置当前当前线程持有锁标记,并且从等待池中删除当前线程。
这里有个需要注意的问题,当前线程放进等待池需要放在for循环前面,因为后面如果失败则进入阻塞状态了不会进行任何代码操作了。
unlock():
5. 进入方法首先判断是否是持有锁标记的线程来释放锁标记的,如果其他线程释放锁标记直接抛出异常。
6. 然后释放锁,将锁的状态从1修改到0,表示当前锁空闲了。
7. 最后从锁池中取出一个锁唤醒,当然这里直接唤醒了第一个,是个公平锁,如果唤醒所有的线程则是非公平锁。
还是用卖票来实验一下:
public class Thead001 extends Thread {
private static int count = 100;
private MyLock lock = new MyLock();
@Override
public void run() {
while (count>0){
lock.lock();
System.out.println(Thread.currentThread().getName() + ",正在出票,第"+(100-count+1)+"张");
count --;
System.out.println("剩余票数"+count);
lock.unLock();
}
}
public static void main(String[] args) {
Thread thread = new Thead001();
new Thread(thread, "窗口1").start();
new Thread(thread, "窗口2").start();
new Thread(thread, "窗口3").start();
new Thread(thread, "窗口4").start();
new Thread(thread, "窗口5").start();
}
}
没加锁的情况下出现了超卖现象:
用我们写的简单锁卖票正常:
内容来源:蚂蚁课堂添加链接描述