多线程---Lock和ReentrantLock使用详情

  • (一)Lock和ReentrantLock介绍和使用
    • (1)Lock和ReentrantLock介绍
    • (2)Lock锁实现同步效果
    • (3)Lock的trylock方法
    • (4)使用Lock时候的线程交互
    • (5)总结Lock(ReentrantLock)和synchronized的区别
  • (二)LockSuport工具类
    • (1)LockSuport介绍
    • (2)LockSuport特征
    • (3)认识parker对象
    • (4)void park()方法【消费线程的许可证,挂起线程】
    • (5)void unpark(Thread thread)方法【给线程发放许可证,唤醒线程】
    • (6)void parkNanos(long nanos)方法
    • (7)【推荐】park(Object blocker)方法
    • (8)void parkNanos(Object blocker, long nanos)方法
    • (9)void parkUntil(Object bocker, long deadline)方法
    • (10)方法测试案例
  • (三)抽象同步队列AQS概述
    • (1)AQS介绍
    • (2)AQS结构设计
    • (3)Node的内部元素
    • (4)state状态信息:
    • (5)操作state独占方式:
    • (6)操作state共享方式:
  • (四)ReentrantLock源码分析
    • 【1】源码中的类和方法介绍
      • (1)类的继承关系
      • (2)类的内部类
        • (1)内部类Sync、NonfairSync、FairSync介绍
        • (2)Sync 类:锁父类
        • (3)NonfairSync:采用非公平策略获取锁
        • (4)FairSync:采用公平策略获取锁
      • (3)类的属性
      • (4)类的构造函数【默认非公平锁】
      • (5)核心函数分析总结【补上过程的详细描述】
      • (6)CAS
      • (7)AQS阻塞队列
      • (48)AQS的公平锁与非公平锁
        • (1)公平锁
        • (2)非公平锁
        • (3)state字段
        • (4)AQS的应用场景
    • 【2】通过demo引出ReentrantLock的实现
      • (1)准备好demo方法的代码
      • (2)考虑给drawMoney方法加一个ReentrantLock锁
      • (3)猜测一下ReentrantLock的lock方法是怎如何阻塞线程?以及为什么?
    • 【3】lock方法:线程获取ReentrantLock锁以及加入等待队列
      • (1)acquire方法【公平&非公平有区别】
      • (2)tryAcquire:尝试加锁或者重入锁【公平&非公平有区别】
      • (3)addWaiter:把当时所有线程加入AQS等待队列
      • (4)acquireQueued:把当前获取锁失败的线程用park方法置为阻塞状态
      • (5)获取锁过程总结
    • 【4】unlock方法:释放锁过程
      • (1)release方法
      • (2)tryRelease方法:当前线程修改state=0释放锁
      • (3)unparkSuccessor方法:unpark唤醒AQS中的下一个线程
      • (4)解锁过程总结
    • 【5】补充知识
      • (1)CANCELLED状态节点生成
      • (2)中断恢复后的执行流程【中断线程】
    • 【6】小结
    • 【7】自定义同步工具
    • (5)lock()与unlock()实现原理【待处理】
  • (五)什么情况下使用ReenTrantLock
    • (1)ReenTrantLock使用场景
    • (2)案例一:使用Lock把MyStack改造为线程安全的类并且实现生产者消费者问题
    • (3)案例二:借助tryLock 解决死锁问题
  • (六)读写锁ReentrantReadWriteLock的原理
    • (1)什么情况下使用ReentrantReadWriteLock
    • (2)ReentrantReadWriteLock的一些方法
    • (3)ReentrantReadWriteLock的优缺点
    • (4)ReentrantReadWriteLock使用详解
  • (七)JDK8中新增的StampedLock锁的原理

(一)Lock和ReentrantLock介绍和使用

(1)Lock和ReentrantLock介绍

(2)Lock锁实现同步效果

Lock是一个接口,为了使用一个Lock对象,需要用到

Lock lock = new ReentrantLock();

与 synchronized (someObject) 类似的,lock()方法,表示当前线程占用lock对象,一旦占用,其他线程就不能占用了。
与 synchronized 不同的是,一旦synchronized 块结束,就会自动释放对someObject的占用。 lock却必须调用unlock方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行。

public class TestThreadLock01 {public static String nowTime() {return new SimpleDateFormat("HH:mm:ss").format(new Date());}public static void log(String msg) {System.out.printf("%s %s %s %n",nowTime(),Thread.currentThread().getName(),msg);}public static void main(String[] args) {Lock lockDemo = new ReentrantLock();Thread thread1 = new Thread() {public void run() {try {log("线程启动");log("试图占有对象:lock");lockDemo.lock();log("占有对象:lock");log("进行5秒的业务操作");Thread.sleep(5000);} catch (Exception e) {e.printStackTrace();} finally {log("释放对象:lock");lockDemo.unlock();}log("线程结束");}};thread1.setName("thread01");thread1.start();try {//先让t1飞2秒Thread.sleep(2000);} catch (InterruptedException e1) {e1.printStackTrace();}Thread thread2 = new Thread() {public void run() {try {log("线程启动");log("试图占有对象:lock");lockDemo.lock();log("占有对象:lock");log("进行5秒的业务操作");Thread.sleep(5000);} catch (Exception e) {e.printStackTrace();} finally {log("释放对象:lock");lockDemo.unlock();}log("线程结束");}};thread2.setName("thread02");thread2.start();}
}

总结:
1-向上转型创建ReentrantLock的对象
2-对象调用lock方法,对线程上锁
3-在finally里对象调用unlock方法,释放锁

(3)Lock的trylock方法

synchronized 是不占用到手不罢休的,会一直试图占用下去。trylock会在指定时间范围内试图占用, 如果时间到了,还占用不成功,就会放弃。

注意: 因为使用trylock有可能成功,有可能失败,所以后面unlock释放锁的时候,需要判断是否占用成功了,如果没占用成功也unlock,就会抛出异常

public class TestThreadTrylock {public static String nowTime() {return new SimpleDateFormat("HH:mm:ss").format(new Date());}public static void log(String msg) {System.out.printf("%s %s %s %n",nowTime(),Thread.currentThread().getName(),msg);}public static void main(String[] args) {Lock lockDemo = new ReentrantLock();Thread thread1 = new Thread() {public void run() {boolean lockResult = false;try {log("线程启动");log("试图占有对象:lock");lockResult = lockDemo.tryLock(1, TimeUnit.SECONDS);if(lockResult){log("占有对象:lock");log("进行5秒的业务操作");Thread.sleep(5000);} else {log("经过1秒钟的努力,还没有占有对象,放弃占有");}} catch (Exception e) {e.printStackTrace();} finally {if(lockResult){log("释放对象:lock");lockDemo.unlock();}}log("线程结束");}};thread1.setName("thread01");thread1.start();try {//先让t1飞2秒Thread.sleep(2000);} catch (InterruptedException e1) {e1.printStackTrace();}Thread thread2 = new Thread() {public void run() {boolean lockResult = false;try {log("线程启动");log("试图占有对象:lock");lockResult = lockDemo.tryLock(1, TimeUnit.SECONDS);if(lockResult){log("占有对象:lock");log("进行5秒的业务操作");Thread.sleep(5000);} else {log("经过1秒钟的努力,还没有占有对象,放弃占有");}} catch (Exception e) {e.printStackTrace();} finally {if(lockResult){log("释放对象:lock");lockDemo.unlock();}}log("线程结束");}};thread2.setName("thread02");thread2.start();}
}

总结:
1-创建ReentrantLock的对象:Lock lock = new ReentrantLock();
2-创建boolean数据代表是否占有成功:boolean locked = false;
3-对象调用trylock方法尝试占用,并且返回结果:locked = lock.tryLock(1,TimeUnit.SECONDS);
4-在finally里判断占用结果并且释放锁
if(locked){
log(“释放对象:lock”);
lock.unlock();
}

(4)使用Lock时候的线程交互

使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法

Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal,signalAll 方法

public class TestThreadAwaitSignal {public static String nowTime() {return new SimpleDateFormat("HH:mm:ss").format(new Date());}public static void log(String msg) {System.out.printf("%s %s %s %n",nowTime(),Thread.currentThread().getName(),msg);}public static void main(String[] args) {Lock lockDemo = new ReentrantLock();Condition condition = lockDemo.newCondition();Thread thread1 = new Thread() {public void run() {boolean lockResult = false;try {log("线程启动");log("试图占有对象:lock");lockDemo.lock();log("占有对象:lock");log("进行5秒的业务操作");Thread.sleep(5000);log("临时释放对象 lock, 并等待");condition.await();log("重新占有对象 lock,并进行5秒的业务操作");Thread.sleep(5000);} catch (Exception e) {e.printStackTrace();} finally {log("释放对象:lock");lockDemo.unlock();}log("线程结束");}};thread1.setName("thread01");thread1.start();try {//先让t1飞2秒Thread.sleep(2000);} catch (InterruptedException e1) {e1.printStackTrace();}Thread thread2 = new Thread() {public void run() {boolean lockResult = false;try {log("线程启动");log("试图占有对象:lock");lockDemo.lock();log("占有对象:lock");log("进行5秒的业务操作");Thread.sleep(5000);log("唤醒等待中的线程");condition.signal();} catch (Exception e) {e.printStackTrace();} finally {log("释放对象:lock");lockDemo.unlock();}log("线程结束");}};thread2.setName("thread02");thread2.start();}
}

(5)总结Lock(ReentrantLock)和synchronized的区别

(1)相似点
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

(2)功能区别

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现需要lock()和unlock()方法配合try/finally语句块来完成。
  2. Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。 借助Lock的这个特性,就能够规避死锁,synchronized必须通过谨慎和良好的设计,才能减少死锁的发生。
  3. synchronized在发生异常和同步块结束的时候,会自动释放锁。而Lock必须手动释放, 所以如果忘记了释放锁,一样会造成死锁。

(3)性能的区别
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

【Synchronized】
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

【ReentrantLock】
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
1-等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
2-公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

//创建一个非公平锁,默认是非公平锁
Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(false);//创建一个公平锁,构造传参true
Lock lock = new ReentrantLock(true);

3-锁绑定多个条件:一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

下面通过伪代码,进行更加直观的比较:

