IT评测·应用市场-qidao123.com

标题: 百万架构师第四十八课:并发编程的原理(三)|JavaGuide [打印本页]

作者: 商道如狼道    时间: 2025-3-10 22:08
标题: 百万架构师第四十八课:并发编程的原理(三)|JavaGuide
原文链接
JavaGuide
并发编程的原理

目的:

J.U.C = java.util.concurrent
Lock 的使用

这是 JVM 层面提供的关键字。
​         JDK 层次有一个 java.util.concurrent 的工具包,属于 并发 在 JDK 层次的一种手段。这个手段也是对我们多线程在操作系统情况下一些控制的保证线程安全的方式。
同步锁

​        我们知道,锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源,在 Lock 接口出现之前,JAVA 应用程序只能依靠 synchronized 关键字来实现同步锁的功能,在 JAVA5 以后,增长了 JUC 的并发包且提供了 Lock 接口用来实现锁的功能,它提供了与 synchroinzed 关键字类似的同步功能,只是它比 synchronized 更灵活,能够显示的获取和释放锁。
Lock的初步使用

​         Lock 是一个接口,核心的两个方法 Lock 和 unlock ,它有很多的实现,比如 ReentrantLock 、 ReentrantReadWriteLock;
  1. public interface Lock {
  2.     void lock();
  3.     boolean tryLock();
  4.     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  5.     void unlock();
  6.     Condition newCondition();
  7. }
复制代码


fair  公平
ReentrantLock

​        重入锁,表示支持重新进入的锁,也就是说,假如当火线程 t1 通过调用 #lock 方法获取了锁之后,再次调用lock,是不会再壅闭去获取锁的,直接增长重试次数就行了。
  1. public class AtomicDemo {
  2.     private static int count=0;
  3.     static Lock lock=new ReentrantLock();
  4.     public static void inc(){
  5.         lock.lock();
  6.         try {
  7.             Thread.sleep(1);
  8.         } catch (InterruptedException e) {
  9.             e.printStackTrace();
  10.         }
  11.         count++;
  12.         lock.unlock();
  13.     }
  14.    
  15.     public static void main(String[] args) throws InterruptedException {
  16.         for(int i=0;i<1000;i++){
  17.             new Thread(()->{AtomicDemo.inc();}).start();;
  18.         }
  19.         Thread.sleep(3000);
  20.     }
  21. }
复制代码
ReentrantReadWriteLock

​        我们从前理解的锁,基本都是排他锁,也就是这些锁在同一时刻只允许一个线程进行访问,而读写所在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程都会被壅闭。读写锁维护了一对锁,一个读锁、一个写锁;一般情况下,读写锁的性能都会比排它锁好,因为大多数场景 读是多于写的 。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
  1. public class RWLockDemo {
  2.     // 排他锁
  3.     // 共享锁,在同一时刻可以有多个线程获得锁
  4.     // 读锁, 写锁
  5.     static Map<String, Object> cacheMap = new HashMap<>();
  6.     // 重入读写锁
  7.     static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  8.     static Lock read = readWriteLock.readLock();    // 读锁
  9.     static Lock write = readWriteLock.writeLock();  // 写锁
  10.     // 缓存的更新和读取的时候
  11.     public static final Object get(String key) {
  12.         read.lock(); // 读取的时候加上读锁
  13.         out.println("开始读取数据");
  14.         try {
  15.             return cacheMap.get(key);
  16.         } finally {
  17.             read.unlock();
  18.         }
  19.     }
  20.     public static final Object set(String key, Object value) {
  21.         write.lock();  // 每一次写数据,都需要先加上写锁
  22.         out.println("开始写数据");
  23.         try {
  24.             return cacheMap.put(key, value);
  25.         } finally {
  26.             write.unlock();
  27.         }
  28.     }
  29. }
复制代码
​        在这个案例中,通过hashmap来模拟了一个内存缓存,然后使用读写锁来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被壅闭,因为读操作不会影响执行结果。
​        在执行写操作时,线程必须要获取写锁,当已经有线程持有写锁的情况下,当火线程会被壅闭,只有当写锁释放以后,其他读写操作才华继续执行。使用读写锁提升读操作的并发性。以保证每次写操作对所有的读写操作的可见性。
Lock 和 synchronized 的简单对比

​        通过我们对 Lock 的使用以及对 synchronized 的了解,基本上可以对比出这两种锁的区别了。因为这个也是在口试过程中比较常见的问题。
Lock 可以实现公平锁、非公平锁; 而 synchronized 只有非公平锁
AQS

