java AQS 一:

时间:2023-03-08 17:47:27

最近加班太多,很久没有更新博客了,周末看了下阿里大神并发的书籍,把知识点做个记录。

一:线程安全的定义

当多个线程并发访问某个类时,如果不用考虑运营环境下的调度和交替运行,且不需要额外的辅助,这里认为这个类就是线程安全的。

原子操作的描述:一个操作单元要么全部成功,要么全部失败。后面再看看数据库中关于事务的ACID,这里A表示的就是原子特性,一个事务单元要么成功,要么失败(数据库中的单机事务依据的是undo),与I要做好区分。

二:关于指令重排序

JVM可以根据机器的特性(我觉得主要是cpu的多级缓存、多核处理),适当的重新排序机器指令,使机器指令更加符合cpu执行特点,发挥机器的最大性能。

三:Happens-Before

没啥好说的了,动作A\B的顺序关系。happens-before有一堆的乱七八糟规则。

四:volatile语义

volatile相当于synchronized的弱实现,说的通白点,就是volatile实现了后者的语义,但是没有后者的锁机制。

volatile不会被缓存在寄存器当中或者其他cpu不可见的地方,每次都是从主存中读取最新的结果值。

但是,但是,volatile并不能保证线程安全。

五:比较重要的概念,CAS

引入这个概念以前,看下我们用锁解决并发会导致的问题:

1.在高并发场景,加锁、释放锁会导致频繁的上下文切换,引发性能问题。(由于我做游戏服务器,之前测试机器人200同频战斗,io线程数量设置为cpu*2并没有cpu的执行效率高)。

2.一个线程持有锁,其他的线程被挂起。这里有可能某个优先级的高的线程等待优先级低的线程,从而导致优先级导致,会不会引发性能风险呢?

什么是独占锁,悲观锁,乐观锁?

独占锁也是悲观锁,synchronized就是悲观锁。反之,更有效的就是乐观锁,假设有冲突就重试,直到完成。

CAS:compare and swap

cas的三个操作数:内存值V,旧的预期值A,要修改的新值B,仅当A==V时,才把V改成B。

现代cpu提供了特殊的指令,可以自动更新共享的数据,并且能够检测到其他线程的干扰。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。

而compareAndSet利用JNI来完成CPU指令的操作。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。

CAS引起的ABA问题?

六:关于AQS概念的引入

号称J.U.C最复杂的一个类,我从网上找到一些,后面略过。。。

基本的思想是表现为一个同步器,支持下面两个操作:

获取锁:首先判断当前状态是否允许获取锁,如果是就获取锁,否则就阻塞操作或者获取失败,也就是说如果是独占锁就可能阻塞,如果是共享锁就可能失败。另外如果是阻塞线程,那么线程就需要进入阻塞队列。当状态位允许获取锁时就修改状态,并且如果进了队列就从队列中移除。

while(synchronization state does not allow acquire){

enqueue current thread if not already queued;

possibly block current thread;

}

dequeue current thread if it was queued;

释放锁:这个过程就是修改状态位,如果有线程因为状态位阻塞的话就唤醒队列中的一个或者更多线程。

update synchronization state;

if(state may permit a blocked thread to acquire)

unlock one or more queued threads;

要支持上面两个操作就必须有下面的条件:

  • 原子性操作同步器的状态位
  • 阻塞和唤醒线程
  • 一个有序的队列

状态位的原子操作

这里使用一个32位的整数来描述状态位,前面章节的原子操作的理论知识整好派上用场,在这里依然使用CAS操作来解决这个问题。事实上这里还有一个64位版本的同步器

(AbstractQueuedLongSynchronizer),这里暂且不谈。

阻塞和唤醒线程

标准的JAVA API里面是无法挂起(阻塞)一个线程,然后在将来某个时刻再唤醒它的。JDK 1.0的API里面有Thread.suspend和Thread.resume,并且一直延续了下来。但是这

些都是过时的API,而且也是不推荐的做法。

在JDK 5.0以后利用JNI在LockSupport类中实现了此特性。

LockSupport.park()
LockSupport.park(Object)
LockSupport.parkNanos(Object, long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(Object, long)
LockSupport.parkUntil(long)
LockSupport.unpark(Thread)

上面的API中park()是在当前线程中调用,导致线程阻塞,带参数的Object是挂起的对象,这样监视的时候就能够知道此线程是因为什么资源而阻塞的。由于park()立即返回,所以

通常情况下需要在循环中去检测竞争资源来决定是否进行下一次阻塞。park()返回的原因有三:

  • 其他某个线程调用将当前线程作为目标调用unpack。
  • 其他线程中断当前线程;
  • 该调用不合逻辑地(即毫无理由地)返回。

其实第三条就决定了需要循环检测了,类似于通常写的while(checkCondition()){Thread.sleep(time);}类似的功能。

AQS采用的CHL模型采用下面的算法完成FIFO的入队列和出队列过程。

对于入队列(enqueue):采用CAS操作,每次比较尾结点是否一致,然后插入的到尾结点中。

do {

pred = tail;

}while ( !compareAndSet(pred,tail,node) );

对于出队列(dequeue):由于每一个节点也缓存了一个状态,决定是否出队列,因此当不满足条件时就需要自旋等待,一旦满足条件就将头结点设置为下一个节点。

while (pred.status != RELEASED) ;

head  = node;

AQS里面有三个核心字段:

private volatile int state;

private transient volatile Node head;

private transient volatile Node tail;

其中state描述的有多少个线程取得了锁,对于互斥锁来说state<=1。head/tail加上CAS操作就构成了一个CHL的FIFO队列。下面是Node节点的属性。

volatile int waitStatus; 节点的等待状态,一个节点可能位于以下几种状态:

  • CANCELLED = 1: 节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。
  • SIGNAL = -1: 节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。
  • CONDITION = -2:表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。
  • 0: 正常状态,新生的非CONDITION节点都是此状态。
  • 非负值标识节点不需要被通知(唤醒)。

volatile Node prev;此节点的前一个节点。节点的waitStatus依赖于前一个节点的状态。

volatile Node next;此节点的后一个节点。后一个节点是否被唤醒(uppark())依赖于当前节点是否被释放。

volatile Thread thread;节点绑定的线程。

Node nextWaiter;下一个等待条件(Condition)的节点,由于Condition是独占模式,因此这里有一个简单的队列来描述Condition上的线程节点。