1、背景:
在1.5之前,加锁只能使用synchronized关键字,而synchronized在实现时,调用了native方法start0,借助操作系统来实现线程同步的控制,需要实现从用户态到内核态的切换,是一把重量锁,加锁的性能比较差。
1.1、交替执行和竞争执行
在并发同步中有两种情况,一种是多线程交替执行,一种是多线程竞争执行,在很多情况下,都是交替执行的,在交替执行时,在一个时刻只有一个线程想去访问共享资源,因此不会有线程需要等待;而在竞争执行时,在一个时刻会有多个线程想去访问同一共享资源
synchronized对于交替执行和竞争执行采用的处理策略是相同的,都需要交给操作系统来处理,实际上对于交替执行,完全在虚拟机层面就可以解决问题
1.2、AQS框架的提出
针对交替执行和竞争执行采用了不同的策略
1.3、自己实现一个简单的自旋锁
- 状态位
- CAS
- 循环实现自旋
使用循环和CAS,设置一个状态位,每一个线程在操作共享资源时,检查自己预期的状态位和实际值是否相同,如果相同,就可以获得锁,并且将状态位进行更新,否则就继续CAS,直到成功
public class MyLock {
//初始状态值为0,使用volatile关键字修饰,确保可见性
volatile int state = 0;
public void lock() {
//当CAS结果和预期不符合时,自旋
while(!compareAndSet(state,1)) {
}
}
//解锁,将状态码重新置为原值
public void unLock() {
this.state = 0;
}
//CAS,当预期状态值和实际值相同时,就更新状态值,返回ture,否则返回false
private boolean compareAndSet(int state,int val) {
//如果状态码和预期相同返回true,否则返回false
if(state == 0) {
this.state = val;
return true;
}
else {
return false;
}
}
//测试方法
private void test() {
lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
unLock();
}
}
}
这种锁就可以实现并发,
对于交替执行的情况,因此每次CAS的时候,都能返回true,(前一个线程已经将状态位复位了),性能是很好的。
对于竞争执行的情况,性能比较差,原因在于竞争不到锁的线程会持续CAS,占用cpu,当线程数比较大时,cpu负荷会很大,因此应该考虑让争抢不到锁的线程暂时睡眠,等待可能获得锁的时候,再次将它们唤醒
1.4、改进自旋锁
考虑到可能同时有多个线程可能需要睡眠等待,因此应该设计一个队列来让等待的线程入队,每次占用锁的线程释放锁时,队列出队一个线程获得锁
- 状态位
- CAS
- 循环实现自旋
- CAS失败的线程进入等待队列,并且睡眠park
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.LockSupport;
public class MyParkLock {
//等待线程队列
volatile Queue<Thread> threadQueue = new LinkedList<>();
//状态位,初始化为0
volatile int status = 0;
void lock(){
//如果CAS失败,就将当前线程park
while(!compareAndSet(0,1)) {
park();
}
}
//解锁,将状态位复位,执行unPark
void unlock() {
this.status = 0;
unpark();
}
//park,将当前线程加入到等待队列中,并调用LockSupport.park()使当前线程睡眠,这个方法会在底层调用native本地方法(Unsafe中)
private void park() {
threadQueue.add(Thread.currentThread());
LockSupport.park(Thread.currentThread());
}
//unpark,当队列非空时,让队列出队一个线程,并使用LockSupport.unpark()唤醒这个线程,同样会调用native方法(Unsafe中)
private void unpark() {
if(!threadQueue.isEmpty()) {
LockSupport.unpark(threadQueue.poll());
}
}
//CAS,逻辑完全不变
private boolean compareAndSet(int status, int val) {
if(status != this.status) {
return false;
}else {
this.status = val;
return true;
}
}
public static void main(String[] args) {
MyParkLock myParkLock = new MyParkLock ();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
myParkLock.test();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
myParkLock.test();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
myParkLock.test();
}
});
t1.start();
t2.start();
t3.start();
}
protected void test() {
// TODO Auto-generated method stub
lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
unlock();
}
}
改造完毕:此时对于交替执行的并发场景,和之前完全一样,直接在jvm层面就解决问题了;但是对于竞争执行的场景,CAS失败的线程会睡眠,释放出cpu,睡眠和唤醒依旧调用了native方法,还是需要操作系统来帮助实现
AQS框架大致就是按照这个思路实现的,但是它肯定比这肯定超级超级复杂了,它通过对加锁和解锁的过程进行细化,设计出不同的锁
- 重入锁:已经获得锁的线程可以再次调用lock方法加锁
- 公平锁和非公平锁:所有的等待线程都要进入等待队列等待时,就是公平锁,当有一个线程争抢锁时可以绕过队列中的线程直接获得锁时(非公平锁在加锁前先进行一次CAS,如果获得锁,就会直接执行而不去排队),就是非公平锁
- 读写锁:独锁是共享锁,写锁是独占锁(写锁时,后序续的读写操作都被阻塞)ReentrantReadWriteLock
- 共享锁:可以允许多个线程同时访问共享资源,状态位的初始值就是最多可以运行同时访问的线程数,当加锁时状态位减1,解锁时状态位加1,要求状态位必须大于0
- 独占锁:同时只允许一个线程访问共享资源
2、AQS:队列同步器
AbstractQueuedSynchronizer用来构建锁或者其他同步组件的基础框架,使用一个int成员变量作为状态位,利用内置的FIFO队列来完成排队
2.1、队列设计
- 队列结点Node:
每一个结点绑定了一个等待线程,这个线程的等待状态,前序结点和后序结点
static final class Node {
volatile Node prev; // initially attached via casTail
volatile Node next; // visibly nonnull when signallable
Thread waiter; // visibly nonnull when enqueued
volatile int status; // written by owner, atomic bit ops by others
}
- AQS控制器:
public abstract class AbstractQueuedSynchronizer {
//Head of the wait queue, lazily initialized.懒初始化的头结点
private transient volatile Node head;
//Tail of the wait queue. After initialization, modified only via casTail.尾结点
private transient volatile Node tail;
//The synchronization state.状态位
private volatile int state;
}
-
整体结构
-
队列是懒初始化的,当不需要排队时,只会CAS,不会创建队列,尽量不去park线程,因为这是一种重量级的锁
-
队列中head指向的头结点对应的Thread始终是null,因为第一个结点就是持有锁的对象,是不需要排队的,
-
如果一个结点的前一个结点是head结点,说明下一个应当获得锁的线程就是自己,因此这个结点在加入队列的过程中会进行两次自旋CAS,如果在这个过程中持有锁的线程释放了锁,那么它就不会进入队列,就会直接获得锁对象。
-
每一个结点入队时,会将它的前一个结点的状态位修改为-1(标识这个结点的线程已经睡眠),为什么不是自己修改自己的状态位呢?因为线程睡眠时调用park方法,应该确保线程已经park了才更新状态位,如果先修改状态位,因为不是原子操作,可能中断异常,线程已经park了,改不了自己的状态位了。
后续
本文只是将AQS的简单原理进行了整理
更详细的过程可以参考源码
两个参考链接:
https://blog.csdn.net/TJtulong/article/details/105345940
https://blog.csdn.net/java_lyvee/article/details/98966684