ReentrantLock非公平锁的加锁操作源码解读

        AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。关于Condition的等待队列我们后面再分析,这里我们先来看看AQS中的同步队列模型

我们看一个ReentranLock小案例,分析一下ReentranLock加锁和解锁

public class ReentranLockTest {
    static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            testSync();
        }, "t1");
        Thread t2 = new Thread(() -> {
            testSync();
        }, "t2");

        t1.start();
        t2.start();
    }

    public static void testSync() {
        reentrantLock.lock();
        System.out.println(Thread.currentThread().getName());

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }
}

我们手写一下lock方法,自旋实现同步的方式,实际上就是采用的CAS算法机制

volatile int status=0;//标识---是否有线程在同步块-----是否有线程上锁成功
void lock(){
	
	while(!compareAndSet(0,1)){
	}
	//lock
    //10  t1

}

void unlock(){
	status=0;
}

boolean compareAndSet(int except,int newValue){
	//cas操作,修改status成功则返回true
  status = 1;   
}

两个线程T1,T2 其实就是t1进来了之后,会去比较status是不是0,如果是0,代表没人在用,修改status状态,然后执行代码,这个是t2进来的时候,状态比较失败,已经现在的状态已经是1了,代表有人在用了,只能不停的while循环判断,直到t1使用结束,把状态改回0!这个就是手动实现的CAS逻辑,当然还有一些小问题!我们先解析一下原理:

实际上简单理解来说,就是使用CAS算法去实现的,我们来看一下源码:

我们在new ReentrantLock的时候无参构造给我们返回的是一个非公平锁,我们来看一下非公平锁里面的lock方法:

我们看下源码,这个方法的期望值(expect是0),更新值(update是1),就是期望是0,判断成功之后会更新成1,如果成功,则独占锁线程设置为当前线程 ,如果失败,再次请求同步状态!

实际上就是比较和交换!

我们点击进去,到最后可以发现实际上是一个native方法!最终到底层调用的是一条原语,请看百度百科对原语的介绍:

计算机进程的控制通常由原语完成。所谓原语,一般是指由若干条指令组成的程序段,用来实现某个特定功能,在执行过程中不可被中断。在操作系统中,某些被进程调用的操作,如队列操作、对信号量的操作、检查启动外设操作等,一旦开始执行,就不能被中断,否则就会出现操作错误,造成系统混乱。所以,这些操作都要用原语来实现 原语是操作系统核心(不是由进程,而是由一组程序模块组成)的一个组成部分,并且常驻内存,通常在管态下执行。原语一旦开始执行,就要连续执行完,不允许中断 [1]  。

最核心的一句话就是:在执行过程中不可被中断(这个其实理解了CPU的时间片就行)

理解了原语之后,CAS算法就非常容易理解了,具体可以看CAS文档!

这里获取锁时,首先对同步状态执行CAS操作,尝试把state的状态从0设置为1,如果返回true则代表获取同步状态成功,也就是当前线程获取锁成,可操作临界资源,如果返回false,则表示已有线程持有该同步状态(其值为1),获取锁失败,注意这里存在并发的情景,也就是可能同时存在多个线程设置state变量,因此是CAS操作保证了state变量操作的原子性。返回false后,执行 acquire(1)方法,该方法是AQS中的方法,它对中断不敏感,即使线程获取同步状态失败,进入同步队列,后续对该线程执行中断操作也不会从同步队列中移出,方法如下

这里传入参数arg表示要获取同步状态后设置的值(即要设置state的值),因为要获取锁,而status为0时是释放锁,1则是获取锁,所以这里一般传递参数为1,进入方法后首先会执行tryAcquire(arg)方法,在前面分析过该方法在AQS中并没有具体实现,而是交由子类实现,因此该方法是由ReetrantLock类内部实现的,这边我们还是看一下非公平锁中的实现:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //判断同步状态是否为0,并尝试再次获取同步状态
    if (c == 0) {
         //执行CAS操作
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //设置当前同步状态,当前只有一个线程持有锁,因为不会发生线程安全问题,可以直接执行 setState(nextc);
        setState(nextc);
        return true;
    }
    return false;
}