​         Lock 之所以能实现线程安全的锁,主要的核心是 AQS ( AbstractQueuedSynchronizer  ) ,  AbstractQueuedSynchronizer 提供了一个 FIFO 队列,可以看作是一个用来实现锁以及其他需要同步功能的框架。这里简称该类为 AQSAQS 的使用依靠继承来完成,子类通过继承 AQS 并实现所需的方法来管理同步状态。例如常见的 ReentrantLock ,CountDownLatch 等 AQS 的两种功能。
从使用上来说, AQS 的功能可以分为两种:独占和共享。
​        很显然,独占锁是一种悲观守旧的加锁策略,它限制了 读/读 辩说,假如某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不须要的并发性,因为读操作并不会影响数据的一致性。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
  1. public class LockDemo {
  2.     static Lock lock = new ReentrantLock();// 有公平重入锁和非公平重入锁
  3.     private static int count = 0;
  4.     public static void incr(){
  5.         try {
  6.             Thread.sleep(1);
  7.         } catch (InterruptedException e) {
  8.             e.printStackTrace();
  9.         }
  10.         lock.lock(); // 获得锁
  11.         count ++;
  12.         lock.unlock();
  13.     }
  14. }
复制代码
​        通过一个重入锁的方式实现一个锁的等待。JVM 层面是如何让一个锁等待的。
​         lock.lock 用到了 CXQ 以及我们的 EntryList ,通过队列的方式,让线程等待,等待之前,它用到了 CAS ,通过自旋的方式去实验获得锁。假如在指定时间内获得锁失败的话,它会去 park,最后当我们这个锁被释放的时候,他会从 EntryList 里边取出一个线程。再次去争夺锁。取出线程,叫醒一个线程 叫做 unpark 。
synchronized 来到了......
AQS的内部实现

​        同步器依靠内部的同步队列(一个 FIFO双向队列 )来完成同步状态的管理,当火线程获取同步状态失败时,同步器会将当火线程以及等待状态等信息构造成为一个节点( Node )并将其加入同步队列,同时会壅闭当火线程,当同步状态释放时,会把首节点中的线程叫醒,使其再次实验获取同步状态。
Node 的主要属性如下
  1. static final class Node {
  2.     int waitStatus; //表示节点的状态,包含cancelled(取消);condition 表示节点在等待condition也就是在condition队列中
  3.     Node prev; //前继节点
  4.     Node next; //后继节点
  5.     Node nextWaiter; //存储在condition队列中的后继节点
  6.     Thread thread; //当前线程
  7. }
复制代码
​         AQS 类底层的数据布局是使用双向链表,是队列的一种实现。包括一个 head 节点和一个 tail 节点,分别表示头结点和尾节点,此中头结点不存储 Thread ,仅保存 next 结点的引用。


​        当一个线程成功地获取了同步状态(大概锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于 CAS 的设置尾节点的方法: compareAndSetTail ( Node expect , Nodeupdate ) ,它需要通报当火线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

​        同步队列遵循 FIFO ,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会叫醒后继节点,而后继节点将会在获取同步状态成功时将本身设置为首节点。

​        设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的 next 引用即可
compareAndSet


java.util.concurrent.locks.AbstractQueuedSynchronizer
​         AQS 中,除了本身的链表布局以外,还有一个很关键的功能,就是 CAS ,这个是保证在多线程并发的情况下保证线程安全的前提下去把线程加入到 AQS 中的方法,可以简单理解为乐观锁
  1. private final boolean compareAndSetHead(Node update) {
  2.     return unsafe.compareAndSwapObject(this, headOffset, null, update);
  3. }
复制代码
​        这个方法里面,首先,用到了 unsafe 类,(Unsafe类是在 sun.misc 包下,不属于 JAVA 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty 、 Hadoop 、 Kafka 等; Unsafe 可认为是 JAVA 中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等) 。然后调用了 #compareAndSwapObject 这个方法。
  1. public final native boolean compareAndSwapObject(Object var1, long var2, Object var4,
  2.                                                  Object var5);
复制代码
​        这个是一个 native 方法,
​        第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值
​        整个方法的作用是假如当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,假如更新成功,则返回 true ,否则返回 false
​        这里传入了一个 headOffset ,这个 headOffset 是什么呢?在下面的代码中,通过 unsafe.objectFieldOffset

然后通过反射获取了 AQS 类中的成员变量,并且这个成员变量被 volatile 修饰的

unsafe.objectFieldOffset

​         headOffset 这个是指类中相应字段在该类的偏移量,在这里详细即是指 head 这个字段在 AQS 类的内存中相对于该类首地址的偏移量。
​        一个 JAVA 对象可以看成是一段内存,每个字段都得按照一定的序次放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移。用于在后面的 compareAndSwapObject 中,去根据偏移量找到对象在内存中的详细位置。
​        这个方法在 unsafe.cpp 文件中,代码如下:
  1. UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject
  2. obj, jlong offset, jobject e_h, jobject x_h))
  3.     UnsafeWrapper("Unsafe_CompareAndSwapObject");
  4. oop x = JNIHandles::resolve(x_h); // 新值
  5. oop e = JNIHandles::resolve(e_h); // 预期值
  6. oop p = JNIHandles::resolve(obj);
  7. HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);// 在内存中的具体位置
  8. oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true);// 调用了另一个方法,实际上就是通过cas操作来替换内存中的值是否成功
  9. jboolean success = (res == e); // 如果返回的res等于e,则判定满足compare条件(说明res应该为内存中的当前值),但实际上会有ABA的问题
  10. if (success) // success为true时,说明此时已经交换成功(调用的是最底层的cmpxchg指令)
  11.     update_barrier_set((void*)addr, x); // 每次Reference类型数据写操作时,都会产生一个WriteBarrier暂时中断操作,配合垃圾收集器
  12. return success;
  13. UNSAFE_END
