aqs原理初探以及公平锁和非公平锁实现

简介: aqs原理初探以及公平锁和非公平锁实现

一,AQS

AbstractQueuedSynchronizer,定义了一套多线程访问共享资源的同步器框架,依赖于状态的同步器

1,ReentrantLock

一种基于AQS框架的应用实现,类似于synchronized是一种互斥锁,可以保证线程安全。它支持手动加锁与解锁,支持加锁的公平性。主要是Lock锁的实现

public class ReentrantLock implements Lock, Serializable

接下来可以手动的猜想一下这个reentrantLock的实现

ReentrantLock lock = new ReentrantLock(true);
HashSet hashSet = new HashSet();
3个线程 T0,T1,T2
lock.lock(); //加锁
  while(true){ //循环,轮询获取锁
    if(加锁成功){ //synchronized,cas  cas:compare and swap
      break;//跳出循环
    }
    //Thread.yeild() //让出cpu使用权
    //Thread.sleep(1000); //睡眠
        hashSet.add(thread); //将当前线程加入到set中
    //阻塞
        LockSupprot.park();
  }
  T0获取锁
    xxxxx业务逻辑
    xxxxx业务逻辑   
lock.unlock();
Thread thread = hashSet.get();
//唤醒当前线程,notify是唤醒随机的线程
LockSupport.unPark(thread);

三大核心:自旋,加锁,LockSupport,队列(LinkQueue),为了解决这个公平锁和非公平锁,因此优先考虑这个队列。

2,CAS

compare and swap,比较与交换

如在jmm模型中,两个工作内存都去修改主内存的值。主内存中存在一个a = 0,线程A和线程B的工作内存同时获取到这个值,如果线程A先修改这个值,则线程A会和主内存比较,如果线程A的值a和主内存的值一致,那么就会直接进行修改,如改成a = 1,那么线程B也要改这个值,线程B中的a = 0,那么会和主内存a比较,发现不一致,主内存a=1,那么就会优先将线程B中的值修改成a = 1,再对a进行修改。就是说相等直接修改,不相等需要重新读取,再进行修改。即在一个原子操作里面进行比较和替换


主要通过这个unsafe类实现,里面的实现也是原子类操作,主要是通过以下三个类实现。

//对象类型
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
//整型值
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//Long型值
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

3,AbstractQueuedSynchronizer

该类是ReentrantLock里面的一个抽象内部类。ctrl + alt + shift + u看所有子类,Ctrl + T,看所有的继承类,可以发现很多地方都继承或者实现了这个抽象类

Sync是ReentrantLock的一个抽象的静态内部类,根据图也可以发现这个Sync继承了这个AQS

abstract static class Sync extends AbstractQueuedSynchronizer

通过实现Sync这个接口得到了FairSync公平锁类和NofairSync非公平锁这个类

3.1,FairSync

实现了公平锁,需要排队获取锁,如存在线程t1,t2,t3依次获取锁,需要依次排队执行,突然来了一个线程t4,也是需要排在线程t3后面

ReentrantLock reentrantLock = new ReentrantLock(true);
public ReentrantLock(boolean fair) {
    //入参,用于判断是公平锁还是非公平锁
    sync = fair ? new FairSync() : new NonfairSync();
}
//公平锁的实现
static final class FairSync extends ReentrantLock.Sync{...}

公平锁获取锁的方式如下