 // **************************Synchronized的使用方式**************************// 1.用于代码块synchronized (this) {}// 2.用于对象synchronized (object) {}// 3.用于方法public synchronized void test () {}// 4.可重入for (int i = 0; i < 100; i++) {synchronized (this) {}}// **************************ReentrantLock的使用方式**************************public void test () throw Exception {// 1.初始化选择公平锁、非公平锁ReentrantLock lock = new ReentrantLock(true);// 2.可用于代码块lock.lock();try {try {// 3.支持多种加锁方式,比较灵活; 具有可重入特性if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }} finally {// 4.手动释放锁lock.unlock()}} finally {lock.unlock();}}

(二)LockSuport工具类

(1)LockSuport介绍

LockSuport是个工具类,主要作用就是挂起和唤醒线程,LockSupport 和 CAS 是Java并发包中并发工具(锁和其他同步类)控制机制的实现基础。LockSuport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSuport里的方法的线程是不持有许可证的。LockSuport是使用Unsafe类实现的。

(2)LockSuport特征

(1)LockSupport是非重入的,这个很简单,因为park的意思仅仅是阻塞某个线程而已,并不是“锁”,调用一次park方法,线程就被阻塞了。
(2)LockSupport的park阻塞、unpark唤醒的调用不需要任何条件对象,也而不需要先获取什么锁。在一定程度上降低代码的耦合度,即LockSupport只与线程绑定,并且被park的线程并不会释放之前获取到的锁。
(3)park阻塞与unpark唤醒的调用顺序可以颠倒,不会出现死锁,并且可以重复多次调用unpark;而stop和resume方法如果顺序反了,就会出现死锁现象。
(4)park支持中断唤醒,但是不会抛出InterruptedException异常,可以从isInterrupted不会清除中断标记)、interrupted(会清除中断标记)方法中获得中断标记。

(3)认识parker对象

每个线程都有自己的一个parker对象,这个parker对象由_counter,_cond(条件变量),_mutex三部分组成。

一个线程就像一台小时候路边杂货店门口需要投币的游戏机,不过这台游戏机比较特殊,无论投多少个游戏币进去只算投进去一个,并且没有游戏币后会马上关机。parker就像这台游戏机的币池,_counter就像币池中的剩余币数(0为耗尽,1为还有余币),_cond就好比币池里控制游戏机电源开关的闸门。

调用park()就是看还有没有余币:如果没有余币,那么游戏机将断电,变成关机状态。如果还有余币,那么游戏机不需要关机,继续运行。

调用unpark()就像投币:如果游戏机是关机状态,就唤醒开机运行。如果游戏机正在运行,那么将补充一枚游戏币,将剩余币数+1,但多次调用unpark()也只能补充1枚游戏币。

例子1
(1)当前线程调用Unsafe.park()方法
(2)检查_counter,发现_counter为0,此时获得_mutex互斥锁
(3)Thread-0进入_cond条件变量阻塞
(4)设置_count = 0

例子2
(1)调用Unsafe.unpark(Thread_0)方法,设置_counter为1
(2)唤醒Thread-0
(3)Thread-0恢复运行
(4)重新设置_counter为0


例子3
(1)调用Unsafe.unpark(Thread_0)方法,设置_counter为1
(2)当前线程调用Unsafe.park()方法
(3)检查_counter的值发现为1,Thread-0无需阻塞,继续运行
(4)将_counter置为0

(4)void park()方法【消费线程的许可证,挂起线程】

park方法用来挂起当前线程,如果当前线程的许可证可用,方法会立刻返回,并且消费掉当前线程的许可证。没有了许可证则当前线程进入挂起状态。如果当前线程的许可证不可用则说明已经被挂起了

(5)void unpark(Thread thread)方法【给线程发放许可证,唤醒线程】

unpark本身就是发放许可,并通知等待的线程,已经可以结束等待了

(1)首先阻塞式的获取mutex锁,获取不到则一直阻塞在此,直到获取成功。
(2)获取到mutex锁之后,获取当前的许可_counter的值保存在变量s中,然后将_counter的值置为1。
(3)如果s小于1,表示没有了许可,此时可能存在线程被挂起,也可能不存在,继续向下判断:
1-如果_cur_index不为-1,那么肯定有在_cur_index对应索引的条件变量上挂起,那么需要唤醒:如果设置了WorkAroundNPTLTimedWaitHang(linux默认设置),那么先signal唤醒在条件变量上等待的线程然后释放mutex锁,方法结束;否则先释放mutex锁然后signal唤醒在条件变量上等待的线程,方法结束。
2-否则_cur_index等于-1,表示没有线程在条件变量上等待,直接释放mutex锁,方法结束。
(4)否则,s等于1,表示一直存在许可,那么就什么都不做,仅仅是unlock释放mutex锁就行了,方法结束。

/*
提供一个许可
*/
void Parker::unpark() {int s, status ;//类似于park,阻塞式的获取互斥量(锁),表示已上锁,如果互斥量已被获取,该线程将在该方法处阻塞,直到获取成功status = pthread_mutex_lock(_mutex);assert (status == 0, "invariant") ;//保存旧的_counters = _counter;//将_counter置为1,这里也能看出来无论调用多少次unpark,“许可”都不会变得更多_counter = 1;//如果原来的_counter为0,表示没有了许可,此时可能存在线程被挂起,也可能不存在if (s < 1) {// 如果_cur_index不等于初始值-1,那么表示有线程在当前parker的对应的条件变量上挂起了//_cur_index为0,则是因为调用相对时间的park方法,在第一个条件变量上挂起,//_cur_index为1,则是因为调用绝对时间的park方法,在第二个条件变量上挂起,if (_cur_index != -1) {// thread is definitely parked/*如果设置了WorkAroundNPTLTimedWaitHang,那么先调用signal再调用unlock,否则相反*///WorkAroundNPTLTimedWaitHang是一个JVM参数,默认为1if (WorkAroundNPTLTimedWaitHang) {//先signal唤醒一条在指定条件变量上等待的线程status = pthread_cond_signal (&_cond[_cur_index]);assert (status == 0, "invariant");//再unlock释放互斥量(锁)status = pthread_mutex_unlock(_mutex);assert (status == 0, "invariant");}/*否则就是先unlock 再signal*/else {//先unlock释放互斥量(锁)status = pthread_mutex_unlock(_mutex);assert (status == 0, "invariant");//再signal唤醒一条在指定条件变量上等待的线程status = pthread_cond_signal (&_cond[_cur_index]);assert (status == 0, "invariant");}}/*否则,表示没有线程在条件变量上等待,仅仅是unlock释放互斥量(锁)就行了,因为park方法返回的时候会设置_cur_index为-1*/else {pthread_mutex_unlock(_mutex);assert (status == 0, "invariant") ;}}/*否则,表示原来的_counter为1,表示一直存在许可,那么仅仅unlock释放互斥量(锁)就行了*/else {pthread_mutex_unlock(_mutex);assert (status == 0, "invariant") ;}
}

(6)void parkNanos(long nanos)方法

(7)【推荐】park(Object blocker)方法

参数:blocker,用来标识当前线程在等待的对象,即记录线程被阻塞时被谁阻塞的,用于线程监控和分析工具来定位。
根据源码可以看到的是参数blocker是在park之前先通过setBlocker()记录阻塞线程的发起者object,当线程锁被释放后再次清除记录;推荐使用该方法,而不是park(),因为这个函数可以记录阻塞的发起者,如果发生死锁方便查看,在线程dump中会明确看到这个对象

public static void park(Object blocker) {//获取当前线程Thread t = Thread.currentThread();//记录当前线程阻塞的原因,底层就是unsafe.putObject,就是把对象存储起来setBlocker(t, blocker);//执行parkunsafe.park(false, 0L);//线程恢复后,去掉阻塞原因setBlocker(t, null);
}

从源码可以看到真实的实现均在 unsafe,unsafe.park核心实现如下

JavaThread* thread=JavaThread::thread_from_jni_environment(env);
...
thread->parker()->park(isAbsolute != 0, time);

就是获取java线程的parker对象,然后执行它的park方法。它继承了os::PlatformParker,内置了一个volatitle的 _counter。parker实现如下

void Parker::park(bool isAbsolute, jlong time) {if (_counter > 0) {//已经有许可了,用掉当前许可_counter = 0 ;//使用内存屏障,确保 _counter赋值为0(写入操作)能够被内存屏障之后的读操作获取内存屏障事前的结果,也就是能够正确的读到0OrderAccess::fence();//立即返回return ;}Thread* thread = Thread::current();assert(thread->is_Java_thread(), "Must be JavaThread");JavaThread *jt = (JavaThread *)thread;if (Thread::is_interrupted(thread, false)) {// 线程执行了中断,返回return;}if (time < 0 || (isAbsolute && time == 0) ) { //时间到了,或者是代表绝对时间,同时绝对时间是0(此时也是时间到了),直接返回,java中的parkUtil传的就是绝对时间,其它都不是return;}if (time > 0) {//传入了时间参数,将其存入absTime,并解析成absTime->tv_sec(秒)和absTime->tv_nsec(纳秒)存储起来,存的是绝对时间unpackTime(&absTime, isAbsolute, time);}//进入safepoint region,更改线程为阻塞状态ThreadBlockInVM tbivm(jt);if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {//如果线程被中断,或者是在尝试给互斥变量加锁的过程中,加锁失败,比如被其它线程锁住了,直接返回return;}
//这里表示线程互斥变量锁成功了int status ;if (_counter > 0)  {// 有许可了,返回_counter = 0;//对互斥变量解锁status = pthread_mutex_unlock(_mutex);assert (status == 0, "invariant") ;OrderAccess::fence();return;}#ifdef ASSERT// Don't catch signals while blocked; let the running threads have the signals.
// (This allows a debugger to break into the running thread.)  //debug用
sigset_t oldsigs;sigset_t* allowdebug_blocked = os::Linux::allowdebug_blocked_signals();pthread_sigmask(SIG_BLOCK, allowdebug_blocked, &oldsigs);
#endif
//将java线程所拥有的操作系统线程设置成 CONDVAR_WAIT状态 ,表示在等待某个条件的发生
OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);
//将java的_suspend_equivalent参数设置为truejt->set_suspend_equivalent();// cleared by handle_special_suspend_equivalent_condition() or java_suspend_self()if (time == 0) {//把调用线程放到等待条件的线程列表上,然后对互斥变量解锁,(这两是原子操作),这个时候线程进入等待,当它返回时,互斥变量再次被锁住。//成功返回0,否则返回错误编号status = pthread_cond_wait (_cond, _mutex) ;} else {//同pthread_cond_wait,只是多了一个超时,如果超时还没有条件出现,那么重新获取胡吃两然后返回错误码 ETIMEDOUTstatus = os::Linux::safe_cond_timedwait (_cond, _mutex, &absTime) ;if (status != 0 && WorkAroundNPTLTimedWaitHang) {//WorkAroundNPTLTimedWaitHang 是JVM的运行参数,默认为1//去除初始化pthread_cond_destroy (_cond) ;
//重新初始化pthread_cond_init    (_cond, NULL);}}assert_status(status == 0 || status == EINTR ||status == ETIME || status == ETIMEDOUT,status, "cond_timedwait");#ifdef ASSERTpthread_sigmask(SIG_SETMASK, &oldsigs, NULL);
#endif//等待结束后,许可被消耗,改为0  _counter = 0 ;
//释放互斥量的锁status = pthread_mutex_unlock(_mutex) ;assert_status(status == 0, status, "invariant") ;// If externally suspended while waiting, re-suspend if (jt->handle_special_suspend_equivalent_condition()) {jt->java_suspend_self();}
//加入内存屏障指令OrderAccess::fence();
}

总结
(1)无论是什么情况返回,park方法本身都不会告知调用方返回的原因,所以调用的时候一般都会去判断返回的场景,根据场景做不同的处理
(2)线程的等待与挂起、唤醒等等就是使用的POSIX的线程API
(3)park的许可通过原子变量_count实现,当被消耗时,_count为0,只要拥有许可,就会立即返回

(8)void parkNanos(Object blocker, long nanos)方法

和park(Object blocker)一样,增加了超时时间,单位为纳秒,超时立即返回,

(9)void parkUntil(Object bocker, long deadline)方法

和park(Object blocker)一样,增加了超时时间点,单位为毫秒,超时立即返回

(10)方法测试案例

(1)park/unpark 基本测试

/*** park/unpark测试*/
@Test
public void test2() {System.out.println("begin park");//调用park方法LockSupport.park();//使当前线程获取到许可证,明显执行不到这一步来,因为在上一步就已经阻塞了LockSupport.unpark(Thread.currentThread());System.out.println("end park");
}/*** park/unpark测试*/
@Test
public void test3() {System.out.println("begin park");//使当前线程先获取到许可证LockSupport.unpark(Thread.currentThread());//再次调用park方法,先获得了许可,因此该方法不会阻塞LockSupport.park();System.out.println("end park");
}/*** park/unpark测试*/
@Test
public void test4() {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {long currentTimeMillis = System.currentTimeMillis();System.out.println("begin park");LockSupport.park();System.out.println("end park");System.out.println(System.currentTimeMillis() - currentTimeMillis);}});thread.start();//开放或者注释该行代码,观察end park时间//Thread.sleep(2000);//使当子线程获取到许可证LockSupport.unpark(thread);
}

(2)Park 线程状态测试

/*** park线程状态测试** @throws InterruptedException*/
@Test
public void test1() throws InterruptedException {//park不限时Thread thread = new Thread(() -> LockSupport.park());//park限时Thread thread2 = new Thread(() -> LockSupport.parkNanos(3000000000l));thread.start();thread2.start();//主线睡眠一秒,让子线程充分运行Thread.sleep(1000);//获取处于park的子线程状态System.out.println(thread.getState()); System.out.println(thread2.getState());
}

结果是:

WAITING
TIMED_WAITING

(3)park broker测试

/*** park broker测试*/
public static void main(String[] args) {//分别尝试注释这两行代码,运行程序,运行cmd,使用jps  命令,找到该进程对应的pid,然后使用jstack pid   命令,就可以看到线程信息.//LockSupport.park();LockSupport.park(new LockSupportTest());
}

分别注释其中一个方法,获得结果如下(找到main线程):
使用park,不能看到boroker信息:
使用park(broker),可以看到broker信息,因此推荐使用该方法阻塞线程:

(三)抽象同步队列AQS概述

(1)AQS介绍

AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包里锁的底层就是使用AQS实现的。大多数开发者可能永远不会直接使用AQS,但是知道其原理对于架构设计会有帮助。

(2)AQS结构设计

AbstractQueuedSynchronizer被设计为一个抽象类,主要包含三个部分
【1】state
它使用了一个volatile int类型的成员变量state来表示同步状态(或者说资源),可以通过getState、setState、compareAndSetState函数修改其值。
对于不同的同步组件的实现来说,state可以有不同的含义。对于ReentrantLock 的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock 来说,state 的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于Semaphore来说,state用来表示当前可用信号的个数:对于CountDownlatch 来说,state 用来表示计数器当前的值。
【2】Node双向队列
通过一个内置的FIFO双向同步队列来完成资源获取线程的排队等待工作,内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node,用来保存获取同步状态失败的线程的线程引用、等待状态以及前驱结点和后继结点。当前线程获取同步状态失败(获取锁失败)的时候,AQS会将当前线程及其等待状态信息构造成一个结点Node并将其加入同步队列中,同时阻塞当前线程,当同步状态由持有线程释放的时候,会将同步队列中的首结点中的线程唤醒使其再次尝试获取同步状态。
【3】ConditionObject条件队列
通过一个或者多个ConditionObject条件队列来实现线程之间的通信(等待和唤醒)。
ConditionObject 可以直接访问AQS 对象内部的变量,比如state 状态值和AQS 同步队列。ConditionObject 又被称为条件变量,每个条件变量实例的内部又维护(对应)了一个条件队列(单向链表,又称等待队列),其用来存放调用Condition的await方法后被阻塞的线程,这个条件队列的头、尾元素分别由firstWaiter 和lastWaiter持有。

上面的介绍能看出来,AQS中包含两个队列的实现,一个同步队列,用于存放获取不到锁的线程,另一个是条件队列,用于存放调用了await方法的线程,但是在两个队列中等待的线程都是WAITING状态,因为Lock锁底层都是调用的LockSupport.park方法来实现阻塞等待的。一个AQS实例拥有一个同步队列,可拥有多个条件队列。

(3)Node的内部元素

【1】SHARED
【2】EXCLUSIVE
【3】waitStatus
1-CANCELLED
2-SIGNAL
3-CONDITION
4-PROPAGATE

(4)state状态信息:

可以用来表示当前线程获取锁的可重入次数。线程同步的关键就是对状态值state进行操作,根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。AQS既可以支持独占式地访问同步状态(比如ReentrantLock),也可以支持共享式地访问同步状态(比如CountDownLatch),这样就可以方便实现不同类型的同步组件。
1-getState():获取当前最新同步状态。
2-setState(int newState):设置当前最新同步状态。
3-compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

这三个方法getState()、setState()、compareAndSetState()都是final方法,是AQS提供的通用设置同步状态的方法,能保证线程安全,我们直接调用即可。

/*** int类型同步状态变量,或者代表共享资源,被volatile修饰,具有volatile的读、写的内存语义*/
private volatile int state;/*** @return 返回同步状态的当前值。此操作具有volatile读的内存语义,因此每次获取的都是最新值*/
protected final int getState() {return state;
}/*** @param newState 设置同步状态的最新值。此操作具有volatile写的内存语义,因此每次写数据都是写回主存并导致其它缓存实效*/
protected final void setState(int newState) {state = newState;
}/*** 如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。* 此操作具有volatile读取和写入的内存语义。** @param expect 预期值* @param update 写入值* @return 如果更新成功返回true,失败则返回false*/
protected final boolean compareAndSetState(int expect, int update) {//内部调用unsafe的方法,该方法是一个CAS方法//这个unsafe类,实际上是比AQS更加底层的底层框架,或者可以认为是AQS框架的基石。//CAS操作在Java中的最底层的实现就是Unsafe类提供的,它是作为Java语言与Hospot源码(C++)以及底层操作系统沟通的桥梁return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

(5)操作state独占方式:

使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会获取失败然后被阻塞,例如独占锁ReentrantLock的实现。当一个线程获取ReentrantLock的锁后,在AQS内部会首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把状态值从1改为2,也就是设置可重入次数,而当另一个线程获取锁时发现自己并不是该锁的持有者就会被放入AQS阻塞队列挂起。

获取和释放资源的方法
(1)acquire(int arg)【获取资源的流程】
独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待。该方法不会响应中断。该方法内部调用了可重写的tryAcquire方法。

当一个线程调用acquire方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源,获取的方式就是设置状态变量state的值,成功就返回,失败则将当前线程封装为类型为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用park方法挂起自己

(2)acquireInterruptibly(int arg)
与acquire方法相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前被中断,则该方法会抛出InterruptedException并返回。

(3)tryAcquireNanos(int arg,long nanos)
在acquireInterruptibly方法基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,获取到了返回true。

(4)release(int arg)【释放资源的流程】
独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个结点包含的线程唤醒。该方法内部调用了可重写的tryRelease方法。

当一个线程调用release方法时会尝试使用tryRelease操作释放资源,释放的方式就是设置状态变量state的值然后调用unpark方法激活AQS队列里面被阻塞的一个线程。被激活的线程则使用tryAcquire尝试,看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

例如继承自AQS实现的独占锁ReentrantLock在实现tryRelease时,在内部需要使用CAS算法把当前的state的值从1修改为0,并设置当前锁的持有者为null,然后返回true,如果CAS失败则返回false

(6)操作state共享方式:

使用共享方式获取的资源是跟具体线程不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时,如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。比如Semaphore信号量,当一个线程通过acquire方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量。

(1)acquireShared(int arg)
共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待。与独占式的不同是同一时刻可以有多个线程获取到同步状态。该方法不会响应中断。该方法内部调用了可重写的tryAcquireShared方法。

当线程调用acquireShared获取共享资源时,会首先使用tryAcquireShared尝试获取资源,具体是设置状态state的值,成功则直接返回,失败则将当前线程封装为Node.SHARED的Node节点后插入到AQS阻塞队列额尾部,并使用park方法挂起自己。

(2)acquireSharedInterruptibly(int arg)
与acquireShared (int arg) 相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前被中断,则该方法会抛出InterruptedException并返回。

(3)tryAcquireSharedNanos(int arg,long nanos)
在acquireSharedInterruptibly方法基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,获取到了返回true。

(4)releaseShared(int arg)
共享式释放同步状态,该方法会在释放同步状态之后,尝试唤醒同步队列中的后继节点中的线程。该方法内部调用了可重写的tryReleaseShared方法。

当一个线程调用releaseShared时会尝试使用tryReleaseShared操作释放资源,具体的操作就是设置状态变量state的值,然后使用unpark激活AQS队列里面被阻塞的一个线程。被激活的线程则使用tryReleaseShared查看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

例如继承自AQS实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryAcquireShared时,首先查看写锁是否被其他线程持有,如果是则直接返回false,否则使用CAS递增state的高16位。读锁在重写tryReleaseShared时,在内部需要使用CAS算法把当前state值的高16位减1,然后返回true,如果CAS失败则返回false。

(四)ReentrantLock源码分析

ReenTrantLock的实现是一种自旋锁,通过循环调用 CAS操作+AQS队列 来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

可重入锁:
独占锁:
自旋锁:

【1】源码中的类和方法介绍

(1)类的继承关系

ReentrantLock 实现了 Lock接口,Lock接口中定义了 lock与 unlock相关操作,并且还存在 newCondition方法,表示生成一个条件。

public class ReentrantLock implements Lock, java.io.Serializable

(2)类的内部类

(1)内部类Sync、NonfairSync、FairSync介绍

ReentrantLock 总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

ReentrantLock 类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与 FairSync类继承自 Sync类,Sync类继承自 AbstractQueuedSynchronizer抽象类。下面逐个进行分析。

AQS提供了大量用于自定义同步器实现的 Protected方法。自定义同步器实现的相关方法也只是为了通过修改 State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock需要实现的方法如下,并不是全部):
(1)protected boolean isHeldExclusively() 该线程是否正在独占资源。只有用到Condition才需要去实现它。
(2)protected boolean tryAcquire(int arg) 独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。
(3)protected boolean tryRelease(int arg) 独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。
(4)protected int tryAcquireShared(int arg) 共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
(5)protected boolean tryReleaseShared(int arg) 共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。

一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease。以非公平锁为例,这里主要阐述一下非公平锁与AQS之间方法的关联之处,具体每一处核心方法的作用会在文章后面详细进行阐述。

(2)Sync 类:锁父类

Sync类直接继承自AQS,AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下。state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被改为2,则就是可重入次数。在该线程释放该锁时,会尝试使用CAS让状态值-1,如果-1后状态值为0,则当前线程释放该锁。