复制代码
所以其实 #compareAndSet 这个方法,最终调用的是 unsafe 类的 #compareAndSwap ,这个指令会对内存中的共享数据做原子的读写操作。
很显然,这是一种乐观锁的实现思绪。
ReentrantLock的实现原理分析

​        之所以叫重入锁是因为同一个线程假如已经获得了锁,那么后续该线程调用lock方法时不需要再次获取锁,也就是不会壅闭;重入锁提供了两种实现,一种是非公平的重入锁,另一种是公平的重入锁。怎么理解公平和非公平呢?
​        假如在绝对时间上,先对锁进行获取的请求一定先被满意获得锁,那么这个锁就是公平锁,反之,就是不公平的。简单来说公平锁就是等待时间最长的线程最优先获取锁。
​        默认的情况下就是非公平锁。
非公平锁的实现流程时序图


源码分析

ReentrantLock.lock
  1. public void lock() {
  2.     sync.lock();
  3. }
复制代码
​        这个是获取锁的入口,调用了 sync.lock ;  sync 是一个实现了 AQS 的抽象类,这个类的主要作用是用来实现同步控制的,并且 sync 有两个实现,一个是 NonfairSync (非公平锁)、另一个是 FailSync (公平锁); 我们先来分析一下非公平锁的实现
NonfairSync.lock
  1. final void lock() {
  2.     if (compareAndSetState(0, 1)) //这是跟公平锁的主要区别,一上来就试探锁是否空闲,如果可以插队,则设置获得锁的线程为当前线程
  3.         //exclusiveOwnerThread属性是AQS从父类AbstractOwnableSynchronizer中继承的属性,用来保存当前占用同步状态的线程
  4.         setExclusiveOwnerThread(Thread.currentThread());
  5.     else
  6.         acquire(1); //尝试去获取锁
  7. }
复制代码
​         compareAndSetState,这个方法在前面提到过了,再简单讲解一下,通过 CAS 算法去改变 state 的值,而这个 state 是什么呢? 在 AQS 中存在一个变量 state ,对于ReentrantLock 来说,假如 state = 0 表示无锁状态、假如 state > 0 表示有锁状态。 ( state 在不同的锁里边表达的意思是不一样的 )
​        所以在这里,是表示当前的 state 假如等于 0 ,则替换为 1 ,假如替换成功表示获取锁成功了由于 ReentrantLock 是可重入锁,所以持有锁的线程可以多次加锁,经过判断加锁线程就是当前持有锁的线程时
(即 exclusiveOwnerThread==Thread.currentThread() ),即可加锁,每次加锁都会将 state 的值 +1 , state 等于几,就代表当前持有锁的线程加了几次锁;
​        解锁时每解一次锁就会将 state 减 1 , state 减到 0 后,锁就被释放掉,这时其他线程可以加锁;
AbstractQueuedSynchronizer.acquire

​        假如 CAS 操作未能成功,阐明 state 已经不为 0 ,此时继续 acquire(1) 操作, acquire 是AQS中的方法 当多个线程同时进入这个方法时,首先通过 CAS 去修改 state 的状态,假如修改成功表示竞争锁成功,竞争失败的, tryAcquire 会返回  false
  1. public final void acquire(int arg) {
  2.     if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  3.         selfInterrupt();
  4. }
复制代码
这个方法的主要作用是
NonfairSync.tryAcquire

​         tryAcquire 方法实验获取锁,假如成功就返回,假如不成功,则把当火线程和等待状态信息构造成一个Node节点,并将结点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功。
  1. protected final boolean tryAcquire(int acquires) {
  2.     return nonfairTryAcquire(acquires);
  3. }
