1、AQS
-
AQS(AbstractQueuedSynchronizer)可以理解为抽象队列同步器,他定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。
-
AQS核心要点:State变量 + CLH队列 + CAS
-
AQS 使用一个volatile的int类型的成员变量来表示同步状态,通过CAS完成对State值的修改。
-
CLH 双端Node队列将要去抢占资源的线程封装成一个Node节点来实现锁的分配。
-
AQS核心思想:如果同步状态未被锁定(即state = 0),那么就将持有操作权线程指向当前线程,同步状态修改为锁定状态(即state = 1);如果同步状态为锁定状态,将暂时获取不到操作权的线程加入到CLH队列中等待唤醒。
-
独占模式
-
共享模式
CLH队列
- 重要方法和属性值的含义
方法和属性值 | 含义 |
---|---|
waitStatus | 当前节点在队列中的状态 |
thread | 表示处于该节点的线程 |
prev | 前驱指针 |
predecessor | 返回前驱节点,没有的话抛出 npe |
next | 后继指针 |
- waitStatus枚举值:
枚举 | 含义 |
---|---|
0 | 当一个 Node 被初始化的时候的默认值 |
CANCELLED | 为 1,表示线程获取锁的请求已经取消了 |
CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 |
PROPAGATE | 为-3,当前线程处在 SHARED 情况下,该字段才会使用 |
SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 |
ReentrantLock
-
实现原理:volatile 变量 + CAS设置值 + AQS(状态值state+CLH双向队列)
-
实现步骤:
- 如果是非公平锁,则会先通过CAS尝试获取锁资源,如果获取成功就把锁的持有者设置为当前线程。
- 未竞争到锁的线程将会被进入acquire方法。该方法内有三个重要方法:
- tyAcquire(arg )尝试获取锁,该方法是AQS的方法,如果子类不重写,那么就抛出异常,因此子类必须重写;
- addWaiter(Node.EXCLUSIVE) 将当前线程封装为Node节点添加到CLH队列中;
- acquireQueued(node, arg) 在CLH队列中尝试获取锁;
- tyAcquire尝试再次获取锁,获取失败判断是否是锁持有者是否是当前线程,如果是就可重
入。 - 未竞争到锁的线程将会被CAS构成一个链表结点加入队列并且被挂起。
- 竞争到锁的线程执行完后释放锁并且将唤醒链表中的下一个节点。
- 被唤醒的节点将从被挂起的地方继续执行逻辑。
-
默认创建非公平锁
- 公平锁和非公平锁都继承Sync内部类,Sync类继承AbstractQueuesynchronizer。
- 唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法.
Condition
- 可以理解为条件等待队列
- 当一个线程在调用了await方法以后,直到线程等待的某个条件成立才会被唤醒;
- 为线程提供了更加简单的等待/通知模式,必须要配合锁一起使用;
- Condition是AQS的内部类,每个Condition对象都包含一个FIFO队列,队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态;
- 当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列;
- 节点引用更新本来就是在获取锁以后的操作,所以不需要CAS保证。同时也是线程安全的操作;
- 调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到同步队列中;
ReentrantReadWriteLock
- ReentrantReadWriteLock 为 读写锁;
ReadWriteLock管理一组锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁 - 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
- 线程进入读锁的前提条件:
- 没有其他线程的写锁。
- 有写请求,但调用线程和持有锁的线程是同一个。
- 线程进入写锁的前提条件:
- 没有任意线程的读锁
- 没有其他线程的写锁
- ReadLock 和 WriteLock 方法都是通过调用Sync的方法实现的;
- AQS 的状态 **state 是32位(int 类型)的,分成两份,读锁用高16位,表示持有读锁的线程数(sharedCount),写锁低16位,表示写锁的重入次数 (exclusiveCount)。**状态值为 0 表示锁空闲,sharedCount 不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁,sharedCount和exclusiveCount 一般不会同时不为 0,只有当线程占用了写锁,该线程可以重入获取读锁。
- 获取写状态:S & ( 1 << 16 - 1):将高16位全部抹去
- 获取读状态:S >>> 16:无符号补0,右移16位
- 写状态加1:S+1
- 读状态加1:S+(1<<16)即S + 0x00010000
- 一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。
BlockingQueue
-
阻塞队列,特性是在任意时刻只有一个线程可以进行take或者put操作;
-
当队列是空的,从队列中获取元素的操作将会被阻塞;
-
当队列是满的,从队列中添加元素的操作将会被阻塞;
-
常用于生产者和消费者的场景;
-
优点:我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这一切BlockingQueue都处理好了;
-
常见的4种阻塞队列
- ArrayBlockingQueue 由数组支持的有界队列
- LinkedBlockingQueue 由链接节点支持的可选有界队列
- PriorityBlockingQueue 由优先级堆支持的无界优先级队列
- DelayQueue 由优先级堆支持的、基于时间的调度队列
CAS
- CAS是指Compare And Swap,比较并交换,是一种基于乐观锁的很重要同步思想;
- 如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。
- CAS缺点:
- 一直循环,开销比较大。
- 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
- 只能保证一个变量的原子操作,多个变量依然要加锁。
- 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
- ABA问题。
- 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了B,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际变化了。
- 解决办法就是使用版本号,在变量前面追加版本号,每次变量更新时把版本号加1
- 一直循环,开销比较大。