 abstract static class Sync extends AbstractQueuedSynchronizer {// 序列号private static final long serialVersionUID = -5179523762034025860L;// 获取锁abstract void lock();// 非公平方式获取final boolean nonfairTryAcquire(int acquires) {// 当前线程final Thread current = Thread.currentThread();// 获取状态int c = getState();if (c == 0) { // 表示没有线程正在竞争该锁if (compareAndSetState(0, acquires)) { // 比较并设置状态成功,状态0表示锁没有被占用// 设置当前线程独占setExclusiveOwnerThread(current); return true; // 成功}}else if (current == getExclusiveOwnerThread()) { // 当前线程拥有该锁int nextc = c + acquires; // 增加重入次数if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 设置状态setState(nextc); // 成功return true; }// 失败return false;}// 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不为独占线程throw new IllegalMonitorStateException(); // 抛出异常// 释放标识boolean free = false; if (c == 0) {free = true;// 已经释放,清空独占setExclusiveOwnerThread(null); }// 设置标识setState(c); return free; }// 判断资源是否被当前线程占有protected final boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}// 新生一个条件final ConditionObject newCondition() {return new ConditionObject();}// 返回资源的占用线程final Thread getOwner() {        return getState() == 0 ? null : getExclusiveOwnerThread();}// 返回状态final int getHoldCount() {            return isHeldExclusively() ? getState() : 0;}// 资源是否被占用final boolean isLocked() {        return getState() != 0;}// 自定义反序列化逻辑private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();setState(0); // reset to unlocked state}}

(3)NonfairSync:采用非公平策略获取锁

NonfairSync 类继承了 Sync类,表示采用非公平策略获取锁,其实现了 Sync类中抽象的 lock方法,源码如下:从 lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。Acquire方法是 FairSync和 UnfairSync的父类 AQS中的核心方法。