复制代码
nofairTryAcquire

​        这里可以看出非公平锁的寄义,即获取锁并不会严格根据争用锁的先后序次决定。这里的实现逻辑类似 synchroized 关键字的偏向锁的做法,即可重入而不用进一步进行锁的竞争,也解释了 ReentrantLock 中 Reentrant 的意义。
  1. final boolean nonfairTryAcquire(int acquires) {
  2.     final Thread current = Thread.currentThread();
  3.     int c = getState(); //获取当前的状态,前面讲过,默认情况下是0表示无锁状态
  4.     if (c == 0) {
  5.         if (compareAndSetState(0, acquires)) { //通过cas来改变state状态的值,如果更新成功,表示获取锁成功,这个操作外部方法lock()就做过一次,这里再做只是为了再尝试一次,尽量以最简单的方式获取锁。
  6.                 setExclusiveOwnerThread(current);
  7.             return true;
  8.         }
  9.     }
  10.     else if (current == getExclusiveOwnerThread()) {//如果当前线程等于获取锁的线程,表示重入,直接累加重入次数
  11.             int nextc = c + acquires;
  12.         if (nextc < 0) // overflow 如果这个状态值越界,抛出异常;如果没有越界,则设置后返回true
  13.             throw new Error("Maximum lock count exceeded");
  14.         setState(nextc);
  15.         return true;
  16.     }
  17.     //如果状态不为0,且当前线程不是owner,则返回false。
  18.         return false; //获取锁失败,返回false
  19. }
复制代码
addWaiter

​        当前锁假如已经被其他线程锁持有,那么当火线程往复请求锁的时候,会进入这个方法,这个方法主要是把当火线程封装成 node ,添加到 AQS 的链表中
  1. private Node addWaiter(Node mode) {
  2.     Node node = new Node(Thread.currentThread(), mode); //创建一个独占的Node节点,mode为排他模式
  3.     // 尝试快速入队,如果失败则降级至full enq
  4.     Node pred = tail; // tail是AQS中表示同步队列队尾的属性,刚开始为null,所以进行enq(node)方法
  5.     if (pred != null) {
  6.         node.prev = pred;
  7.         if (compareAndSetTail(pred, node)) { // 防止有其他线程修改tail,使用CAS进行修改,如果失败则降级至full enq
  8.             pred.next = node; // 如果成功之后旧的tail的next指针再指向新的tail,成为双向链表
  9.             return node;
  10.         }
  11.     }
  12.     enq(node); // 如果队列为null或者CAS设置新的tail失败
  13.     return node;
  14. }
复制代码
enq

enq就是通过自旋操作把当前节点加入到队列中
  1. private Node enq(final Node node) {
  2.     for (;;) { //无效的循环,为什么采用for(;;),是因为它执行的指令少,不占用寄存器
  3.         Node t = tail;// 此时head, tail都为null
  4.         if (t == null) { // Must initialize// 如果tail为null则说明队列首次使用,需要进行初始化
  5.             if (compareAndSetHead(new Node()))// 设置头节点,如果失败则存在竞争,留至下一轮循环
  6.                 tail = head; // 用CAS的方式创建一个空的Node作为头结点,因为此时队列中只一个头结点,所以tail也指向head,第一次循环执行结束
  7.         } else {
  8.             //进行第二次循环时,tail不为null,进入else区域。将当前线程的Node结点的prev指向tail,然后使用CAS将tail指向Node
  9.                 //这部分代码和addWaiter代码一样,将当前节点添加到队列
  10.                 node.prev = t;
  11.             if (compareAndSetTail(t, node)) {
  12.                 t.next = node; //t此时指向tail,所以可以CAS成功,将tail重新指向CNode。此时t为更新前的tail的值,即指向空的头结点,t.next=node,就将头结点的后续结点指向Node,返回头结点。
  13.                     return t;
  14.             }
  15.         }
  16.     }
  17. }
复制代码
代码运行到这里, AQS 队列的布局就是这样一个表现。

acquireQueued

