并发编程集合

2023-11-03

--转载自郑金维老师 

一、synchronized

一、原子性、有序性、可见性

1.1 原子性

数据库的事务:ACID

A:原子性-事务是一个最小的执行的单位,一次事务的多次操作要么都成功,要么都失败。

并发编程的原子性:一个或多个指令在CPU执行过程中不允许中断的。

i++;操作是原子性?

肯定不是:i++操作一共有三个指令

getfield:从主内存拉取数据到CPU寄存器

iadd:在寄存器内部对数据进行+1

putfield:将CPU寄存器中的结果甩到主内存中

如何保证i++是原子性的。

锁:synchronized,lock,Atomic(CAS)

 

使用lock锁也会有类似的概念,也就是在操作i++的三个指令前,先基于AQS成功修改state后才可以操作

使用synchronized和lock锁时,可能会触发将线程挂起的操作,而这种操作会触发内核态和用户态的切换,从而导致消耗资源。

CAS方式就相对synchronized和lock锁的效率更高(也说不定),因为CAS不会触发线程挂起操作!

CAS:compare and swap

线程基于CAS修改数据的方式:先获取主内存数据,在修改之前,先比较数据是否一致,如果一致修改主内存数据,如果不一致,放弃这次修改

CAS就是比较和交换,而比较和交换是一个原子操作

 

 

CAS在Java层面就是Unsafe类中提供的一个native方法,这个方法只提供了CAS成功返回true,失败返回false,如果需要重试策略,自己实现!

CAS问题:

  • CAS只能对一个变量的修改实现原子性。
  • CAS存在ABA问题。
    • A线程修改主内存数据从1~2,卡在了获取1之后。
    • B线程修改主内存数据从1~2,完成。
    • C线程修改主内存数据从2~1,完成。
    • A线程执行CAS操作,发现主内存是1,没问题,直接修改
    • 解决方案:加版本号
  • 在CAS执行次数过多,但是依旧无法实现对数据的修改,CPU会一直调度这个线程,造成对CPU的性能损耗
    • synchronized的实现方式:CAS自旋一定次数后,如果还不成,挂起线程
    • LongAdder的实现方式:当CAS失败后,将操作的值,存储起来,后续一起添加

CAS:在多核情况下,有lock指令保证只有一个线程在执行当前CAS

1.2 有序性

指令在CPU调度执行时,CPU会为了提升执行效率,在不影响结果的前提下,对CPU指令进行重新排序。

如果不希望CPU对指定进行重排序,怎么办?

可以对属性追加volatile修饰,就不会对当前属性的操作进行指令重排序。

什么时候指令重排:满足happens-before原则,即可重排序

单例模式-DCL双重判断。

申请内存,初始化,关联是正常顺序,如果CPU对指令重排,可能会造成

申请内存,关联,初始化,在还没有初始化时,其他线程来获取数据,导致获取到的数据虽然有地址引用,但是内部的数据还没初始化,都是默认值,导致使用时,可能出现与预期不符的结果

1.3 可见性

可见性:前面说过CPU在处理时,需要将主内存数据甩到我的寄存机中再执行指令,指向完指令后,需要将寄存器数据扔回到主内存中。倒是寄存器数据同步到主内存是遵循MESI协议的,说人话就是,

不是每次操作结束就将CPU缓存数据同步到主内存。造成多个线程看到的数据不一样。

volatile每次操作后,立即同步数据到主内存。

synchronized,触发同步数据到主内存。

final,也可以解决可见性问题。

二、synchronized使用

2.1 synchronized的使用方式

synchronized方法

synchronized代码块

类锁和对象锁:

类锁:基础当前类的Class加锁

对象锁:基于this对象加锁

synchronized是互斥锁,每个线程获取synchronized时,基于synchronized绑定的对象去获取锁!

synchronized锁是基于对象实现的!

synchronized是如何基于对象实现的互斥锁,先了解对象再内存中是如何存储的。

  

三、synchronized锁升级

synchronized在jdk1.6之前,一直是重量级锁:只要线程获取锁资源失败,直接挂起线程(用户-内核)

在jdk1.6之前synchronized效率贼低,再加上Doug Lea推出了ReentrantLock,效率比synchronized快多了,导致JDK团队不得不在jdk1.6将synchronized做优化