从代码执行流程可以看出,这里做了两件事,一是尝试再次获取同步状态,如果获取成功则将当前线程设置为OwnerThread,否则失败,二是判断当前线程current是否为OwnerThread,如果是则属于重入锁,state自增1,并获取锁成功,返回true,反之失败,返回false,也就是tryAcquire(arg)执行失败,返回false。需要注意的是nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值,可以保证state的更改是线程安全的,因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功即可获取锁,不管该线程是新到来的还是已在同步队列的线程,毕竟这是非公平锁,并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态然后新到来的线程恰好获取到同步状态)先获取到锁,接着看之前的方法acquire(int arg)

如果tryAcquire(arg)返回true,acquireQueued自然不会执行,这是最理想的,因为毕竟当前线程已获取到锁,如果tryAcquire(arg)返回false,则会执行addWaiter(Node.EXCLUSIVE)进行入队操作,由于ReentrantLock属于独占锁,因此结点类型为Node.EXCLUSIVE,下面看看addWaiter方法具体实现

private Node addWaiter(Node mode) {
    //将请求同步状态失败的线程封装成结点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    //如果是第一个结点加入肯定为空,跳过。直接执行enq代码
    //如果非第一个结点则直接执行CAS入队操作,尝试在尾部快速添加
    if (pred != null) {
        node.prev = pred;
       //使用CAS执行尾部结点替换,尝试在尾部快速添加
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果第一次加入或者CAS操作没有成功执行enq入队操作
    enq(node);
    return node;
}

创建了一个Node.EXCLUSIVE类型Node结点用于封装线程及其相关信息,其中tail是AQS的成员变量,指向队尾(AQS维持的是一个双向的链表结构同步队列),如果是第一个结点,则为tail肯定为空,那么将执行enq(node)操作,如果非第一个结点即tail指向不为null,直接尝试执行CAS操作加入队尾,如果CAS操作失败还是会执行enq(node),继续看enq(node):

private Node enq(final Node node) {
    //这边用的死循环!!
    for (;;) {
        Node t = tail;
        //如果队列为null,即没有头结点
        if (t == null) { // Must initialize
            //创建并使用CAS设置头结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {//队尾添加新结点
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

这个方法使用一个死循环进行CAS操作,可以解决多线程并发问题。这里做了两件事,一是如果还没有初始同步队列则创建新结点并使用compareAndSetHead设置头结点,tail也指向head(实际上这个时候,循环并没有退出)二是队列已存在,则将新结点node添加到队尾。注意这两个步骤都存在同一时间多个线程操作的可能,如果有一个线程修改head和tail成功,那么其他线程将继续循环,直到修改成功,这里使用CAS原子操作进行头结点设置和尾结点tail替换可以保证线程安全,从这里也可以看出head结点本身不存在任何数据,它只是作为一个牵头结点,而tail永远指向尾部结点(前提是队列不为null)。

添加到同步队列后,结点就会进入一个自旋过程,即每个结点都在观察时机待条件满足获取同步状态,然后从同步队列退出并结束自旋,回到之前的acquire()方法,自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的,代码如下

addWaiter(Node.EXCLUSIVE)方法返回的线程包装后的节点EXCLUSIVE节点,

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {//自旋,死循环
            //获取前驱结点
            final Node p = node.predecessor();
            当且仅当p为头结点才尝试获取同步状态
            if (p == head && tryAcquire(arg)) {//当p为头结点,并且当前节点重新获取锁成功才执行
            //这边要注意的是使用了&&!Java基础知识,就是当p为头节点,才会去执行tryAcquire方法
                //将node设置为头结点
                setHead(node);//
                //清空原来头结点的引用便于GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //如果前驱结点不是head,判断是否挂起线程。其实是为了保护CPU资源,不允许有大量的线程不停
            //在自旋想要获取锁
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

当前线程在自旋(死循环)中获取同步状态,当且仅当前驱结点为头结点才尝试获取同步状态,这符合FIFO的规则,即(先入先出队列(First Input First Output,FIFO)),其次head是当前获取同步状态的线程结点,只有当head释放同步状态唤醒后继结点,后继结点才有可能获取到同步状态,因此后继结点在其前继结点为head时,才进行尝试获取同步状态,其他时刻将被挂起。进入if语句后调用setHead(node)方法,将当前线程结点设置为head

private void setHead(Node node) {
    head = node;
    //清空结点数据
    node.thread = null;
    node.prev = null;
}

设置为node结点被设置为head后,其thread信息和前驱结点将被清空,因为该线程已获取到同步状态(锁),正在执行了,也就没有必要存储相关信息了,head只有保存指向后继结点的指针即可,便于head结点释放同步状态后唤醒后继结点,执行结果如下图(return interrupted;这个地方返回的是false,)

从图可知更新head结点的指向,将后继结点的线程唤醒并获取同步状态(这边的说法并不妥当,后续的线程一直也在运行尝试获取锁,只不过它不是头结点,判断失败,然后一直获取不到,上一个线程执行结束,把它设置成头节点,它就可以获取到了),调用setHead(node)将其替换为head结点,清除相关无用数据。当然如果前驱结点不是head,那么执行如下

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
     //获取当前结点的等待状态
     int ws = pred.waitStatus;
     //如果为等待唤醒(SIGNAL)状态则返回true
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
        //如果ws>0 则说明是结束状态,
        //遍历前驱结点直到找到没有结束状态的结点
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;//参见下图
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
         //如果ws小于0又不是SIGNAL状态,
        //则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

node.prev = pred = pred.prev;辅助理解图

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
      //获取线程中断状态,interrupted()是判断当前中断状态,
        //并非中断线程,因此可能true也可能false,并返回
    return Thread.interrupted();
}

shouldParkAfterFailedAcquire()方法的作用是判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态),如果是则返回true。如果结点的ws为CANCELLED状态(值为1>0),即结束状态,则说明该前驱结点已没有用应该从同步队列移除,执行while循环,直到寻找到非CANCELLED状态的结点。倘若前驱结点的ws值不为CANCELLED,也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时,结点状态为CONDITION因此需要转换为SIGNAL),那么将其转换为SIGNAL状态,等待被唤醒。

若shouldParkAfterFailedAcquire()方法返回true,即前驱结点为SIGNAL状态同时又不是head结点,(也就是在当前线程前面已经有线程在等待唤醒了,那么当前线程需要被挂起),那么使用parkAndCheckInterrupt()方法挂起当前线程,并且设置一个中断的标志位!称为WAITING状态,需要等待一个unpark()操作来唤醒它,到此ReetrantLock内部间接通过AQS的FIFO的同步队列就完成了lock()操作,这里我们总结成逻辑流程图

当然关于获取锁的操作,这里看看另外一种可中断的获取方式,即调用ReentrantLock类的lockInterruptibly()或者tryLock()方法,最终它们都间接调用到doAcquireInterruptibly()

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

检测到线程的中断操作后,直接抛出异常,从而中断线程的同步状态请求,移除同步队列。

上面我们的手动的CAS缺点:耗费cpu资源。没有竞争到锁的线程会一直占用cpu资源进行cas操作,假如一个线程获得锁后要花费Ns处理业务逻辑,那另外一个线程就会白白的花费Ns的cpu资源

思路:让得不到锁的线程让出CPU

yield+自旋

volatile int status=0;
void lock(){
	while(!compareAndSet(0,1)){
     yield();//自己实现
	}
	//lock logic

}
void unlock(){
	status=0;
}

要解决自旋锁的性能问题必须让竞争锁失败的线程不空转,而在获取不到锁的时候能把cpu资源给让出来,yield()方法就能让出cpu资源,当线程竞争锁失败时,会调用yield方法让出cpu。自旋+yield的方式并没有完全解决问题,当系统只有两个线程竞争锁时,yield是有效的。需要注意的是该方法只是当前让出cpu,有可能操作系统下次还是选择运行该线程,比如里面有2000个线程,想想会有什么问题?

有2000的线程,只有一个线程获取到了锁,现在让出了CPU,获得锁的线程也不一定能抢到CPU

sleep+自旋

volatile int status=0;
void lock(){
	while(!compareAndSet(0,1)){
		sleep(2);
	}
	//lock---10m

}
void unlock(){
	status=0;
}

使用睡眠的话,难点在于睡眠多长时间是不确定的,有多少线程?硬件设置处理效率如何?每个线程执行多长时间,什么时候轮到我?很难确定时间!

park+自旋

volatile int status=0;
Queue parkQueue;//集合 数组  list

void lock(){
	while(!compareAndSet(0,1)){
		//
		park();----
	}
	//lock    10分钟
   。。。。。。
   unlock()
}

void unlock(){
	lock_notify();
}

void park(){
	//将当期线程加入到等待队列
	parkQueue.add(currentThread);
	//将当期线程释放cpu  阻塞   睡眠
	releaseCpu(); 
}
void lock_notify(){
    //status=0
	//得到要唤醒的线程头部线程
	Thread t=parkQueue.header();
	//唤醒等待线程
	unpark(t);
}

具体可以看Unsafe类解析,

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值