 // 非公平锁static final class NonfairSync extends Sync {// 版本号private static final long serialVersionUID = 7316153563782823691L;// 获得锁final void lock() {/*** 若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。* 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入Acquire方法进行后续处理。*/if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用// 把当前线程设置独占了锁setExclusiveOwnerThread(Thread.currentThread());else // 锁已经被占用,或者set失败// 以独占模式获取对象,忽略中断acquire(1); //Acquire方法是FairSync和UnfairSync的父类AQS中的核心方法。}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}

(4)FairSync:采用公平策略获取锁

FairSync 类也继承了 Sync类,表示采用公平策略获取锁,其实现了 Sync类中的抽象 lock方法

 // 公平锁static final class FairSync extends Sync {// 版本序列化private static final long serialVersionUID = -3000897897090466540L;final void lock() {// 以独占模式获取对象,忽略中断acquire(1);}// 尝试公平获取锁protected final boolean tryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取状态int c = getState();if (c == 0) { // 状态为0if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功// 设置当前线程独占setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据// 下一个状态int nextc = c + acquires;if (nextc < 0) // 超过了int的表示范围throw new Error("Maximum lock count exceeded");// 设置状态setState(nextc);return true;}return false;}}

跟踪 lock方法的源码可知,当资源空闲时,它总是会先判断 sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync 类的 lock的方法调用如下,只给出了主要的方法。

可以看出只要资源被其他线程占用,该线程就会添加到 sync queue中的尾部,而不会先尝试获取资源。这也是和 Nonfair最大的区别,Nonfair每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。

为了帮助大家理解 ReentrantLock和 AQS之间方法的交互过程,以非公平锁为例,我们将加锁和解锁的交互流程单独拎出来强调一下,以便于对后续内容的理解。

(1)加锁
1-通过ReentrantLock的加锁方法Lock进行加锁操作。
2-会调用到内部类Sync的Lock方法,由于Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的Lock方法,本质上都会执行AQS的Acquire方法。
3-AQS的Acquire方法会执行tryAcquire方法,但是由于tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire。
4-tryAcquire是获取锁逻辑,获取失败后,会执行框架 AQS的后续逻辑,跟ReentrantLock自定义同步器无关。

(2)解锁
1-通过 ReentrantLock的解锁方法 Unlock进行解锁。
2-Unlock会调用内部类 Sync的 Release方法,该方法继承于AQS。
3-Release中会调用 tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
4-释放成功后,所有处理由AQS框架完成,与自定义同步器无关。

通过上面的描述,大概可以总结出 ReentrantLock加锁解锁时 API层核心方法的映射关系。

(3)类的属性

ReentrantLock 类的 sync非常重要,对ReentrantLock 类的操作大部分都直接转化为对 sync和 AQS类的操作。

public class ReentrantLock implements Lock, java.io.Serializable {// 序列号private static final long serialVersionUID = 7373984872572414699L;    // 同步队列private final Sync sync;
}

(4)类的构造函数【默认非公平锁】

(1)ReentrantLock 构造函数:默认是采用的非公平策略获取锁

public ReentrantLock() {// 默认非公平策略sync = new NonfairSync();
}

(2)ReentrantLock(boolean) 构造函数:可以传递参数确定采用公平策略或者是非公平策略,参数为 true表示公平策略,否则,采用非公平策略。

public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

(5)核心函数分析总结【补上过程的详细描述】

通过分析 ReentrantLock的源码,可知对其操作都转化为对 Sync对象的操作,由于 Sync继承了 AQS,所以基本上都可以转化为对 AQS的操作。如将 ReentrantLock的 lock函数转化为对 Sync的 lock函数的调用,而具体会根据采用的策略(如公平策略或者非公平策略)的不同而调用到 Sync的不同子类。所以可知,在 ReentrantLock的背后,是 AQS对其服务提供了支持。下面还是通过例子来更进一步分析源码。

(6)CAS

CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现

(7)AQS阻塞队列

AbstractQueuedSynchronizer简称AQS

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

ReentrantLock的可重入应用:ReentrantLock的可重入性是 AQS很好的应用之一,在了解完上述知识点以后,我们很容易得知ReentrantLock实现可重入的方法。在ReentrantLock里面,不管是公平锁还是非公平锁,都有一段逻辑。

(48)AQS的公平锁与非公平锁

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,要根据是公平锁还是非公平锁确定是否会被其他线程抢占。

(1)公平锁

如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

// java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquireif (c == 0) {if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}
}
else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;
}

(2)非公平锁

如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquireif (c == 0) {if (compareAndSetState(0, acquires)){setExclusiveOwnerThread(current);return true;}
}
else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;
}

从上面这两段都可以看到,有一个同步状态State来控制整体可重入的情况。State是Volatile修饰的,用于保证一定的可见性和有序性。

(3)state字段

AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下。state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被改为2,则就是可重入次数。在该线程释放该锁时,会尝试使用CAS让状态值-1,如果-1后状态值为0,则当前线程释放该锁。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;

接下来看State这个字段主要的过程:
1-State初始化的时候为0,表示没有任何线程持有锁。
2-当有线程持有该锁时,值就会在原来的基础上+1,同一个线程多次获得锁是,就会多次+1,这里就是可重入的概念。
3-解锁也是对这个字段-1,一直到0,此线程对锁释放。

(4)AQS的应用场景

JUC中的应用场景:除了上边ReentrantLock的可重入性的应用,AQS作为并发编程的框架,为很多其他同步工具提供了良好的解决方案。下面列出了JUC中的几种同步工具,大体介绍一下AQS的应用场景:
(1)ReentrantLock:使用AQS保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。
(2)Semaphore:使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。
(3)CountDownLatch:使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过。
(4)ReentrantReadWriteLock:使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数。
(5)ThreadPoolExecutor:Worker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease)。

