JavaAQS框架个人学习笔记整理

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值