final void lock() {
    acquire(1);
}
public final void acquire(int arg) {
    //tryAcquire:尝试去获取锁
    //addWaiter:线程入队,一个同步等待队列,基于双向列表实现
    //链表中的每一个结点为一个Node结点,
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//线程入队操作
private Node addWaiter(Node mode) {
    //获取当前线程结点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //结点入队
    enq(node);
    return node;
}
//判断是否获取锁
protected final boolean tryAcquire(int acquires) {
    //获取当前获取锁的线程
    final Thread current = Thread.currentThread();
    //获取当前同步器状态,被volatile修饰的整型值,默认为0
    int c = getState();
    //如果同步状态器的值为0,说明外面的线程可以进行加锁操作
    if (c == 0) {
        //hasQueuedPredecessors:判断队列中是否还有在排队的节点
        //compareAndSetState:原子操作,比较与交换,进行加锁的操作,将state变量将0变为1
        //setExclusiveOwnerThread:设置获取锁的的线程拥有者
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果状态同步器不为0,可能由自己持有,也可能由别的线程持有锁
    //重复加锁,如定义一个全局锁,出现了这个可重入锁的问题
    else if (current == getExclusiveOwnerThread()) {
        //如果自己持有锁的话,state+1即可,反正不等于0就可以
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //别的线程获有锁,直接返回
    return false;
}

总而言之就是说,公平锁就是就是通过一个队列实现,需要进行排队的去获取锁资源。主要是通过这个state的资源状态器来控制获取锁的拥有者,如果state为0,则表示队列中的下一个线程可以去获取锁,并且通过cas的方式来保证锁的安全并发问题。通过队列的思想,来保证这个获取锁的公平性和有序性。

3.2,NofairSync

实现了非公平锁,默认为非公平锁,会存在抢锁的情况

ReentrantLock reentrantLock = new ReentrantLock(flase);
//如果不传入参数,默认是非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    //入参,用于判断是公平锁还是非公平锁
    sync = fair ? new FairSync() : new NonfairSync();
}
//非公平锁的实现
static final class NonfairSync extends ReentrantLock.Sync{...}

非公平锁获取锁的方法和公平锁类似,只是少了几步使用队列的几个方法

3.3,AQS中几个重要的相关参数

exclusiveOwnerThread:用于记录当前独占模式下,获取锁的线程是谁


state:同步状态器,默认为0,表示当前没有线程获取锁,外面的线程可以来获取锁了


Node:双向链表结构,是一个同步等待队列,head:队头,tail:队尾,prev前躯指针,next,后继指针


waiteState:结点的什生命状态


Lock锁和synchronized锁都是可重入锁

3.4,Node

pre:前驱指针

next:后继指针

waitStatues:每个结点都存在很多状态,这个主要是存储结点的生命状态

结点的几个生命状态如下

SIGNAL:-1     //可被唤醒
CANCELLED:1   //代表异常,中断引起的,需要被废弃
CONDITION:-2  //条件等待
PROPAGATE:-3  //传播
Init:0初始状态

结点入队顺序如下,入队时,将入队结点得前驱指针指向链表的tail结点,将tail节点的next节点指向当前节点,并将当前结点设置为tail尾指针结点。

private Node enq(final Node node) {
    //自旋
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            //如果队列为空,要防止出现多个线程的并发问题,结点直接放在队头
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            //保证这个入队的安全性,防止入队出现并发问题
            //结点入队到队尾
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

当前结点前面有结点获取锁,当前节点需要进行阻塞park,线程要开始排队等待。

结点在阻塞之前,还得尝试获取一次锁。

a,如果结点可以获取到锁,即当前节点为头结点的下一个结点,头结点即将锁被释放,则把当前结点作为头结点。之前的头结点就可以被GC回收了

b,如果结点不能获取到锁,那么当前结点就要等待被唤醒

(1),第一轮循环会去修改head状态,并且将waitState修改为sinal = -1可被唤醒状态

(2),第二轮,阻塞线程

final 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 GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

每个结点的生命状态的变换如下。默认的结点状态为0,需要将节点状态(waitState)转化为-1可唤醒状态。前一个节点中的waitStatus状态记录着后一个节点的生命状态。

//pred:前驱节点 node:当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //默认为初始状态0
    int ws = pred.waitStatus;
    //SIGNAL为-1,可被唤醒状态
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //将头结点初始状态转化为可唤醒状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

在修改成可被唤醒的状态之后,就进行阻塞操作。

private final boolean parkAndCheckInterrupt() {
    //阻塞线程,并且判断该线程是否是被中断的
    LockSupport.park(this);
    return Thread.interrupted();
}

在头结点释放锁的时候,也会发一个通知去告知下一个需要获取锁的线程来抢锁,即唤醒队列中的下一个被阻塞的线程

//真正的释放锁
public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    //尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        //因为前一个结点中存放后一个节点中的声明状态,因此
        //如果头结点的waitStatus不为0,就说明后一个结点是一个可被唤醒的状态
        if (h != null && h.waitStatus != 0)
            //唤醒
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//尝试去释放锁
protected final boolean tryRelease(int releases) {
    //修改同步状态器state
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //将同步状态器state 变为 0,说明当前同步状态器可以进行锁的获取了
    if (c == 0) {
        free = true;
        //置空当前线程
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

最后在这个unparkSuccessor方法中,也有这个具体的unpark唤醒操作

if (s != null)
    LockSupport.unpark(s.thread)

通过上述代码描述,也验证了一开始的猜想:自旋,加锁,LockSupport,队列(LinkQueue)

相关文章
|
5月前
|
消息中间件 设计模式 安全
深入理解AQS队列同步器原理-从ReentrantLock的非公平独占锁实现来看AQS的原理
深入理解AQS队列同步器原理-从ReentrantLock的非公平独占锁实现来看AQS的原理
|
6月前
|
存储 设计模式 安全
理解 AQS 和 ReentrantLock
在多线程编程中,同步机制是确保线程安全的关键。AQS(AbstractQueuedSynchronizer)和ReentrantLock是Java中两种常见的同步机制,它们各自具有不同的特性和适用场景。了解和掌握这两种机制对于编写高效、安全的并发程序至关重要。这篇文章将带你取了解和掌握这两种机制!另外值得一提的是:公平锁的实现与非公平锁是很像的,只不过在获取锁时不会直接尝试使用CAS来获取锁。只有当队列没节点并且state为0时才会去获取锁,不然都会把当前线程放到队列中。
170 1
|
Java C++
什么是AQS?
AQS(AbstractQueuedSynchronizer)是Java中的一个同步器框架
438 1
AQS(abstractQueuedSynchronizer)锁实现原理详解
AQS(abstractQueuedSynchronizer)抽象队列同步器。其本身是一个抽象类,提供lock锁的实现。聚合大量的锁机制实现的共用方法。
153 0
|
Java API 调度
图解ReentrantLock公平锁和非公平锁实现
图解ReentrantLock公平锁和非公平锁实现
141 0
图解ReentrantLock公平锁和非公平锁实现
ReentrantLock可重入锁、公平锁非公平锁区别与实现原理
ReentrantLock可重入锁、公平锁非公平锁区别与实现原理
|
算法 Java Linux
AQS这样学就很简单了
哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。
123 0
AQS这样学就很简单了
Java多线程 -- 公平锁和非公平锁的一些思考
在java的锁机制中,公平和非公平的参考物是什么,个人而言觉得是相对产生的结果而立,简单的来说,如果一个线程组里,能保证每个线程都能拿到锁,那么这个锁就是公平锁。
1359 0
|
测试技术 API
Juc并发编程04——可重入锁、公平锁与非公平锁
1.ReentrantLock使用介绍 之前我们一直使用的Lock实例都用的是ReentrantLock,实际上,这是一种可重入锁。简单来说,就是对同一个线程可以进行多次的加锁操作