【2】通过demo引出ReentrantLock的实现

(1)准备好demo方法的代码

代码中是一个去银行取钱的案例,取钱这个过程应该是线程安全的

public class testReentrantLock01 {public static void main(String[] args) {new Thread(()->{drawMoney();},"线程1").start();new Thread(()->{drawMoney();},"线程2").start();}private static void drawMoney() {System.out.println(Thread.currentThread().getName()+"正在取钱...");sleep(3000);System.out.println(Thread.currentThread().getName()+"取完了");}private static void sleep(long millis) {try {Thread.sleep(millis);} catch (InterruptedException e) {e.printStackTrace();}}}

可以看到执行的结果

(2)考虑给drawMoney方法加一个ReentrantLock锁

加锁的意义可以这么来理解,一个方法等于一个仓库,没有锁之前,所有人都可以随时进入这个仓库拿东西放东西,这样的话一个人刚放进去的东西扭头就被别人拿走了,这个人的操作流程就被打乱了不完整了,导致每个人到这个仓库拿东西都比较乱。

于是我们引入了锁,每个人进入这个仓库后会给仓库门上把锁锁上,别人再想进入这仓库就需要等待解锁,这样就可以保证爱某一时间段里仓库里只有一个人,这个人想怎么操作都不会被别人的行为打扰,这样就是线程安全的。

现在drawMoney方法就是那个仓库,ReentrantLock就是那把锁,每个线程都是想进入这个仓库的人。

加完锁以后每个线程的执行顺序就可以保证有序了

(3)猜测一下ReentrantLock的lock方法是怎如何阻塞线程?以及为什么?

(1)能想到的阻塞线程的方法,以及是否合适的原因
1-wait:不合适,因为wait需要好synchronized搭配使用,单独使用会抛出异常,但是如果要用synchronized的话就没有必要用Lock了
2-sleep:sleep可以阻塞线程一段时间,但是不能自动的控制阻塞的时间,每次时间都是人为设定的
3-park
4-while循环:不停的循环判断标识state是不是0,如果是0就返回true,如果是01就一值循环,直到别的线程调用unlock解锁把state改为0。这样会导致占用大量CPU


(2)使用while循环和CAS方法保证只有一个线程能完成state由0到1的修改
现在想保证当两个线程进入lock方法的时候,只有一个线程能把state从0改为1完成加锁,于是考虑使用CAS方法,用CAS就需要引入Unsafe类调用compareAndSwapInt方法。

this表示当前线程,stateOffset代表变量加上偏移量之后的值,0是预期值,1是目标值。运行的流程就是,线程1进入lock,用state值和预期值0比较,如果state是0,就把state改成目标值1,完成上锁;线程2进入lock,用state值和预期值0比较,但是现在state已经是1了,所以这里不会再进行修改

通过CAS来控制后,就可以保证同一时间段只有1个线程能获得state=1,其他没能完成加锁的线程自然就进入了while循环里等待了,直到state再次变为0,才会有一个线程从while循环里出来。

这种实现属于乐观锁,自旋锁,非公平锁!


但是这样有个问题,一个线程进入while循环后就不会不停的执行方法,这样随着线程增多就会导致占满CPU

(3)使用上面的自旋锁再加上park方法实现一把公平锁

【3】lock方法:线程获取ReentrantLock锁以及加入等待队列

点进lock方法,看到调用的是父类Sync里的lock方法
点进Sync的lock方法,可以看到这个lock方法是抽象的,交给子类去实现了
lock方法的实现有两种,公平锁和非公平锁

(1)acquire方法【公平&非公平有区别】

(1)公平锁FairSync的形式
点进来公平锁的lock方法,看到调用的是acquire方法,并且传参值为1


(2)非公平锁NonfairSync的形式

(3)调用的方法解析

【1】tryAcquire方法:尝试加锁
用来尝试修改state值为arg=1,或者重入锁给state+1,修改成功就返回true,否则就返回false;

既然加锁没有成功,那么接下来就应该线程进入等待队列了

【2】addWaiter:往等待队列里添加线程Node
传递的参数是Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了AQS的独占锁功能

【3】acquireQueued:阻塞这个等待线程

【4】selfInterrupt方法:线程中断
就是让当前线程中断,暂时不用管这个方法

(2)tryAcquire:尝试加锁或者重入锁【公平&非公平有区别】

(1)公平锁的形式
为什么要有hasQueuedPredecessors方法的判断?
其实就是判断当前线程是否需要排队。
判断当前等待对列有没有头结点,其实就是判断当前等待队列里有没有其他线程节点在排队。如果有其他线程在排队,那就要返回false然后去排队了,只有当等待节点是空的时候才有可能去修改state值获取锁。

hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False,说明当前线程可以争取共享资源;如果返回True,说明队列中存在有效节点,当前线程必须加入到等待队列中。

看到这里,我们理解一下h != t && ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么?

双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。当h != t时: 如果(s = h.next)==null,等待队列正在有线程进行初始化,但只是进行到了Tail指向Head,没有将Head指向Tail,此时队列中有元素,需要返回True(这块具体见下边代码分析)。 如果(s = h.next) != null,说明此时队列中至少有一个有效节点。如果此时s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。

 // java.util.concurrent.locks.AbstractQueuedSynchronizer#enqif (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}

节点入队不是原子操作,所以会出现短暂的head != tail,此时Tail指向最后一个节点,而且Tail指向Head。如果Head没有指向Tail(可见5、6、7行),这种情况下也需要将相关线程加入队列中。所以这块代码是为了解决极端情况下的并发问题。

(2)非公平锁的形式

注意一点,非公平锁里根据不去判断AQS等待队列里有没有正在排队的队列,直接使用CAS去给state修改值,就是摆明了直接插队。
所以,非公平锁加锁的过程就是在lock方法里CAS一次尝试加锁,在tryAcquire方法里再CAS一次尝试加锁。如果两次加锁不成功的话,才会加入AQS等待队列等待。总结起来就是先尝试两次CAS加锁,成功的话就直接加锁,两次都失败后会加入AQS队列里等待。

(3)addWaiter:把当时所有线程加入AQS等待队列

----------------------------CAS部分-----------------------------
1-先把当前线程生成Node对象
2-获取等待队列的尾节点
3-如果尾节点不是空的,就是等待队列不为空,就把新的线程节点从队列尾部入队
4-如果队列是空的,节点会直接调用enq方法,再次判断队列是不是空,如果依然为空就把当前节点放到队列头结点位置,如果不为空了就队尾入队。
5-如果队列不是空的,但是线程CAS竞争失败了,也是会调用enq方法。


主要流程
(1)通过当前的线程和锁模式新建一个节点。
(2)Pred指针指向尾节点Tail。
(3)将New中Node的Prev指针指向Pred。
(4)通过compareAndSetTail方法,完成尾节点的设置。这个方法主要是对 tailOffset和 Expect进行比较,如果 tailOffset的 Node和 Expect的 Node地址是相同的,那么设置 Tail的值为 Update的值。

 // java.util.concurrent.locks.AbstractQueuedSynchronizerstatic {try {stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));} catch (Exception ex) { throw new Error(ex); }
}