​         addWaiter 返回了插入的节点,作为 acquireQueued 方法的入参,这个方法主要用于争抢锁。
  1. final boolean acquireQueued(final Node node, int arg) {
  2.     boolean failed = true;
  3.     try {
  4.         boolean interrupted = false;
  5.         for (;;) {
  6.             final Node p = node.predecessor();// 获取prev节点,若为null即刻抛出NullPointException
  7.             if (p == head && tryAcquire(arg)) {// 如果前驱为head才有资格进行锁的抢夺
  8.                 setHead(node); // 获取锁成功后就不需要再进行同步操作了,获取锁成功的线程作为新的head节点
  9.                 //凡是head节点,head.thread与head.prev永远为null,但是head.next不为null
  10.                 p.next = null; // help GC
  11.                 failed = false; //获取锁成功
  12.                 return interrupted;
  13.             }
  14.             //如果获取锁失败,则根据节点的waitStatus决定是否需要挂起线程
  15.             if (shouldParkAfterFailedAcquire(p, node) &&
  16.                 parkAndCheckInterrupt())// 若前面为true,则执行挂起,待下次唤醒的时候检测中断的标志
  17.                 interrupted = true;
  18.         }
  19.     } finally {
  20.         if (failed) // 如果抛出异常则取消锁的获取,进行出队(sync queue)操作
  21.             cancelAcquire(node);
  22.     }
  23. }
复制代码
​        原来的 head 节点释放锁以后,会从队列中移除,原来 head 节点的 next 节点会成为 head 节点

shouldParkAfterFailedAcquire

​        从上面的分析可以看出,只有队列的第二个节点可以有机会争用锁,假如成功获取锁,则此节点晋升为头节点。对于第三个及以后的节点, if (p == head) 条件不建立,首先进行 shouldParkAfterFailedAcquire(p, node) 操作。
​         #shouldParkAfterFailedAcquire 方法是判断一个争用锁的线程是否应该被壅闭。它首先判断一个节点的前置节点的状态是否为 Node.SIGNAL ,假如是,是阐明此节点已经将状态设置-假如锁释放,则应当通知它,所以它可以安全地壅闭了,返回 true 。
  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  2.     int ws = pred.waitStatus; // 前继节点的状态
  3.     if (ws == Node.SIGNAL)//如果是SIGNAL状态,意味着当前线程需要被unpark唤醒
  4.         return true;
  5.     // 如果前节点的状态大于0,即为CANCELLED状态时,则会从前节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire时,返回true。这个操作实际是把队列中CANCELLED的节点剔除掉。
  6.     if (ws > 0) {// 如果前继节点是“取消”状态,则设置 “当前节点”的 “当前前继节点” 为 “‘原前继节点'的前继节点”。
  7.             do {
  8.                 node.prev = pred = pred.prev;
  9.             } while (pred.waitStatus > 0);
  10.         pred.next = node;
  11.     } else { // 如果前继节点为“0”或者“共享锁”状态,则设置前继节点为SIGNAL状态。
  12.         /*
  13. * waitStatus must be 0 or PROPAGATE. Indicate that we
  14. * need a signal, but don't park yet. Caller will need to
  15. * retry to make sure it cannot acquire before parking.
  16. */
  17.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  18.     }
  19.     return false;
  20. }
复制代码
解读:假如有t1,t2两个线程都加入到了链表中
img
假如head节点位置的线程一直持有锁,那么t1和t2就是挂起状态,而HEAD以及Thread1的awaitStatus都是 SIGNAL ,在多次实验获取锁失败以后,就会通过下面的方法进行挂起(这个地方就是制止了惊群效应,每个节点只需要关心上一个节点的状态即可)

parkAndCheckInterrupt

​        假如 shouldParkAfterFailedAcquire 返回了 true ,则执行:“ parkAndCheckInterrupt() ”方法,它是通过 LockSupport.park(this)将当火线程挂起到WATING状态,它需要等待一个中断、unpark方法来叫醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作
  1. private final boolean parkAndCheckInterrupt() {
  2.     LockSupport.park(this);// LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞
  3.     return Thread.interrupted();
  4. }
复制代码
ReentrantLock.unlock

加锁的过程分析完以后,再来分析一下释放锁的过程,调用 release 方法,这个方法里面做两件事。
  1. public final boolean release(int arg) {
  2.     if (tryRelease(arg)) {
  3.         Node h = head;
  4.         if (h != null && h.waitStatus != 0)
  5.             unparkSuccessor(h);
  6.         return true;
  7.     }
  8.     return false;
  9. }
复制代码
tryRelease

​        这个动作可以认为就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是 1 ),假如结果状态为 0 ,就将排它锁的 Owner 设置为 null ,以使得其他的线程有机会进行执行。 在排它锁中,加锁的时候状态会增长 1 (固然可以本身修改这个值),在解锁的时候减掉 1 ,同一个锁,在可以重入后,大概会被叠加为 2、3、4这些值,只有 unlock() 的次数与 lock() 的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true  。
  1. protected final boolean tryRelease(int releases) {
  2.     int c = getState() - releases; // 这里是将锁的数量减1
  3.     if (Thread.currentThread() != getExclusiveOwnerThread())// 如果释放的线程和获取锁的线程不是同一个,抛出非法监视器状态异常
  4.         throw new IllegalMonitorStateException();
  5.     boolean free = false;
  6.     if (c == 0) {
  7.         // 由于重入的关系,不是每次释放锁c都等于0,
  8.         // 直到最后一次释放锁时,才会把当前线程释放
  9.         free = true;
  10.         setExclusiveOwnerThread(null);
  11.     }
  12.     setState(c);
  13.     return free;
  14. }
