CAS

Compare and Swap先比较再交换,CAS是一种无锁优化算法,也可以说是乐观锁,它基于共享数据不会被修改的假设,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B,当且仅当预期值A和内存值V相同时,将内存值修改为B,否则什么都不做。

CAS的实现原理

1.在java层面,提供CAS的方法是在Unsafe类中使用native修饰的:

2.native是直接调用本地依赖库C++中的方法

3.在CAS底层,如果是多核的操作系统,需要追加一个lock指令,单核不需要加,因为cmpxchg是一行指令,不能再被拆分了

cmpxchg ,是汇编的指令,CPU硬件底层就支持 比较和交换 (cmpxchg),cmpxchg并不保证原子性的。(cmpxchg的操作是不能再拆分的指令)

所以才会出现判断CPU是否是多核,如果是多核就追加lock指令。

lock指令你可以理解为是CPU层面的锁,一般锁的粒度就是 缓存行 级别的锁,当然也有 总线锁 ,但是成本太高,CPU会根据情况选择。

CAS中存在的问题

1.ABA
对于普通数据类型ABA不一定是问题,因为他不会影像线程执行的最终结果。对于引用数据类型,可以指定版本号来解决,Java中的atomic包里提供了一个类AtomicStampedReference就是这样的。
2.自旋次数过长
如果CAS一直不成功,自旋次数很大,会给CPU带来非常大的开销,可以限制自旋次数,比如synchronized,CAS失败一定次数后,就会将线程挂起,避免占用过多的CPU资源。

AQS

AQS就是一个抽象队列同步器,abstract queued sychronizer。本质上就是一个抽象类,提供了一套可用于实现线程同步机制的框架,实现类只需要继承该类,并重写指定方法即可实现一套线程同步机制。比如JUC下的很多工具类都是基于AQS事项的,比如CountDownLatch,Semaphore,ReentrantLock,线程池等都用到了AQS。

原理:AQS维护了一个volatile int state变量和一个CLH队列,这个队列是一个双向链表,链表中的Node对象包装了线程信息,Node对象通过getState()、setState()和compareAndSetState()对state进行修改和访问从而来达到获取锁的目的,然后在AQS的内部类ConditionObject中也提供了一个Node对象组成的单项链表,他的作用是提供了类似于synchronized中wait和notify同等作用的await和
signal方法。获取到锁的Node对象调用await方法会将此节点加入到condition的单项链表中。

源码分析
AQS获取锁资源的方法有两个,acquire(),acquiredShared()
acquire() --- 独占模式获取锁

这里AQS使用了模板方法模式,tryAcuire()就是一个钩子方法。在AQS中,此方法会抛出UnsupportedOperationException,所以需要子类去实现。tryAcquire(arg)返回false,其实就是获取锁失败的情况。

在获取锁失败后,代码会执行到addWaiter()这个方法里,这个方法里有enq()这个函数,代码一起贴出来


addWaiter()的作用是获取锁失败的线程包装成一个Node对象插入到CLH队列中。插入的步骤是这样的,首先先将当前线程包装到一个Node对象中,然后拿到CLH队列当前维护的尾节点,这时候有两种情况,尾节点tail为空,也就是此时CLH队列中没有一个节点;尾节点不为空,CLH队列中已经有其他节点存在。先看看第二种情况,此时先将需要插入进去的节点的前置节点prev设置为当前尾节点,然后用CAS的方式去修改CLH队列的尾节点,如果失败代码会走到enq方法中,enq中有个死循环,直到设置成功后才会跳出。如果是第一种情况,会先设置一个空节点当作head,然后将此节点设置为tail。

在插入到CLH队列完成后,会将此Node对象作为acquireQueued()方法的参数,执行acquireQueued()方法,这个方法中有shouldParkAfterFailedAcquire(),parkAndCheckInterrupt()这两个方法,也一并贴出来

在acquireQueued()方法中,首先会拿到当前的节点的前驱节点,判断前驱节点等于CLH队列的head节点时,会再次尝试获取锁,获得成功后,会将当前节点设置为CLH队列的head节点,并将前驱节点的next指针置空,移除队列,然后返回,执行当前线程的代码。
当再次尝试获取锁失败或者当前驱节点不是CLH队列的head节点时,会进入到shouldParkAfterFailedAcquire()方法中,这个方法的作用是修改他的前驱节点的waitStatus为SIGNAL。成功后调用parkAndCheckInterrupt()方法,使用park()方法阻塞此线程,等待被唤醒。当前驱节点释放锁后会使用unPark()方法将中断阻塞,然后继续自旋尝试获取锁。

Q:为什么AQS使用双向链表?
A:因为AQS中存在取消节点的操作,节点被取消后,需要从链表中断开,只需要修改前后节点的next和prev指针即可,单项链表需要遍历这个链表才能完成此操作,比较浪费资源。
Q:为什么AQS会有一个空的head节点:
A:每个节点都需要设置前置节点的waitStatus为SIGNAL,否则自己将永远无法被唤醒,而第一个节点是没有前置节点的,所以需要创建一个虚拟节点。

各种JUC同步锁

ReentrantLock
trylock:尝试获取锁,如果获取不到,不会进入阻塞队列,而是跳过继续执行后面的代码
lockInterruptibly:获取某个锁时,如果不能获取到,在等待获取锁的时候,是可以通过interrupt()中断。
公平锁和非公平锁:公平锁和非公平锁的tryAcquire()实现上有一点不同,这个线程上来会先检查队列里有没有原来等着的,如果有的话他就先进队列里排队等别人先运行。

CountDownLatch(同步计数器)
使用await方法阻塞住某个线程,直到在new CountDownLatch(n)时设定的n个线程执行完毕后,才会被唤醒,每个线程执行完后会coutdown(倒数)一次。相比与join更加的灵活。

CyclicBarrier
CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作,使用await方法阻塞所有线程
Semaphore(信号量)
Semaphore控制一定数量的许可(permit)的方式,来达到限制通用资源访问的目的。例如:控制并发的线程数。通过AQS中的state属性进行的记录,获取许可是将该值进行减少,释放许可是将该值进行增加,当没有足够的许可时,线程会加入到阻塞队列中等待其他线程释放许可并唤醒。


MockingJay
7 声望3 粉丝