从AQS的静态代码块可以看出,都是获取一个对象的属性相对于该对象在内存当中的偏移量,这样我们就可以根据这个偏移量在对象内存当中找到这个属性。tailOffset指的是 tail对应的偏移量,所以这个时候会将 new出来的 Node置为当前队列的尾节点。同时,由于是双向链表,也需要将前一个节点指向尾节点。

----------------------------enq部分-----------------------------

可能会有多个线程同时进入判断,同时想获取这个新的Node来完成入队,那么就需要使用compareAndSetTail(pred, node)判断保留的尾节点是不是没有变,没变就成功入队;变了就说明别的线程捷足先登入队了,那么剩下的所有Node就要使用enq去重新入队了,enq里会有for循环的自旋操作把当前节点加入到队列中。

如果 Pred指针是 Null(说明等待队列中没有元素),或者当前 Pred指针和 Tail指向的位置不同(说明被别的线程已经修改),就需要看一下 Enq的方法。

如果没有被初始化,需要进行初始化一个头结点出来。但请注意,初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素,则与之前的方法相同。其实,addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。

要注意一点,第一个节点的thread是null的

(4)acquireQueued:把当前获取锁失败的线程用park方法置为阻塞状态

上文解释了addWaiter方法,这个方法其实就是把对应的线程以 Node的数据结构形式加入到双端队列里,返回的是一个包含该线程的Node。而这个 Node会作为参数,进入到 acquireQueued方法中。acquireQueued方法可以对排队中的线程进行“获锁”操作。总的来说,一个线程获取锁失败了,被放入等待队列,acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。

下面我们从“何时出队列?”和“如何出队列?”两个方向来分析一下acquireQueued源码:

// java.util.concurrent.locks.AbstractQueuedSynchronizerfinal boolean acquireQueued(final Node node, int arg) {// 标记是否成功拿到资源boolean failed = true;try {// 标记等待过程中是否中断过boolean interrupted = false;// 开始自旋,要么获取锁,要么中断for (;;) {// 获取当前节点的前驱节点final Node p = node.predecessor();// 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点)if (p == head && tryAcquire(arg)) {// 获取锁成功,头指针移动到当前nodesetHead(node);p.next = null; // help GCfailed = false;return interrupted;}// 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。具体两个方法下面细细分析if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}


(1)如果当前节点的前一个节点是头结点(代表前一个节点已经获取锁),并且当前节点尝试获取锁成功
1-把当前节点设置为新的头结点,修改当前节点的thread=null、prev=null,把AQS阻塞队列的head指针后移一位到当前节点上,这样就把当前节点设置为新的头结点了。

!!!头节点一定表示当前获取锁的线程节点。!!!

注:setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据。

2-把上一个头结点的next指针置为null,这样这个头结点Node对象就没有了引用和联系,就可以被GC清除了

(2)shouldParkAfterFailedAcquire方法:根据waitStatus判断当前节点是否需要阻塞挂起
新加入等待队列的当前节点的waitStatus默认值为0,正常状态下会把前一个节点的waitStatus=修改为-1
1、如果前一个节点的waitStatus=-1待唤醒状态(如果前一个节点已经获取锁了的话waitStatus为1),说明前一个节点还在等待唤醒,就结束并返回,当前节点可以阻塞,停止循环并阻塞挂起;
2、如果前一个节点的waitStatus=1为取消状态(前一个节点已经获取锁或者已经中断),do-while循环向前查找所有取消状态的节点(前面可能有很多中断的节点都是取消状态),把取消的节点从队列中剔除,当前节点不可以阻塞,继续循环;
3、如果前一个节点的waitStatus不是1也不是-1(一般队尾的节点默认为0),就用CAS方法把前一个节点的waitStatus改为-1,把当前节点的waitStatus设为0,当前节点不可以阻塞,继续循环(下一次循环判断为-1就会阻塞了)。

// java.util.concurrent.locks.AbstractQueuedSynchronizer// CANCELLED:由于超时或中断,此节点被取消。节点一旦被取消了就不会再改变状态。特别是,取消节点的线程不会再阻塞。
static final int CANCELLED =  1;
// SIGNAL:此节点后面的节点已(或即将)被阻止(通过park),因此当前节点在释放或取消时必须断开后面的节点
// 为了避免竞争,acquire方法时前面的节点必须是SIGNAL状态,然后重试原子acquire,然后在失败时阻塞。
static final int SIGNAL    = -1;
// 此节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列(此时状态将设置为0),直到条件时才会被重新移动到同步等待队列 。(此处使用此值与字段的其他用途无关,但简化了机制。)
static final int CONDITION = -2;
//传播:应将releaseShared传播到其他节点。这是在doReleaseShared中设置的(仅适用于头部节点),以确保传播继续,即使此后有其他操作介入。
static final int PROPAGATE = -3;//0:以上数值均未按数字排列以简化使用。非负值表示节点不需要发出信号。所以,大多数代码不需要检查特定的值,只需要检查符号。
//对于正常同步节点,该字段初始化为0;对于条件节点,该字段初始化为条件。它是使用CAS修改的(或者在可能的情况下,使用无条件的volatile写入)。// 靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 获取头结点的节点状态int ws = pred.waitStatus;// 说明头结点处于唤醒状态,前继节点还在等待触发,还没当前节点的什么事儿,所以当前节点可以被parkif (ws == Node.SIGNAL)return true; // 通过枚举值我们知道waitStatus>0是取消状态// 前继节点是CANCELLED>0 ,则需要充同步队列中删除,并检测新接上的前继节点的状态,若还是为CANCELLED ,还需要重复上述步骤if (ws > 0) {do {// 循环向前查找取消节点,把取消节点从队列中剔除node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 设置前任节点等待状态为SIGNAL// 到这一步,waitstatus只有可能有2种状态,一个是0,一个是PROPAGATE,无论是哪个都需要把当前节点的状态设置为SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}

(3)调用parkAndCheckInterrupt方法,里面的park方法会挂起当前线程
parkAndCheckInterrupt主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。

----------------------------waitStatus概述-----------------------------
waitStatus
AQS等待队列中都是前一个线程节点在释放锁的时候unpark唤醒后一个线程节点,然后出队,这样可以保证公平锁的排队机制。每一个节点入队的时候自身的waitStatus为0,但是会先把前一个节点的waitStatus改为-1再park挂起,等于跟前面的线程打声招呼,你用完锁以后unpark叫我一下。那么只有队列中最后一个节点尾结点的waitStatus才会是0。线程释放锁的时候,会根据自身节点的waitStatus判断当前节点后面还有没有其他的线程需要unpark唤醒,如果是0就不需要unpark了。

为什么在队列中还要根据waitStatus来判断唤醒,直接唤醒下一个不就行了吗?这是因为有的线程虽然进入了等待队列,但是在等待的时候被中断了,那么这个线程就不需要被唤醒了,它的waitStatus就会被改成0表示不需要unpark了。

(5)获取锁过程总结

上述方法的流程图如下:

从上图可以看出,跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,具体挂起流程用流程图表示如下(shouldParkAfterFailedAcquire流程):

从队列中释放节点的疑虑打消了,那么又有新问题了:
1-shouldParkAfterFailedAcquire中取消节点是怎么生成的呢?什么时候会把一个节点的waitStatus设置为-1?
2-是在什么时间释放节点通知到被挂起的线程呢?

【4】unlock方法:释放锁过程

点进方法看到unlock方法的实现体是Sync的release方法

(1)release方法

点进release方法

(1)tryRelease方法释放锁
(2)判断头结点和waitStatus
这里的判断条件为什么是h != null && h.waitStatus != 0?

h == null Head还没初始化。初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null 的情况。
h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。
h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。

(3)unparkSuccessor方法

(2)tryRelease方法:当前线程修改state=0释放锁

在ReentrantLock里面的公平锁和非公平锁的父类Sync定义了可重入锁的释放锁机制。

// java.util.concurrent.locks.ReentrantLock.Sync// 方法返回当前锁是不是没有被线程持有
protected final boolean tryRelease(int releases) {// 减少可重入次数int c = getState() - releases;// 当前线程不是持有锁的线程,抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 如果持有线程全部释放,将当前独占锁所有线程设置为null,并更新stateif (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;
}

(3)unparkSuccessor方法:unpark唤醒AQS中的下一个线程

再看一下 unparkSuccessor方法

// java.util.concurrent.locks.AbstractQueuedSynchronizerprivate void unparkSuccessor(Node node) {// 获取头结点waitStatusint ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 获取当前节点的下一个节点Node s = node.next;// 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点if (s == null || s.waitStatus > 0) {s = null;// 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unparkif (s != null)LockSupport.unpark(s.thread);
}

为什么要从后往前找第一个非Cancelled的节点呢?原因如下。

之前的 addWaiter方法:

// java.util.concurrent.locks.AbstractQueuedSynchronizerprivate Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;
}

我们从这里可以看到,节点入队并不是原子操作,也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。

(4)解锁过程总结

综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。继续执行acquireQueued方法以后,中断如何处理?

【5】补充知识

(1)CANCELLED状态节点生成

(1)acquireQueued方法中的 Finally代码:

// java.util.concurrent.locks.AbstractQueuedSynchronizerfinal boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {...for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {...failed = false;...}...} finally {if (failed)cancelAcquire(node);}
}

(2)通过cancelAcquire方法,将Node的状态标记为CANCELLED。接下来,我们逐行来分析这个方法的原理:

// java.util.concurrent.locks.AbstractQueuedSynchronizerprivate void cancelAcquire(Node node) {// 将无效节点过滤if (node == null)return;// 设置该节点不关联任何线程,也就是虚节点node.thread = null;Node pred = node.prev;// 通过前驱节点,跳过取消状态的nodewhile (pred.waitStatus > 0)node.prev = pred = pred.prev;// 获取过滤后的前驱节点的后继节点Node predNext = pred.next;// 把当前node的状态设置为CANCELLEDnode.waitStatus = Node.CANCELLED;// 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点// 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为nullif (node == tail && compareAndSetTail(node, pred)) {compareAndSetNext(pred, predNext, null);} else {int ws;// 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功// 如果1和2中有一个为true,再判断当前节点的线程是否为null// 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {Node next = node.next;if (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next);} else {// 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点unparkSuccessor(node);}node.next = node; // help GC}
}

(3)当前的流程
获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。

根据当前节点的位置,考虑以下三种情况:
1-当前节点是尾节点。
2-当前节点是Head的后继节点。
3-当前节点不是Head的后继节点,也不是尾节点。

根据上述第二条,我们来分析每一种情况的流程。
当前节点是尾节点。

当前节点是Head的后继节点。
当前节点不是Head的后继节点,也不是尾节点。

通过上面的流程,我们对于CANCELLED节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作?

执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了),如果此时修改Prev指针,有可能会导致Prev指向另一个已经移除队列的Node,因此这块变化Prev指针不安全。 shouldParkAfterFailedAcquire方法中,会执行下面的代码,其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更Prev指针比较安全。

do {node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);

(2)中断恢复后的执行流程【中断线程】

唤醒后,会执行 return Thread.interrupted();,这个函数返回的是当前执行线程的中断状态,并清除。

// java.util.concurrent.locks.AbstractQueuedSynchronizerprivate final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}

再回到 acquireQueued代码,当 parkAndCheckInterrupt返回True或者False的时候,interrupted的值不同,但都会执行下次循环。如果这个时候获取锁成功,就会把当前 interrupted返回。

// java.util.concurrent.locks.AbstractQueuedSynchronizerfinal boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

如果acquireQueued为True,就会执行selfInterrupt方法。

// java.util.concurrent.locks.AbstractQueuedSynchronizerstatic void selfInterrupt() {Thread.currentThread().interrupt();
}

该方法其实是为了中断线程。但为什么获取了锁以后还要中断线程呢?这部分属于Java提供的协作式中断知识内容,感兴趣同学可以查阅一下。这里简单介绍一下:
1-当中断线程被唤醒时,并不知道被唤醒的原因,可能是当前线程在等待中被中断,也可能是释放了锁以后被唤醒。因此我们通过Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为False),并记录下来,如果发现该线程被中断过,就再中断一次。
2-线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。

这里的处理方式主要是运用线程池中基本运作单元Worder中的runWorker,通过Thread.interrupted()进行额外的判断处理,感兴趣的同学可以看下ThreadPoolExecutor源码。

【6】小结

Q:某个线程获取锁失败的后续流程是什么呢?
A:存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。
Q:既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
A:是CLH变体的FIFO双端队列。
Q:处于排队等候机制中的线程,什么时候可以有机会获取锁呢?
A:可以详细看下2.3.1.3小节。
Q:如果处于排队等候机制中的线程一直无法获取锁,需要一直等待么?还是有别的策略来解决这一问题?
A:线程所在节点的状态会变成取消状态,取消状态的节点会从队列中释放,具体可见2.3.2小节。
Q:Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢?
A:AQS的Acquire会调用tryAcquire方法,tryAcquire由各个自定义同步器实现,通过tryAcquire完成加锁过程。

【7】自定义同步工具

了解 AQS基本原理以后,按照上面所说的 AQS知识点,自己实现一个同步工具。

public class LeeLock  {private static class Sync extends AbstractQueuedSynchronizer {@Overrideprotected boolean tryAcquire (int arg) {return compareAndSetState(0, 1);}@Overrideprotected boolean tryRelease (int arg) {setState(0);return true;}@Overrideprotected boolean isHeldExclusively () {return getState() == 1;}}private Sync sync = new Sync();public void lock () {sync.acquire(1);}public void unlock () {sync.release(1);}
}

通过我们自己定义的Lock完成一定的同步功能。

public class LeeMain {static int count = 0;static LeeLock leeLock = new LeeLock();public static void main (String[] args) throws InterruptedException {Runnable runnable = new Runnable() {@Overridepublic void run () {try {leeLock.lock();for (int i = 0; i < 10000; i++) {count++;}} catch (Exception e) {e.printStackTrace();} finally {leeLock.unlock();}}};Thread thread1 = new Thread(runnable);Thread thread2 = new Thread(runnable);thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(count);}
}

上述代码每次运行结果都会是20000。通过简单的几行代码就能实现同步功能,这就是AQS的强大之处。

(5)lock()与unlock()实现原理【待处理】

可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。

可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。

公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。

CAS操作(CompareAndSwap)。CAS操作简单的说就是比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作。

ReentrantLock提供了两个构造器,分别是

public ReentrantLock() {sync = new NonfairSync();
}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

默认构造器初始化为NonfairSync对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。由lock()和unlock的源码可以看到,它们只是分别调用了sync对象的lock()和release(1)方法。

【NonfairSync】
首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。

“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。

若当前有三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到了else里面。我们往下看acquire。

1-第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。

tryAcquire(arg)
final boolean nonfairTryAcquire(int acquires) {//获取当前线程final Thread current = Thread.currentThread();//获取state变量值int c = getState();if (c == 0) { //没有线程占用锁if (compareAndSetState(0, acquires)) {//占用锁成功,设置独占线程为当前线程setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 更新state值为新的重入次数setState(nextc);return true;}//获取锁失败return false;
}

非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。

2-第二步,入队。由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁死死不放,那么B和C就会被挂起。
先看下入队的过程。先看addWaiter(Node.EXCLUSIVE)

/*** 将新节点和当前线程关联并且入队列* @param mode 独占/共享* @return 新节点*/
private Node addWaiter(Node mode) {//初始化节点,设置关联线程和模式(独占 or 共享)Node node = new Node(Thread.currentThread(), mode);// 获取尾节点引用Node pred = tail;// 尾节点不为空,说明队列已经初始化过if (pred != null) {node.prev = pred;// 设置新节点为尾节点if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点enq(node);return node;
}

B、C线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设同时走到了enq(node)里。

这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。

(五)什么情况下使用ReenTrantLock

(1)ReenTrantLock使用场景

(1)如果发现该操作已经在执行中则不再执行(有状态执行)
a、用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。
b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。

以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)

(2)如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)
主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是不公平的。

这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)

ReentrantLock默认情况下为不公平锁

不公平锁与公平锁的区别:
公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)
不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)

(3)如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)
等待获得锁的操作有一个时间的限制,如果超时则放弃执行。
用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。

(4)如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作

synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景2的另一种改进,没有超时,只能等待中断或执行完毕)

这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)

代码实例

// 以下是ReentrantLock中断机制的一个代码实现、如果换成synchronized就会出现死锁
import java.util.concurrent.*;
import java.util.concurrent.locks.*;  public class AttemptLocking {  private ReentrantLock lock = new ReentrantLock();  public void untimed() {  boolean captured = lock.tryLock();  try {  System.out.println("tryLock(): " + captured);  } finally {  if (captured)  lock.unlock();  }  }  public void timed() {  boolean captured = false;  try {  captured = lock.tryLock(2, TimeUnit.SECONDS);  } catch (InterruptedException e) {  throw new RuntimeException(e);  }  try {  System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);  } finally {  if (captured)  lock.unlock();  }  }  public static void main(String[] args) throws InterruptedException {  final AttemptLocking al = new AttemptLocking();  al.untimed(); // True -- 可以成功获得锁  al.timed(); // True --可以成功获得锁  //新创建一个线程获得锁并且不释放  new Thread() {  {  setDaemon(true);  }  public void run() {  al.lock.lock();  System.out.println("acquired");  }  }.start();  Thread.sleep(100);// 保证新线程能够先执行  al.untimed(); // False -- 马上中断放弃  al.timed(); // False -- 等两秒超时后中断放弃  }
}

(2)案例一:使用Lock把MyStack改造为线程安全的类并且实现生产者消费者问题

public class MyStack<T> {LinkedList<T> values = new LinkedList<T>();Lock lock = new ReentrantLock();Condition condition = lock.newCondition();public void push(T t) {try {lock.lock();while (values.size() >= 200) {try {condition.await();} catch (InterruptedException e) {e.printStackTrace();}}condition.signalAll();values.addLast(t);} finally {lock.unlock();}}public T pull() {T t=null;try {lock.lock();while (values.isEmpty()) {try {condition.await();} catch (InterruptedException e) {e.printStackTrace();}}condition.signalAll();t= values.removeLast();} finally {lock.unlock();}return t;}public T peek() {return values.getLast();}
}

(3)案例二:借助tryLock 解决死锁问题