复制代码
LockSupport

​         LockSupport 类是 Java6 引入的一个类,提供了基本的线程同步原语。LockSupport 实际上是调用了 Unsafe 类里的函数,归结到 Unsafe 里,只有两个函数:
  1. public native void unpark(Thread jthread);
  2. public native void park(boolean isAbsolute, long time);
复制代码
​         unpark 函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。
​        permit相当于0/1的开关,默认是0,调用一次 unpark 就加 1 酿成了 1 .调用一次 park 会消费 permit ,又会酿成 0 。 假如再调用一次 park 会壅闭,因为 permit 已经是 0 了。直到 permit 酿成 1 .这时调用 unpark 会把permit 设置为 1 .每个线程都有一个相关的 permit , permit 最多只有一个,重复调用unpark不会累积。
​        在使用 LockSupport 之前,我们对线程做同步,只能使用 wait 和 notify ,但是 wait 和 notify 其实不是很灵活,并且耦合性很高,调用notify必须要确保某个线程处于 wait 状态,而 park/unpark 模子真正解耦了线程之间的同步,先后序次没有没有直接关联,同时线程之间不再需要一个Object大概其他变量来存储状态,不再需要关心对方的状态。
总结

​        分析了独占式同步状态获取和释放过程后,做个简单的总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或克制自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg) 方法释放同步状态,然后叫醒头节点的后继节点。
公平锁和非公平锁的区别

​        锁的公平性是相对于获取锁的序次而言的,假如是一个公平锁,那么锁的获取序次就应该符合请求的绝对时间序次,也就是 FIFO 。 在上面分析的例子来说,只要 CAS 设置同步状态成功,则表示当火线程获取了锁,而公平锁则不一样,差异点有两个。
FairSync.tryAcquire
  1. final void** lock() {
  2.     acquire(1);
  3. }
复制代码
非公平锁在获取锁的时候,会先通过CAS进行抢占,而公平锁则不会
FairSync.tryAcquire
  1. protected final boolean* tryAcquire(int acquires) {
  2.     final Thread current = Thread.currentThread*();
  3.     int c = getState();
  4.     if (c == 0) {
  5.         if (!hasQueuedPredecessors() &&
  6.             compareAndSetState(0, acquires)) {
  7.             setExclusiveOwnerThread(current);
  8.             return true;
  9.         }
  10.     }
  11.     else if (current == getExclusiveOwnerThread()) {
  12.         int nextc = c + acquires;
  13.         if (nextc < 0)
  14.             throw new Error("Maximum lock count exceeded");
  15.         setState(nextc);
  16.         return true;
  17.     }
  18.     return false;
  19. }
复制代码
​        这个方法与 nonfairTryAcquire(int acquires) 比较,不同的地方在于判断条件多了 hasQueuedPredecessors() 方法,也就是加入了[同步队列中当前节点是否有前驱节点]的判断,假如该方法返回 true ,则表示有线程比当火线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才华继续获取锁。
Condition

​        通过前面的课程学习,我们知道任意一个Java对象,都拥有一组监视器方法(定义在 java.lang.Object上),主要包括 wait() 、 notify() 以及 notifyAll() 方法,这些方法与同步关键字配合,可以实现等待/通知模式  J.U.C 包提供了 Condition 来对锁进行精准控制, Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件( Condition ),只有满意条件时,线程才会被叫醒。
condition使用案例

ConditionWait
  1. @RequiredArgsConstructor
  2. public class ConditionWait extends Thread {
  3.     private final Lock lock;
  4.     private final Condition condition;
  5.     @Override
  6.     public void run() {
  7.         lock.lock();
  8.         try {
  9.             System.out.println("【" + Thread.currentThread().getName() + "】开始执行 condition.await()");
  10.             condition.await();  //
  11.             System.out.println("【" + Thread.currentThread().getName() + "】执行结束 condition.await()");
  12.         } catch (InterruptedException e) {
  13.             e.printStackTrace();
  14.         } finally {
  15.             lock.unlock();
  16.         }
  17.     }
  18. }
