从零开始多线程学习-AQS与ReentrantLock

1:ReentrantLock简介
ReentrantLock是java并发包里面的可重入锁,它是基于AQS(AbstractQueuedSynchronizer)实现的,它有两种锁,公平和非公平锁,本文主要用来记录我个人学习的总结。

1.1:ReetrantLock的小Demo说明

public class LockExample {
    private ReentrantLock lock = new ReentrantLock(true);
    
    public void modifyResources(String threadName){
        System.out.println("通知吴亦凡:--->"+threadName+"准备说唱");
        lock.lock();
        System.out.println("舞台:--->"+threadName+"第一次说唱");
        lock.lock();
        System.out.println("舞台:"+threadName+"第二次说唱");
        lock.unlock();
        System.out.println("舞台:"+threadName+"你不行");
        lock.unlock();
        System.out.println("舞台:"+threadName+"还是不行");
    }
    public static void main(String[] args){
        LockExample tp = new LockExample();
        new Thread(()->{
            String threadName = Thread.currentThread().getName();
            tp.modifyResources(threadName);
        },"Rapper:阿Giao").start();
        new Thread(()->{
            String threadName = Thread.currentThread().getName();
            tp.modifyResources(threadName);
        },"Rapper:药水哥").start();
    }
}

我写了一个Rapper小demo代码很简单就不介绍了,下面为运行结果,可以发现阿Giao必须要释放锁后,吴亦凡才会去听药水哥说唱。

通知吴亦凡:--->Rapper:阿Giao准备说唱
舞台:--->Rapper:阿Giao第一次说唱
通知吴亦凡:--->Rapper:药水哥准备说唱
舞台:Rapper:阿Giao第二次说唱
舞台:Rapper:阿Giao你不行
舞台:Rapper:阿Giao还是不行
舞台:--->Rapper:药水哥第一次说唱
舞台:Rapper:药水哥第二次说唱
舞台:Rapper:药水哥你不行
舞台:Rapper:药水哥还是不行

2:ReentrantLock lock = new ReentrantLock(true)创建一个公平锁
当我们创建lock的时候,给它带上参数true则会创建一个公平锁,反之如果传false则为非公平锁。

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我们可以发现一个特别的类sync那么,这个类是什么呢?我们进入到代码看看

2.1:sync类
可以发现该类继承了AbstractQueuedSynchronizer也就是AQS类,到这里我们找到了ReentrantLock里面最重要的类也是我们需要学习的一个类。

 abstract static class Sync extends AbstractQueuedSynchronizer

2.2:AbstractQueuedSynchronizer学习
这里附上lock所有的参数,先从外层解析一下,首先lock有4个参数
1:head#头部节点
2:tail#尾部节点
3:state#加锁次数
4:exclusiveOwnerThread#当前的加锁线程
整体的参数构成就是这样。
在这里插入图片描述
接下来主要讲一下sync里面的head和tail,要想讲清楚它们首先要来认识一下CLH队列,这个队列主要就是负责记录阻塞线程,以及线程节点的状态。

在这里插入图片描述
进入到Sync里面我们有几个重要的参数负责记录信息,主要在内部类Node中
1:waitStatus#队列里面线程的状态分为(-3,-2,-1,0,1)
2:prev#前驱节点
3:next#后驱节点
4:thread#节点同步状态的线程
5:nextWaiter#等待队列中的后继节点
waitStatus的状态类型分别对应
static final int CANCELLED = 1;#线程等待超时或者被中断
static final int SIGNAL = -1;#后继节点的线程处于等待状态
static final int CONDITION= -2;#节点在等待队列中,节点的线程等待在Condition上
static final int PROPAGATE= -3;#表示下一次共享式同步状态获取将会被无条件地传播下去

3:加锁过程

final void lock() {
        acquire(1);
    }