public class TestThreadTrylockDeadlock {public static void main(String[] args) {final Hero ahri = new Hero("九尾妖狐");final Hero annie = new Hero("安妮");Lock lock_ahri = new ReentrantLock();Lock lock_annie = new ReentrantLock();Thread thread1 = new Thread() {public void run(){// 占有九尾妖狐boolean ahriLocked = false;boolean annieLocked = false;try {ahriLocked = lock_ahri.tryLock(10, TimeUnit.SECONDS);if (ahriLocked) {System.out.println("t1 已占有九尾妖狐");// 停顿1000秒,另一个线程有足够的时间占有安妮Thread.sleep(1000);System.out.println("t1 试图在10秒内占有安妮");try {annieLocked = lock_annie.tryLock(10, TimeUnit.SECONDS);if (annieLocked)System.out.println("t1 成功占有安妮,开始啪啪啪");else{System.out.println("t1 老是占用不了安妮,放弃");}} finally {if (annieLocked){System.out.println("t1 释放安妮");lock_annie.unlock();}}}} catch (InterruptedException e) {e.printStackTrace();} finally {if (ahriLocked){System.out.println("t1 释放九尾狐");lock_ahri.unlock();}}}};thread1.start();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}Thread thread2 = new Thread() {public void run() {boolean annieLocked = false;boolean ahriLocked = false;try {annieLocked = lock_annie.tryLock(10,TimeUnit.SECONDS);if(annieLocked){System.out.println("t2 已占有安妮");// 停顿1000秒,另一个线程有足够的时间占有安妮Thread.sleep(1000);System.out.println("t2 试图在10秒内占有九尾妖狐");}try {ahriLocked = lock_ahri.tryLock(10, TimeUnit.SECONDS);if (ahriLocked)System.out.println("t2 成功占有九尾妖狐,开始啪啪啪");else{System.out.println("t2 老是占用不了九尾妖狐,放弃");}} finally {if (ahriLocked){System.out.println("t2 释放九尾狐");lock_ahri.unlock();}}} catch (InterruptedException e) {e.printStackTrace();} finally {if (annieLocked){System.out.println("t2 释放安妮");lock_annie.unlock();}}}};thread2.start();}
}

(六)读写锁ReentrantReadWriteLock的原理

(1)什么情况下使用ReentrantReadWriteLock

如果只是要解决线程安全的问题,使用ReentrantLock就可以了,但是这个是独占锁,在同一时刻只能由一个线程可以获取这个锁,而实际使用中会出现写少读多的情况,显然这时的ReentrantLock就满足不了情况了,就可以使用ReentrantReadWriteLock,采用了读写分离的策略,允许多个线程可以同时获取读锁。

(2)ReentrantReadWriteLock的一些方法

(3)ReentrantReadWriteLock的优缺点

优点: 读读不能互斥,提升锁性能,减少线程竞争。
缺点:当读锁过多时候,写锁少,存在锁饥饿现象。

(4)ReentrantReadWriteLock使用详解

ReentrantReadWriteLock 也提供了公平和非公平锁,基于构造默认非公平锁, ReentrantReadWriteLock 读写锁内部也是基于AQS队列实现的。

//读写锁private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);//写锁private final static Lock writeLock = readWriteLock.writeLock();//读锁private final static Lock readLock = readWriteLock.readLock();private final static List<Long> longs = new ArrayList<>();public final static void main(String[] args) throws InterruptedException {//        new Thread(ReentrantReadWriteLockTest::write).start();
//        TimeUnit.SECONDS.sleep(1);
//        new Thread(ReentrantReadWriteLockTest::write).start();new Thread(ReentrantReadWriteLockTest::write).start();TimeUnit.SECONDS.sleep(1);new Thread(ReentrantReadWriteLockTest::read).start();new Thread(ReentrantReadWriteLockTest::read).start();}static void write() {try {writeLock.lock();TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName() + " write ");longs.add(System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();} finally {writeLock.unlock();}}static void read() {try {readLock.lock();TimeUnit.SECONDS.sleep(1);longs.forEach(x -> System.out.println(x));} catch (InterruptedException e) {e.printStackTrace();} finally {readLock.lock();}}

可以看到我们写了一条数据,两条数据同时打印出来,读读是不互斥的。

Thread-0 write
1648997092090
1648997092090

读写锁 存在一个问题:
当读锁比例很多,写锁很少,锁竞争情况下,写锁抢到锁的机会就回少,读锁数量太大的情况下,写锁不一定能抢到锁.

我们使用非公平锁,来测试,启动5个读锁,一个写锁。

//读写锁private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false);//写锁private final static Lock writeLock = readWriteLock.writeLock();//读锁private final static Lock readLock = readWriteLock.readLock();private final static List<Long> longs = new ArrayList<>();public final static void main(String[] args) throws InterruptedException {new Thread(ReentrantReadWriteLockTest2::write).start();TimeUnit.SECONDS.sleep(1);//new Thread(ReentrantReadWriteLockTest2::read).start();//new Thread(ReentrantReadWriteLockTest2::read).start();for (int i = 0; i <5; i++) {new Thread(ReentrantReadWriteLockTest2::read).start();}}static void write() {for (;;){try {writeLock.lock();TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName() + " write ");longs.add(System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();} finally {writeLock.unlock();}}}static void read() {for (;;){try {readLock.lock();TimeUnit.SECONDS.sleep(1);longs.forEach(x -> System.out.println(x));} catch (InterruptedException e) {e.printStackTrace();} finally {readLock.lock();}}}

当出现读比例特别大时候,ReentrantReadWriteLock锁就不适合了,此时JDK8之后提供的StampedLock锁更适合读写比例大的场景

(七)JDK8中新增的StampedLock锁的原理

【多线程】Lock和ReentrantLock使用和源码分析相关推荐

  1. 并发编程五:java并发线程池底层原理详解和源码分析

    文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...

  2. Google Mock(Gmock)简单使用和源码分析——源码分析

    源码分析 通过<Google Mock(Gmock)简单使用和源码分析--简单使用>中的例子,我们发现被mock的相关方法在mock类中已经被重新实现了,否则它们也不会按照我们的期待的行为 ...

  3. java.lang.ThreadLocal实现原理和源码分析

    java.lang.ThreadLocal实现原理和源码分析 1.ThreadLocal的原理:为每一个线程维护变量的副本.某个线程修改的只是自己的副本. 2.ThreadLocal是如何做到把变量变 ...

  4. 【原创】【专栏】《Linux设备驱动程序》--- LDD3源码目录结构和源码分析经典链接

    http://blog.csdn.net/geng823/article/details/37567557 [原创][专栏]<Linux设备驱动程序>--- LDD3源码目录结构和源码分析 ...

  5. java校验框架源码解析_Spring Boot原理剖析和源码分析

    Spring Boot原理剖析和源码分析 依赖管理 问题一:为什么导入dependency时不需要指定版本? spring-boot-starter-parent依赖 org.springframew ...

  6. SRS流媒体服务器——Forward集群搭建和源码分析

    SRS流媒体服务器--Forward集群搭建和源码分析 目录 Forward集群原理 RTMP流转发(Forward)部署实例 Forward集群源码分析 1. Forward集群原理 Forward ...

  7. Android RxJava(一) create操作符的用法和源码分析

    RxJava(一) create操作符的用法和源码分析 转载于:https://www.cnblogs.com/zhujiabin/p/7291901.html

  8. android lottie字体json,从json文件到炫酷动画-Lottie实现思路和源码分析

    从json文件到炫酷动画-Lottie实现思路和源码分析,Lottie是最近Airbnb开源的动画项目,支持Android.iOS.ReactNaitve三个平台,本文分析主要Lottie把json文 ...

  9. Nacos高级特性Raft算法以及原理和源码分析

    Nacos高级特性Raft算法以及原理和源码分析 对比springcloud-config配置中心 springcloud-config工作原理 Nacos的工作原理图 springcloud-con ...

  10. JAVA-LocalDateTime时间格式化,转换时间戳和源码分析

    JAVA-LocalDateTime时间格式化,转换时间戳和源码分析 LocalDateTime LocalDateTime作为java8新加的时间类型,也是后面开发中常用的时间类型.因为没用过之前的 ...

最新文章

  1. vue 父页面中的方法 调用_解决Vue中页面成功渲染数据undefined的问题
  2. 关于Verilog HDL的一些技巧、易错、易忘点(不定期更新)
  3. pycharm 调试程序时如何监控、监视变量?
  4. 什么是算术运算和逻辑运算_8086微处理器的算术和逻辑运算
  5. MySQL练习题和代码附录
  6. VScode配置C语言环境 亲测 可用!!!
  7. promise常见错误
  8. linux sysvinit 机制,Linux基于SysVinit和systemd实现开机自启动服务
  9. 设计模式-行为性模式(模板方法模式,策略模式,命令模式,责任链模式,观察者模式,中介者模式,迭代器模式)
  10. 【Drools】Drools使用入门(一)Drools上手教程(包括动态加载规则文件)
  11. tar命令打包并压缩指定的文件夹并且排除指定的文件
  12. 怎么在linux系统中输入日历,Linux命令行上如何使用日历详解
  13. Centos上卸载阿里云盾
  14. Excel 宏的用法的教程
  15. Unity高级知识点总结:性能优化与图形渲染进阶!
  16. 机房装饰实施方案的评审
  17. vfp计算机sql,浅析VFP与SQL的应用(计算机科学与技术毕业论文)
  18. mysql导入数据表
  19. 记手动迁移网站到Centos7、安装lnmp套装踩到的坑
  20. python实现对密码的加密和解密

热门文章

  1. 百度身份证认证一直失败
  2. php 视频 源码下载,PHP下载哔哩哔哩视频
  3. 《带您走进西藏》网课章节测验及答案
  4. 国内CDN加速的背景和现状
  5. 静态页面:html5个人博客模板《绅士》
  6. vue_elementui拖住滑块拼图验证代码
  7. 大学生试用期辞退之没有工钱//2021-1-22
  8. alps interview
  9. 简单快速将pdf转换成jpg的方法
  10. 只需3步让你电脑瞬间变快10倍