复制代码
ConditionSignal
  1. @RequiredArgsConstructor
  2. public class ConditionNotify extends Thread {
  3.     private final Lock lock;
  4.     private final Condition condition;
  5.     @Override
  6.     public void run() {
  7.         lock.lock();
  8.         try {
  9.             System.out.println("【" + Thread.currentThread().getName() + "】开始执行 condition.signal()");
  10.             condition.signal();  // signal 和 signalAll
  11.             System.out.println("【" + Thread.currentThread().getName() + "】执行结束 condition.signal()");
  12.         } finally {
  13.             lock.unlock();
  14.         }
  15.     }
  16. }
复制代码
​        通过这个案例简单实现了 wait 和 notify 的功能,当调用 await 方法后,当火线程会释放锁并等待,而其他线程调用 condition 对象的 signal 大概 signalall 方法通知被壅闭的线程,然后本身执行 unlock 释放锁,被叫醒的线程获得之前的锁继续执行,最后释放锁。
所以,condition 中两个最重要的方法,一个是 await ,一个是 signal 方法
  1. public class ConnditionDemo {
  2.     public static void main(String[] args) {
  3.         Lock lock = new ReentrantLock();
  4.         Condition condition = lock.newCondition();
  5.         ConditionWait conditionWait = new ConditionWait(lock, condition);
  6.         conditionWait.start();
  7.         ConditionNotify conditionNotify = new ConditionNotify(lock, condition);
  8.         conditionNotify.start();
  9.     }
  10. }
复制代码
  1. 【Thread-0】开始执行 condition.await()
  2. 【Thread-1】开始执行 condition.signal()
  3. 【Thread-1】执行结束 condition.signal()
  4. 【Thread-0】执行结束 condition.await()
复制代码
await 方法

​        调用 Condition 的 await() 方法(大概以 await 开头的方法),会使当火线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当火线程一定获取了 Condition 相关联的锁。
  1. public final void await() throws InterruptedException {
  2.     if (Thread.interrupted())
  3.         throw new InterruptedException();
  4.     Node node = addConditionWaiter(); //创建一个新的节点,节点状态为condition,采用的数据结构仍然是链表
  5.     int savedState = fullyRelease(node); //释放当前的锁,得到锁的状态,并唤醒AQS队列中的一个线程
  6.     int interruptMode = 0;
  7.     //如果当前节点没有在同步队列上,即还没有被signal,则将当前线程阻塞
  8.     //isOnSyncQueue 判断当前 node 状态,如果是 CONDITION 状态,或者不在队列上了,就继续阻塞,还在队列上且不是 CONDITION 状态了,就结束循环和阻塞
  9.     while (!isOnSyncQueue(node)) {//第一次判断的是false,因为前面已经释放锁了
  10.         LockSupport.park(this); // 第一次总是 park 自己,开始阻塞等待
  11.         // 线程判断自己在等待过程中是否被中断了,如果没有中断,则再次循环,会在 isOnSyncQueue 中判断自己是否在队列上.
  12.         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  13.             break;
  14.     }
  15.     // 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了.
  16.     // interruptMode != THROW_IE -> 表示这个线程没有成功将 node 入队,但 signal 执行了 enq 方法让其入队了.
  17.     // 将这个变量设置成 REINTERRUPT.
  18.     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
  19.         interruptMode = REINTERRUPT;
  20.     // 如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.
  21.     // 如果是 null ,就没有什么好清理的了.
  22.     if (node.nextWaiter != null) // clean up if cancelled
  23.         unlinkCancelledWaiters();
  24.     // 如果线程被中断了,需要抛出异常.或者什么都不做
  25.     if (interruptMode != 0)
  26.         reportInterruptAfterWait(interruptMode);
  27. }
复制代码
signal

​        调用 Condition 的 signal() 方法,将会叫醒在等待队列中等待时间最长的节点(首节点),在叫醒节点之前,会将节点移到同步队列中
  1. public final void signal() {
  2.     if (!isHeldExclusively()) //先判断当前线程是否获得了锁
  3.         throw new IllegalMonitorStateException();
  4.     Node first = firstWaiter; // 拿到 Condition 队列上第一个节点
  5.     if (first != null)
  6.         doSignal(first);
  7. }
复制代码
  1. private void doSignal(Node first) {
  2.     do {
  3.         if ( (firstWaiter = first.nextWaiter) == null)// 如果第一个节点的下一个节点是 null,那么, 最后一个节点也是 null.
  4.             lastWaiter = null; // 将 next 节点设置成 null
  5.         first.nextWaiter = null;
  6.     } while (!transferForSignal(first) &&
  7.              (first = firstWaiter) != null);
  8. }
