AQS
Lock的特性:
- 可重入
这个特性就是加了几次锁也要释放几次锁
synchronized也有可重入性 - 公平性与非公平性
实现锁的核心:
- CAS
保证加锁永远只有一个线程能够成功 - LockSupport
对线程阻塞和唤醒 - 自旋
cas加锁失败,则这些线程就要自旋,阻塞住 就不占用cpu资源 - queue
放阻塞的那些线程,用队列是因为队列的FIFO可以保证公平性
CAS
CAS Compare and swap
能够保证不管并发有多高,都能保证这个执行的原子性
通过cas算法去加锁,这样保证加锁永远只有一个线程能够成功
CAS 工作原理
主内存中有一个expect=0,如果两个线程都要去修改expect,则CAS就会让这两个线程都复制一份到自己线程中,然后再用另一个变量比如是refresh存修改后的值, 线程A和线程B就都去和主内存比较 如果expect的值相等,则将refresh中的值修改主内存的值,如果expect值不等,则不改主内存的值。
CAS的使用
Unsafe类中提供了三个关于CAS的方法:
线程阻塞就不会占用cpu的资源
通过java的LockSupport.part() 就可以阻塞线程
等到线程Aunlock()的时候 唤醒线程B 这样线程B就可以接着去执行 去加锁
LockSupport.park和unpark的使用
ReentrantLock
ReentrantLock定义在java.util.concurrent包下
java.util.concurrent包中很多功能就是基于AQS框架的
公平锁和非公平锁实现的特性就是通过 在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对 该抽象类的部分方法做了实现;
并且还定义了两个子类:
这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个 ReentrantLock同时具备公平与非公平特性。
- FairSync 公平锁的实现
- NonfairSync 非公平锁的实现
AQS源码分析
超类中有一个变量:记录当前获取锁的线程是谁
AQS类下的变量state状态器,表明当前同步器的状态
state为0表明是无锁状态,没有被任何一个线程持有
队列的创建:
AQS会基于Node构建一条队列,Node是AQS的内部类,
Node的重要属性:
- Node的prev和next属性用来形成双向链表。
- Node的thread属性用来保持对线程的引用
- Node的SHARED属性表示锁是共享锁,Semaphore锁是共享的
- Node的EXCLUSIVE属性表示锁是互斥的,ReentrantLock需要锁是互斥的
- Node的waitestate属性表示当前结点的生命状态(信号量)
- SIGNAL=-1 可被唤醒
- CANCELLED=1 代表出现异常,中断引起的,需要废弃结束
- CONDITION=-2 条件等待
- PROPAGATE=-3 传播
- 0 是初始状态Init状态
AQS的head属性会指向Node的头部,tail属性会指向Node的尾部
形成的双向队列:
公平锁:
FairSync的lock()方法调用acquire()方法
acquire()方法:
-
tryAcquire 尝试去获取锁:
- 通过Thread.currentThread()获取当前线程的引用
- getState() 获取同步器的状态
- c==0 无锁状态
-
对于公平锁,首先要判断是否有线程在排队
通过判断队列的队头队尾是否一样 -
没有线程排队,才用CAS去加锁,加锁其实就是把改同步器的状态为1
-
把当前线程的引用赋给exclusiveOwnerThread
-
- c!=0
- 第一种情况,这个锁是被当前线程持有的,则再对state++
Lock的可重入性就是通过这部分逻辑做到的 - 第二种情况,这个锁是被其他线程持有的,则返回false表示加锁失败
- 第一种情况,这个锁是被当前线程持有的,则再对state++
- c==0 无锁状态
-
addWaiter 线程入队
返回队尾的node
-
创建Node结点
入参是当前线程引用和mode是EXCLUSIVE互斥锁,这时默认waiteState即为0 -
enq(node)
-
t==null 则给队列初始化
构建队列要先给队列做初始化,即创建一个空结点,thread为null,队头head队尾tail同时指向这个创建好的空结点
-
t!=null 进行入队操作
入队也存在竞争,为了保证所有阻塞线程对象能够被唤醒即都能入队,所以要用CAS保证入队的原子性- 把prev指向t即队尾指向的node
- 用CAS的方式移动尾部指针
- 原来尾部即t的node的next指向当前入队的node
-
-
-
acquireQueued 阻塞
-
如果当前node是队列的第一个 则再通过tryAcquire尝试获取锁,尽可能避免线程被阻塞。
- 获取到锁了 节点就出队。 并且把head往后挪一个节点
通过setHead 把head指向当前node,并把当前node的thread、pred置位null,也就是变成了一个空结点 - 如果没有抢到锁 就阻塞
第一轮循环,通过shouldParkAfterFailedAcquire修改head的状态为-1即SIGNAL
第二轮循环,阻塞线程。-
shouldParkAfterFailedAcquire
取出前驱结点的状态waitStatus,当前结点能否被唤醒取决于前驱结点的状态
- 如果前驱结点的状态是signal ws==Node.SIGNAL 直接返回true代表是当前结点可唤醒的
- ws>0 代表前驱节点 出现异常要被cancelled
- ws是0或propagate,我们就通过CAS方式将前驱节点设置为可唤醒状态SIGNAL,即将ws设置为-1.
head的waitState设置为-1的原因:因为持有锁的线程T0在释放锁的时候,会去唤醒队列中排队的第一个线程T1,要判断head的waitState是否!=0。成立的话会把waitState改为0,然后把把T1被唤醒;T1接着走循环去抢锁,可能会再失败(在非公平锁场景下),就会再次被阻塞,head的节点就又经历两轮循环 waitState从0又变成-1.
-
parkAndCheckInterrupt 阻塞线程,并且需要判断线程是否是由中断信号唤醒的
调用LockSupport.park进行阻塞
-
- 获取到锁了 节点就出队。 并且把head往后挪一个节点
-
持有锁的这边逻辑
执行unlock()
release()
这里把-1又改成0的原因是,如果在非公平锁的情况下,当前线程被唤醒后有可能还是会抢不到锁,那这样就要保持是0的状态继续去执行阻塞的逻辑