通过调用lock方法进入获取锁

  public final void acquire(int arg) {
		#首先尝试获取锁如果获取不到尝试将线程加入队列
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
 protected final boolean tryAcquire(int acquires) {
			#获取当前线程信息
            final Thread current = Thread.currentThread();
            #获取加锁的次数
            int c = getState();
            #如果当前没有任何线程拿到锁
            if (c == 0) {
            	#判断当前节点是否有前驱节点
            	#如果没有前驱节点,则通过CAS比较将加锁次数置为1
                if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                    //将锁给当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            #如果当前线程等于拥有锁的线程
            else if (current == getExclusiveOwnerThread()) {
            	#将锁的次数加1(也就是为什么我们截图锁的次数为2的原因)
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
 public final boolean hasQueuedPredecessors() {
 		#获取尾节点
        Node t = tail; // Read fields in reverse initialization order
        #获取头结点
        Node h = head;
        Node s;
        #如果尾节点与头结点相等,队列为空队列
        return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
    }

到这里如果我们获取锁失败了则会进入队列方法

private Node addWaiter(Node mode) {
       	#构造一个当前线程的Node节点
        Node node = new Node(Thread.currentThread(), mode);
        #获取尾部节点Node
        Node pred = tail;
        #判断尾部节点是否为空
        if (pred != null) {
            #将当前节点的前驱节点置为当前队列尾部节点
            node.prev = pred;
            #通过CAS比较算法将尾部节点与当前节点替换实现尾部插入当前节点
            if (compareAndSetTail(pred, node)) {
            	#将旧尾部节点的后置节点置为当前节点
                pred.next = node;
                return node;
                #实际上这里新加入的节点必须在为部排队所以采用尾部插入的方法
            }
        }
        #如果尾部节点为空,证明当前队列没有节点
        enq(node);
        return node;
    }
 private Node enq(final Node node) {
 		#自旋,死循环
        for (;;) {
        	#获取尾部节点
            Node t = tail;
            #如果尾部节点为空,证明这就是个空队列,需要初始化队列
            if (t == null) { // Must initialize
                #创建空的头节点
                if (compareAndSetHead(new Node()))
                	#尾部同时指向该空node(类似于一个空节点被head和tail指向)
                    tail = head;
            } else {
            	#当前传入节点的前驱节点指向head也就是刚才的空节点
                node.prev = t;
                #CAS设置尾部节点
                if (compareAndSetTail(t, node)) {
                	#前驱节点的next指针指向当前节点
                    t.next = node; 
                    return t;
                }
            }
        }
    }

到此线程已经成功被加入到了CHL队列当中

	#尝试阻塞队列
 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            #自旋死循环
            for (;;) {
            	#获取当前节点的前驱节点
                final Node p = node.predecessor();
                #如果当前节点的前驱节点是head节点证明队列中我就是下一个执行的线程,尝试获取锁
                if (p == head && tryAcquire(arg)) {
                	//获取成功,将当前结点设置为头结点。
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                #如果当前节点前驱节点不是head,则尝试阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 		#获取当前节点的前驱节点的状态值
        int ws = pred.waitStatus;
        #若前驱结点是SIGNAL也就是-1,意味着当前结点可以被安全地park
        if (ws == Node.SIGNAL)
            return true;
        #若前驱节点的状态值为CANCELLED -1则前驱节点可能中断或超时了需要移除
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
          #如果尾0或者其他状态会被现行替换为-1然后在呗阻塞
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

接下来让我们看看解锁过程,以及怎么样唤醒CHL中的节点取拿锁,其实在阻塞的方法中,线程一直在死循环中,也就是只能获取到锁才会跳出死循环。

4:解锁过程

 public final boolean release(int arg) {
 		#尝试解锁
        if (tryRelease(arg)) {
        	#获取头结点
            Node h = head;
            #如果头结点不为空,并且头结点的状态不为0,所有结点初始为0
            if (h != null && h.waitStatus != 0)
            	#解锁当前的头结点,也就是这次线程结束后,后面队列的线程
                unparkSuccessor(h);
            return true;
        }
        return false;
protected final boolean tryRelease(int releases) {
			#当前锁的数量-1
            int c = getState() - releases;
            #如果当前线程不是锁的拥有者则会报错
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            #如果没有加锁的线程了
            if (c == 0) {
                free = true;
                #锁的拥有者置为空
                setExclusiveOwnerThread(null);
            }
            #加锁次数置空
            setState(c);
            return free;
        }
private void unparkSuccessor(Node node) {
    	#获取头结点的状态
        int ws = node.waitStatus;
        if (ws < 0)
        	#将头结点的状态置为0
            compareAndSetWaitStatus(node, ws, 0);
        #获取头结点的下一个节点
        Node s = node.next;
        #判断队列有无下一个节点,或者下一个节点是否死亡中断
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	#解锁队列中下一个节点,让他运行拿锁
            LockSupport.unpark(s.thread);
    }

到此解锁过程结束

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值