复制代码
​        该方法先是 CAS 修改了节点状态,假如成功,就将这个节点放到 AQS 队列中,然后叫醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒。
  1. final boolean transferForSignal(Node node) {
  2.     if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
  3.         return false;
  4.     Node p = enq(node);
  5.     int ws = p.waitStatus;
  6.     // 如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL 失败了(SIGNAL 表示: 他的
  7.     next 节点需要停止阻塞),
  8.     if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
  9.         LockSupport.unpark(node.thread); // 唤醒输入节点上的线程.
  10.     return true;
  11. }
复制代码
读写锁与 synchronized 在只读的线程中需要泯灭大量的时间的时候的性能对比,本质在于当读操作时,不进行壅闭
  1. // [readWrite] 9999次读操作{每次读操作 睡眠 1 ms},1次 " +1" 操作以后, 结果为:1[耗时]:665
  2. // [synchronized]  9999次读操作{每次读操作 睡眠 1 ms},1次 " +1" 操作以后, 结果为:1[耗时]:19007
复制代码
  1. public class TestperformanceDemo {
  2.     static Integer demoInteger = 0;
  3.     // 重入读写锁
  4.     static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  5.     static Lock read = readWriteLock.readLock();    // 读锁
  6.     static Lock write = readWriteLock.writeLock();  // 写锁
  7.     public static void main(String[] args) {
  8.         int countSum = 10000;
  9.         add1000Seconds(countSum);
  10.         add1000synchronized(countSum);
  11.         // [readWrite] 9999次读操作{每次读操作 睡眠 1 ms},1次 " +1" 操作以后, 结果为:1[耗时]:665
  12.         // [synchronized]  9999次读操作{每次读操作 睡眠 1 ms},1次 " +1" 操作以后, 结果为:1[耗时]:19007
  13.     }
  14.     /***
  15.      * 第二种方式
  16.      */
  17.     public static void add1000Seconds(int countSum) {
  18.         demoInteger = 0;
  19.         ExecutorService executorService = Executors.newFixedThreadPool(countSum);
  20.         long start = System.currentTimeMillis();
  21.         CompletionService completionService = new ExecutorCompletionService(executorService);
  22.         for (int i = 0; i < countSum; i++) {
  23.             int finalI = i;
  24.             completionService.submit(() -> {
  25.                 if (finalI == 0) {
  26.                      write.lock();
  27.                     try {
  28.                         demoInteger++;
  29.                     } finally {
  30.                         write.unlock();
  31.                     }
  32.                 } else {
  33.                     read.lock();
  34.                     try {
  35.                         Integer value = demoInteger;
  36.                         Thread.sleep(1);
  37.                     } catch (InterruptedException e) {
  38.                         e.printStackTrace();
  39.                     } finally {
  40.                         read.unlock();
  41.                     }
  42.                 }
  43.             }, null);
  44.         }
  45.         int count = 0;
  46.         while (count < countSum) { // 等待任务完成全部
  47.             if (completionService.poll() != null) {
  48.                 count++;
  49.             }
  50.         }
  51.         long end = System.currentTimeMillis();
  52.         System.out.println("[readWrite] " + (countSum - 1) + "次读操作{每次读操作 睡眠 1 ms},"
  53.                            + 1 + "次 " +1" 操作以后, 结果为:" + demoInteger
  54.                            + "[耗时]:" + (end - start));
  55.     }
  56.     /***
  57.      * synchronized
  58.      */
  59.     public static void add1000synchronized(int countSum) {
  60.         demoInteger = 0;
  61.         ExecutorService executorService = Executors.newFixedThreadPool(countSum);
  62.         long start = System.currentTimeMillis();
  63.         CompletionService completionService = new ExecutorCompletionService(executorService);
  64.         for (int i = 0; i < countSum; i++) {
  65.             int finalI = i;
  66.             completionService.submit(() -> {
  67.                 synchronized (RWLockDemo.class) {
  68.                     if (finalI == 0) {
  69.                         demoInteger++;
  70.                     } else {
  71.                         try {
  72.                             Thread.sleep(1);
  73.                         } catch (InterruptedException e) {
  74.                             e.printStackTrace();
  75.                         }
  76.                         Integer value = demoInteger;
  77.                     }
  78.                 }
  79.             }, null);
  80.         }
  81.         int count = 0;
  82.         while (count < countSum) { // 等待任务完成全部
  83.             if (completionService.poll() != null) {
  84.                 count++;
  85.             }
  86.         }
  87.         long end = System.currentTimeMillis();
  88.         System.out.println("[synchronized]  " + (countSum - 1) + "次读操作{每次读操作 睡眠 1 ms},"
  89.                            + 1 + "次 " +1" 操作以后, 结果为:" + demoInteger
  90.                            + "[耗时]:" + (end - start));
  91.     }
  92. }
复制代码
JavaGuide
来源于:  https://javaguide.net
微信公众号:不止极客

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4