锁升级:

  • 无锁状态、匿名偏向状态:没有线程拿锁。
  • 偏向锁状态:没有线程的竞争,只有一个线程再获取锁资源。 线程竞争锁资源时,发现当前synchronized没有线程占用锁资源,并且锁是偏向锁,使用CAS的方式,设置o的线程ID为当前线程,获取到锁资源,下次当前线程再次获取时,只需要判断是偏向锁,并且线程ID是当前线程ID即可,直接获得到锁资源。
  • 轻量级锁:偏向锁出现竞争时,会升级到轻量级锁(触发偏向锁撤销)。 轻量级锁的状态下,线程会基于CAS的方式,尝试获取锁资源,CAS的次数是基于自适应自旋锁实现的,JVM会自动的基于上一次获取锁是否成功,来决定这次获取锁资源要CAS多少次。
  • 重量级锁:轻量级锁CAS一段次数后,没有拿到锁资源,升级为重量级锁(其实CAS操作是在重量级锁时执行的)。 重量级锁就是线程拿不到锁,就挂起。

偏向锁是延迟开启的,并且在开启偏向锁之后,默认不存在无锁状态,只存在匿名偏向synchronized因为不存在从重量级锁降级到偏向或者是轻量。

synchronized在偏向锁升级到轻量锁时,会涉及到偏向锁撤销,需要等到一个安全点,stw,才可以撤销,并发偏向锁撤销比较消耗资源 在程序启动时,偏向锁有一个延迟开启的操作,因为项目启动时,ClassLoader会加载.class文件,这里会涉及到synchronized操作, 为了避免启动时,涉及到偏向锁撤销,导致启动效率变慢,所以程序启动时,默认不是开启偏向锁的。 如果在开启偏向锁的情况下,查看对象,默认对象是匿名偏向。

编译器优化的结果,出现了下列效果

锁消除:线程在执行一段synchronized代码块时,发现没有共享数据的操作,自动帮你把synchronized去掉。

锁膨胀:在一个多次循环的操作中频繁的获取和释放锁资源,synchronized在编译时,可能会优化到循环外部。

四、synchronized-ObjectMonitor

涉及ObjectMonitor一般是到达了重量级锁才会涉及到。

在到达重量级锁之后,重量级锁的指针会指向ObjectMonitor对象。

jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/runtime/objectMonitor.hpp

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;     // 抢占锁资源的线程个数
    _waiters      = 0,     // 调用wait的线程个数。
    _recursions   = 0;     // 可重入锁标记,
    _object       = NULL; 
    _owner        = NULL;  // 持有锁的线程
    _WaitSet      = NULL;  // wait的线程  (双向链表)
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;  // 假定的继承人(锁释放后,被唤醒的线程,有可能拿到锁资源)
    _cxq          = NULL ;  // 挂起线程存放的位置。(单向链表)
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // _cxq会在一定的机制下,将_cxq里的等待线程扔到当前_EntryList里。  (双向链表)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

偏向锁会降级到无锁状态嘛?怎么降?

会,当偏向锁状态下,获取当前对象的hashcode值,会因为对象头空间无法存储hashcode,导致降级到无锁状态。

 

二、ReentrantLock源码

一、ReentrantLock介绍

Java中提供锁,一般就是synchronized和lock锁,ReentrantLock是互斥锁,跟synchronized一样。

如果竞争比较激烈,推荐lock锁,效率更高

如果几乎没有竞争,推荐synchronized

原因:synchronized只有锁升级,当升级到重量级锁后,无法降级到轻量级、偏向锁。

lock锁的使用相对synchronized成本更高。

synchronized是非公平锁,lock是公平+非公平锁

lock锁提供的功能更完善,lock可以使用tryLock指定等待锁的时间

lock锁还提供了lockInterruptibly允许线程在获取锁的期间被中断。

synchronized基于对象实现,lock锁基于AQS+CAS实现

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    lock.lock();
    try{
        // 业务代码
    }finally{
        lock.unlock();
    }
}

二、ReentrantLock的lock方法源码

清楚lock方法是如何实现让当前线程获取到锁资源(什么效果算是拿到了锁资源)

非公平锁:上来先尝试将state从0修改为1,如果成功,代表获取锁资源。如果没有成功,调用acquire

公平锁:调用acquire

state是个什么鬼?

state是AQS中的一个由volatile修饰的int类型变量,多个线程会通过CAS的方式修改state,在并发情况下,只会有一个线程成功的修改state(从0~1)

如果线程修改state失败怎么办?

如果线程没有拿到锁资源,会到AQS的双向链表中排队等待(在期间,线程可能会挂起)

AQS的双向链表(队列)是个啥?

AQS中的双向链表是基于内部类Node在维护,Node中包含prev,next,thread属性,并且在AQS中还有两个属性,分别是head,tail。

画一下AQS的核心

 

公平&非公平的方法源码

// 公平锁的sync的lock方法
final void lock() {
    acquire(1);
}

// 非公平锁的sync的lock方法
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

三、ReentrantLock的acquire方法源码

acquire是一个业务方法,里面并没有实际的业务处理,都是在调用其他方法

// 核心acquire     arg = 1
public final void acquire(int arg) {
    //1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法。 没有拿到锁资源,
    //   需要执行&&后面的方法

    //2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象,
    //   并且插入到AQS的队列的末尾,并且作为tail

    //3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源
    //   如果没在前面,尝试将线程挂起,阻塞起来!
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

四、ReentrantLock的tryAcquire方法

tryAcquire分为公平和非公平两种、

tryAcquire主要做了两件事:

  • 如果state为0,尝试获取锁资源
  • 如果state不为0,看一下是不是锁重入操作

非公平:

// 非公平锁实现!
final boolean nonfairTryAcquire(int acquires) {
    // 拿到当前线程!
    final Thread current = Thread.currentThread();
    // 拿到AQS的state
    int c = getState();
    // 如果state == 0,说明没有线程占用着当前的锁资源
    if (c == 0) {
        // 没人占用锁资源,我直接抢一波(不管有没有线程在排队)
        if (compareAndSetState(0, acquires)) {
            // 将当前占用这个互斥锁的线程属性设置为当前线程
            setExclusiveOwnerThread(current);
            // 返回true,拿锁成功
            return true;
        }
    }
    // 当前state != 0,说明有线程占用着锁资源
    // 判断拿着锁的线程是不是当前线程(锁重入)
    else if (current == getExclusiveOwnerThread()) {
        // 将state再次+1
        int nextc = c + acquires;
        // 锁重入是否超过最大限制
        // 01111111 11111111 11111111 11111111   + 1
        // 10000000 00000000 00000000 00000000
        // 抛出error
        if (nextc < 0) 
            throw new Error("Maximum lock count exceeded");
        // 将值设置给state
        setState(nextc);
        // 返回true,拿锁成功
        return true;
    }
    return false;
}

公平锁:

// 公平锁实现
protected final boolean tryAcquire(int acquires) {
    // 拿到当前线程!
    final Thread current = Thread.currentThread();
    // 拿到AQS的state
    int c = getState();
    // 阿巴阿巴~~~~
    if (c == 0) {
        // 判断是否有线程在排队,如果有线程排队,返回true,配上前面的!,那会直接不执行返回最外层的false
        if (!hasQueuedPredecessors() &&
            // 如果没有线程排队,直接CAS尝试获取锁资源
            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;
    }
    return false;
}

五、ReentrantLock的addWaiter方法

在获取锁资源失败后,需要将当前线程封装为Node对象,并且插入到AQS队列的末尾

// 将当前线程封装为Node对象,并且插入到AQS队列的末尾
private Node addWaiter(Node mode) {
    // 将当前线程封装为Node对象,mode为null,代表互斥锁
    Node node = new Node(Thread.currentThread(), mode);
    // pred是tail节点
    Node pred = tail;
    // 如果pred不为null,有线程正在排队
    if (pred != null) {
        // 将当前节点的prev,指定tail尾节点
        node.prev = pred;
        // 以CAS的方式,将当前节点变为tail节点
        if (compareAndSetTail(pred, node)) {
            // 之前的tail的next指向当前节点
            pred.next = node;
            return node;
        }
    }
    // 添加的流程为,  自己prev指向、tail指向自己、前节点next指向我
    // 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列
    enq(node);
    return node;
}

// enq,无论怎样都添加进入
private Node enq(final Node node) {
    for (;;) {
        // 拿到tail
        Node t = tail;
        // 如果tail为null,说明当前没有Node在队列中
        if (t == null) { 
            // 创建一个新的Node作为head,并且将tail和head指向一个Node
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 和上述代码一致!
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

六、ReentrantLock的acquireQueued

acquireQueued方法会查看当前排队的Node是否是head的next,如果是,尝试获取锁资源,如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())

在挂起线程前,需要确认当前节点的上一个节点的状态必须是小于等于0,

如果为1,代表是取消的节点,不能挂起

如果为-1,代表挂起当前线程

如果为-2,-3,需要将状态改为-1之后,才能挂起当前线程

// acquireQueued方法
// 查看当前排队的Node是否是head的next,
// 如果是,尝试获取锁资源,
// 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
final boolean acquireQueued(final Node node, int arg) {
    // 标识。
    boolean failed = true;
    try {
        // 循环走起
        for (;;) {
            // 拿到上一个节点
            final Node p = node.predecessor();
            if (p == head && // 说明当前节点是head的next
                tryAcquire(arg)) { // 竞争锁资源,成功:true,失败:false
                // 进来说明拿到锁资源成功
                // 将当前节点置位head,thread和prev属性置位null
                setHead(node);
                // 帮助快速GC
                p.next = null; 
                // 设置获取锁资源成功
                failed = false;
                // 不管线程中断。
                return interrupted;
            }
            // 如果不是或者获取锁资源失败,尝试将线程挂起
            // 第一个事情,当前节点的上一个节点的状态正常!
            // 第二个事情,挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 通过LockSupport将当前线程挂起
                parkAndCheckInterrupt())
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 确保上一个节点状态是正确的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 拿到上一个节点的状态
    int ws = pred.waitStatus;
    // 如果上一个节点为 -1
    if (ws == Node.SIGNAL)
        // 返回true,挂起线程
        return true;
    // 如果上一个节点是取消状态
    if (ws > 0) {
        // 循环往前找,找到一个状态小于等于0的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 将小于等于0的节点状态该为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

七、ReentrantLock的unlock方法

释放锁资源:

  • 将state-1。
  • 如果state减为0了,唤醒在队列中排队的Node。(一定唤醒离head最近的)

释放锁不分公平和非公平,就一个方法。

// 真正释放锁资源的方法
public final boolean release(int arg) {
    // 核心的释放锁资源方法
    if (tryRelease(arg)) {
        // 释放锁资源释放干净了。  (state == 0)
        Node h = head;
        // 如果头节点不为null,并且头节点的状态不为0,唤醒排队的线程
        if (h != null && h.waitStatus != 0)、
            // 唤醒线程
            unparkSuccessor(h);
        return true;
    }
    // 释放锁成功,但是state != 0
    return false;
}
// 核心的释放锁资源方法
protected final boolean tryRelease(int releases) {
    // 获取state - 1
    int c = getState() - releases;
    // 如果释放锁的线程不是占用锁的线程,抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否成功的将锁资源释放利索 (state == 0)
    boolean free = false;
    if (c == 0) {
        // 锁资源释放干净。
        free = true;
        // 将占用锁资源的属性设置为null
        setExclusiveOwnerThread(null);
    }
    // 将state赋值
    setState(c);
    // 返回true,代表释放干净了
    return free;
}

// 唤醒节点
private void unparkSuccessor(Node node) {
    // 拿到头节点状态
    int ws = node.waitStatus;
    // 如果头节点状态小于0,换为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 拿到当前节点的next
    Node s = node.next;
    // 如果s == null ,或者s的状态为1
    if (s == null || s.waitStatus > 0) {
        // next节点不需要唤醒,需要唤醒next的next
        s = null;
        // 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 经过循环的获取,如果拿到状态正常的节点,并且不为null
    if (s != null)
        // 唤醒线程
        LockSupport.unpark(s.thread);
}

为什么唤醒线程时,为啥从尾部往前找,而不是从前往后找?

因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是将tail赋值给当前Node,最后才是能上一个节点的next指针,指向当前Node。

如果从前往后,通过next去找,可能会丢失某个节点,导致这个节点不会被唤醒~

如果从后往前找,肯定可以找到全部的节点。

三、ReentrantReadWriteLock读写锁源码

一、为什么要出现读写锁

因为ReentrantLock是互斥锁,如果有一个操作是读多写少,同时还需要保证线程安全,那么使用ReentrantLock会导致效率比较低。

因为多个线程在对同一个数据进行读操作时,也不会造成线程安全问题。

所以出现了ReentrantReadWriteLock锁:

读读操作是共享的。

写写操作是互斥的。

读写操作是互斥的。

写读操作是互斥的。

单个线程获取写锁后,再次获取读锁,可以拿到。(写读可重入)

单个线程获取读锁后,再次获取写锁,拿不到。(读写不可重入)

使用方式:

public class XxxTest {
    // 读写锁!
    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    // 写锁
    static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    // 读锁
    static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    public static void main(String[] args) throws InterruptedException {
        readLock.lock();
        try {
            System.out.println("拿到读锁!");
        } finally {
            readLock.unlock();
        }

        writeLock.lock();
        try {
            System.out.println("拿到写锁!");
        } finally {
            writeLock.unlock();
        }
    }
}

二、读写锁的核心思想

ReentrantReadWriteLock还是基于AQS实现的。很多功能的实现和ReentrantLock类似

还是基于AQS的state来确定当前线程是否拿到锁资源

state表示读锁:将state的高16位作为读锁的标识

state表示写锁:将state的低16位作为写锁的标识

锁重入问题:

  • 写锁重入怎么玩:因为写操作和其他操作是互斥的,代表同一时间,只有一个线程持有着写锁,只要锁重入,就对低位+1即可。而且锁重入的限制,从原来的2^31 - 1,变为了2 ^ 16 -1。变短了~~
  • 读锁重入怎么玩:读锁的重入不能仿照写锁的方式,因为写锁属于互斥锁,同一时间只会有一个线程持有写锁,但是读锁是共享锁,同一时间会有多个线程持有读锁。所以每个获取到读锁的线程,记录锁重入的方式都是基于自己的ThreadLocal存储锁重入次数。

读锁重入的时候就不操作state了?不对,每次锁重入还要修改state,只是记录当前线程锁重入的次数,需要基于ThreadLocal记录

00000000 00000000 00000000 00000000 : state

写锁:

00000000 00000000 00000000 00000001

写锁:

00000000 00000000 00000000 00000010

A读锁:拿不到,排队

00000000 00000000 00000000 00000010

写锁全部释放(唤醒)

00000000 00000000 00000000 00000000

A读锁:

00000000 00000001 00000000 00000000

B读锁:

00000000 00000010 00000000 00000000

B再次读锁:

00000000 00000011 00000000 00000000

每个读操作的线程,在获取读锁时,都需要开辟一个ThreadLocal。读写锁为了优化这个事情,做了两手操作:

  • 第一个拿到读锁的线程,不用ThreadLocal记录重入次数,在读写锁内有有一个firstRead记录重入次数
  • 还记录了最后一个拿到读锁的线程的重入次数,交给cachedHoldCounter属性标识,可以避免频繁的在锁重入时,从TL中获取

三、写锁的操作

3.1 写锁加锁-acquire

public final void acquire(int arg) {
    // 尝试获取锁资源(看一下,能否以CAS的方式将state 从0 ~ 1,改成功,拿锁成功)
    // 成功走人
    // 不成功执行下面方法
    if (!tryAcquire(arg) &&
        // addWaiter:将当前没按到锁资源的,封装成Node,排到AQS里
        // acquireQueued:当前排队的能否竞争锁资源,不能挂起线程阻塞
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

因为都是AQS的实现,主要看tryAcquire

// state,高16:读,低16:写
00000000 00000000 00000000 00000000

00000000 00000001 00000000 00000000 - SHARED_UNIT

00000000 00000000 11111111 11111111 - MAX_COUNT

00000000 00000000 11111111 11111111 - EXCLUSIVE_MASK
&
00000000 00000000 00000000 00000001 

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

// 只拿到表示读锁的高16位。
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
// 只拿到表示写锁的低16位。
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }


// 读写锁的写锁,获取流程
protected final boolean tryAcquire(int acquires) {
    // 拿到当前线程
    Thread current = Thread.currentThread();
    // 拿到state
    int c = getState();
    // 拿到了写锁的低16位标识w
    int w = exclusiveCount(c);
    // c != 0:要么有读操作拿着锁,要么有写操作拿着锁
    if (c != 0) {
        // 如果w == 0,代表没有写锁,拿不到!拜拜!
        // 如果w != 0,代表有写锁,看一下拿占用写锁是不是当前线程,如果不是,拿不到!拜拜!
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 到这,说明肯定是写锁,并且是当前线程持有
        // 判断对低位 + 1,是否会超过MAX_COUNT,超过抛Error
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 如果没超过锁重入次数, + 1,返回true,拿到锁资源。
        setState(c + acquires);
        return true;
    }
    // 到这,说明c == 0
    // 读写锁也分为公平锁和非公平锁
    // 公平:看下排队不,排队就不抢了
    // 走hasQueuedPredecessors方法,有排队的返回true,没排队的返回false
    // 非公平:直接抢!
    // 方法实现直接返回false
    if (writerShouldBlock() ||
        // 以CAS的方式,将state从0修改为 1
        !compareAndSetState(c, c + acquires))
        // 要么不让抢,要么CAS操作失败,返回false
        return false;
    // 将当前持有互斥锁的线程,设置为自己
    setExclusiveOwnerThread(current);
    return true;
}

剩下的addWaiter和acquireQueued和ReentrantLock看的一样,都是AQS自身提供的方法

3.2 写锁-释放锁操作

读写锁的释放操作,跟ReentrantLock一致,只是需要单独获取低16位,判断是否为0,为0就释放成功

// 写锁的释放锁
public final boolean release(int arg) {
    // 只有tryRealse是读写锁重新实现的方法,其他的和ReentrantLock一致
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// 读写锁的真正释放
protected final boolean tryRelease(int releases) {
    // 判断释放锁的线程是不是持有锁的线程
    if (!isHeldExclusively())
        // 不是抛异常
        throw new IllegalMonitorStateException();
    // 对state - 1
    int nextc = getState() - releases;
    // 拿着next从获取低16位的值,判断是否为0
    boolean free = exclusiveCount(nextc) == 0;
    // 返回true
    if (free)
        // 将持有互斥锁的线程信息置位null
        setExclusiveOwnerThread(null);
    // 将-1之后的nextc复制给state
    setState(nextc);
    return free;
}

四、读锁的操作

4.1 读锁的加锁操作

// 读锁加锁操作
public final void acquireShared(int arg) {
    // tryAcquireShared,尝试获取锁资源,获取到返回1,没获取到返回-1
    if (tryAcquireShared(arg) < 0)
        // doAcquireShared 前面没拿到锁,这边需要排队~
        doAcquireShared(arg);
}

// tryAcquireShared方法
protected final int tryAcquireShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 拿到state
    int c = getState();
    // 那写锁标识,如果 !=0,代表有写锁
    if (exclusiveCount(c) != 0 &&
        // 如果持有写锁的不是当前线程,排队去!
        getExclusiveOwnerThread() != current)
        // 排队!
        return -1;
    // 没有写锁!
    // 获取读锁信息
    int r = sharedCount(c);
    // 公平锁: 有人排队,返回true,直接拜拜,没人排队,返回false
    // 非公平锁:正常的逻辑是非公平直接抢,因为是读锁,每次抢占只要CAS成功,必然成功
    // 这就会出现问题,写操作无法在读锁的情况抢占资源,导致写线程饥饿,一致阻塞…………
    // 非公平锁会查看next是否是写锁的,如果是,返回true,如果不是返回false
    if (!readerShouldBlock() &&
        // 查看读锁是否已经达到了最大限制
        r < MAX_COUNT &&
        // 以CAS的方式,对state的高16位+1
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 拿到锁资源成功!!!
        if (r == 0) {
            // 第一个拿到锁资源的线程,用first存储
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 我是锁重入,我就是第一个拿到读锁的线程,直接对firstReaderHoldCount++记录重入的次数
            firstReaderHoldCount++;
        } else {
            // 不是第一个拿到锁资源的
            // 先拿到cachedHoldCounter,最后一个线程的重入次数
            HoldCounter rh = cachedHoldCounter;
            // rh == null: 我是第二个拿到读锁的!
            // 或者发现之前有最后一个来的,但是不我,将我设置为最后一个。
            if (rh == null || rh.tid != getThreadId(current))
                // 获取自己的重入次数,并赋值给cachedHoldCounter
                cachedHoldCounter = rh = readHolds.get();
            // 之前拿过,现在如果为0,赋值给TL
            else if (rh.count == 0)
                readHolds.set(rh);
            // 重入次数+1,
            // 第一个:可能是第一次拿
            // 第二个:可能是重入操作
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

// 通过tryAcquireShared没拿到锁资源,也没返回-1,就走这
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        // 拿state
        int c = getState();
        // 现在有互斥锁,不是自己,拜拜!
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;

        // 公平:有排队的,进入逻辑。   没排队的,过!
        // 非公平:head的next是写不,是,进入逻辑。   如果不是,过!
        } else if (readerShouldBlock()) {
            // 这里代码特别乱,因为这里的代码为了处理JDK1.5的内存泄漏问题,修改过~
            // 这个逻辑里不会让你拿到锁,做被阻塞前的准备
            if (firstReader == current) {
                // 什么都不做
            } else {
                if (rh == null) {
                    // 获取最后一个拿到读锁资源的
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        // 拿到我自己的记录重入次数的。
                        rh = readHolds.get();
                        // 如果我的次数是0,绝对不是重入操作!
                        if (rh.count == 0)
                            // 将我的TL中的值移除掉,不移除会造成内存泄漏
                            readHolds.remove();
                    }
                }
                // 如果我的次数是0,绝对不是重入操作!
                if (rh.count == 0)
                    // 返回-1,等待阻塞吧!
                    return -1;
            }
        }
        // 超过读锁的最大值了没?
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 到这,就CAS竞争锁资源
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 跟tryAcquireShared一模一样
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; 
            }
            return 1;
        }
    }
}

4.2 加锁-扔到队列准备阻塞操作

// 没拿到锁,准备挂起
private void doAcquireShared(int arg) {
    // 将当前线程封装为Node,当前Node为共享锁,并添加到队列的模式
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取上一个节点
            final Node p = node.predecessor();
            if (p == head) {
                // 如果我的上一个是head,尝试再次获取锁资源
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 如果r大于等于0,代表获取锁资源成功
                    // 唤醒AQS中我后面的要获取读锁的线程(SHARED模式的Node)
                    setHeadAndPropagate(node, r);
                    p.next = null; 
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 能否挂起当前线程,需要保证我前面Node的状态为-1,才能执行后面操作
            if (shouldParkAfterFailedAcquire(p, node) &&
                //LockSupport.park挂起~~
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

并发编程集合 的相关文章

随机推荐

  • VSCODE同步插件以及代码片段

    利用 share code 插件同步代码片段 利用 Settings Sync可以同步 VS code 配置 但它只能同步插件 利用 Settings Sync 再配合 share code 插件可以同步自定义代码片段 可以把 VS cod
  • BatchConfigTool批量配置工具

    海康批量配置工具BatchConfigTool是一款支持设备在线搜索 批量配置参数 批量升级等功能的软件 支持对大批量设备同时进行各参数的配置 极大的简化了操作过程 软件功能 1 对在线设备进行搜索 激活 修改设备的网络参数等 2 批量对设
  • hadoop 运行java 清洗数据 报错Failed to set permissions of path: \tmp\...

    清洗数据写好代码后 运行报错 ERROR org apache hadoop mapred TaskTracker Can not start task tracker because java io IOException Failed
  • 转载的关于 二级制的反码,补码,原码等,筛选过的.

    一 机器数和真值 在学习原码 反码和补码之前 需要先了解机器数和真值的概念 1 机器数 一个数在计算机中的二进制表示形式 叫做这个数的机器数 机器数是带符号的 在计算机用一个数的最高位存放符号 正数为0 负数为1 比如 十进制中的数 3 计
  • 关系数据库范式(1NF,2NF,3NF,BCNF,4NF,5NF)全解析

    1 范式的基本概念 设计关系数据库时 遵从不同的规范要求 设计出合理的关系型数据库 这些不同的规范要求被称为不同的范式 各种范式呈递次规范 越高的范式数据库冗余越小 没有冗余的数据库未必是最好的数据库 有时为了提高运行效率 就必须降低范式标
  • 【VHDL】分频器设计要求:25分频,占空比为50%

    VHDL 分频器设计要求 25分频 占空比为50 程序 LIBRARY IEEE USE IEEE STD LOGIC 1164 all entity DIV 25 IS PORT CLK IN STD LOGIC S1 S2 BUFFER
  • java引入包的关键字_java 包和导包关键字import

    包的概念 相当于 文件夹 person java package com jd public class person 注意该处为public 这样才能被访问 String name int age public person String
  • 用代码生成Glitch Art风格的抖音字体

    最近看到不少文章教大家用 photoshop 实现抖音的 logo 跟字体 我也非常喜欢这种风格的字体 于是趁着晚上的时间 动手用代码实现了下此类风格的字体特效 顺便开发了个小工具 地址见文末 本文主要是从 艺术手法 和 JS 前端 实现
  • UVA1347 Tour

    2021 5 22 刷题的时候突然看到手机推送 袁隆平院士逝世 心中一颤 后来得到辟谣 心情稍微放松几分 正在刷着辟谣的文章时 央视新闻发文 13点07分 袁隆平院士逝世 没过多久又看到吴孟超院士逝世的新闻 心情难以平复 特在本文的开头 向
  • 【华为OD机试】求满足条件的最长子串的长度(C++ Python Java)2023 B卷

    题目描述 给定一个字符串 只包含字母和数字 按要求找出字符串中的最长 连续 子串的长度 字符串本身是其最长的子串 子串要求 1 只包含1个字母 a z A Z 其余必须是数字 2 字母可以在子串中的任意位置 如果找不到满足要求的子串 如全是
  • 力扣17电话号码的字母组合(c++)

    力扣17电话号码的字母组合 c 力扣题目链接 思路 阅读完本题题目要求后 要解决如下3个问题 1 数字和字母如何映射 2 两个字母就两个for循环 三个字符我就三个for循环 以此类推 然后发现代码根本写不出来 3 输入1 按键等等异常情况
  • 从零开始搭建kafka开发环境

    Part1前言 最近选用kafka作为消息缓存 来低于大流量的数据 Kafka是一种高吞吐量的分布式发布订阅消息系统 有如下特性 通过O 1 的磁盘数据结构提供消息的持久化 这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能 高吞
  • 16位片内地址的I2C SLAVE接口设计

    8位片内地址的I2C SLAVE在OPENCORS org上面有 但是我没有找到16位的 我打算用B210的接EEPROM的I2C总线实现跟FPGA通讯就对照24C256的数据手册写了一个 以下代码2022 6 6更新已经实际运行通过 i2
  • YOLOv5——报错解决:UnicodeDecodeError: ‘utf-8‘ codec can‘t decode byte 0xb2 in position 6:invalidstartbyte

    在提示报错的torch utils py文件58行 将原来的decode 改成decode encoding gbk
  • 一文读懂伪回归、协整、格兰杰

    一 什么叫做伪回归 若是所建立的回归模型在经济意义上没有因果关系 那么这个就是伪回归 例如路边小树年增长率和国民经济年增长率之间存在很大的相关系数 但是建立的模型却是伪回归 如果你直接用数据回归 那肯定存在正相关 而其实这个是没有意义的回归
  • 【AI实战】最强开源 6B 中文大语言模型ChatGLM2-6B,从零开始搭建

    AI实战 最强开源 6B 中英文大语言模型ChatGLM2 6B 从零开始搭建 ChatGLM2 6B 简介 ChatGLM2 6B 评测结果 ChatGLM2 6B 搭建 参考 ChatGLM2 6B 简介 ChatGLM2 6B 是开源
  • 区块链相关概念与简介

    摘要 2017 2018年 互联网界最火热的话题之一就是区块链 各大公司相继宣布对区块链方面的投资和开发 各大互联网公司也前后推出自己区块链产品 例如阿里的麻吉宝 百度的莱次狗 腾讯的TrustSQL等等 那么区块链到底是什么 本文简单介绍
  • strace 命令来查看系统调用

    strace p pid strace p pid i i 显示函数地址
  • Android Service两种启动方式

    1 Context startService 方式启动 Context startService 方式的生命周期 启动时 startService gt onCreate gt onStart 可多次调用 Service running 停
  • 并发编程集合

    转载自郑金维老师 一 synchronized 一 原子性 有序性 可见性 1 1 原子性 数据库的事务 ACID A 原子性 事务是一个最小的执行的单位 一次事务的多次操作要么都成功 要么都失败 并发编程的原子性 一个或多个指令在CPU执