Android并发内存模型AQS——ReentrantLock原理与加锁流程

ReentrantLock的基本概念

概念:ReentrantLock是基于AQS进行的实现,AQS的全程AbstractQueuedSynchronizer,是阻塞式锁和同步器工具的框架,用一个state来表示锁状态,通过子类CAS去操作,内部维护了一个等待队列进行排队,使用条件变量来实现等待、唤醒,支持多个条件变量功能。

AQS有两种模式一种独占,一种共享(读写锁),今天只谈独占锁。

(1)什么时候用锁?

当有多个线程需要对同一个共享变量进行操作的时候就需要考虑上锁了,但是如果说这些线程是交替执行的,例如T1执行完T2执行,T2执行完T3执行,如果线程操作是按照这种顺序执行的那其实不需要上锁,既然业务上无法保证,那就需要通过锁进行控制了,简单点说,加锁就是为了保证多线程并发下在经过某一个代码块对某一个共享变量进行操作的时候,保证串行。

(2) 实现一把自己的锁都需要什么?

a.既然是面向对象编程那就可以先想一下,一把锁,大概都会有什么属性,肯定会有一个锁状态state来确定当前有没有锁。

b.多个线程并发执行只有一个线程可以执行成功那么其他线程就需要进行自旋、阻塞等待、睡眠任一操作,如果是自旋,java里面提供的自旋概念就是死循环,如果高并发下N多线程做死循环肯定会影响CPU的性能,睡眠又不确定需要多长时间,那大概率需要使用park操作来阻塞线程。

c.那线程阻塞后需要一个地方暂存它等待被唤醒,这里需要一种数据结构进行暂存get和set。 所以实现一把简单的锁大概需要锁状态、阻塞操作、暂存结构。

(3)公平锁和非公平锁:

在java中公平锁和非公平锁只有在抢锁的时候才有区分,公平锁会先看有没有其他线程排队,如果有则加入排队,如果没有抢锁,非公平锁则是直接抢锁,并不是说公平锁是排队等待唤醒,非公平锁是随机唤醒,只要进了等待队列,那就是一朝排队,永久排队。 竞争激励:调用park方法(操作系统层面)

竞争不激烈:多一次自旋 ,如果能获取到锁,则在jvm层面执行;不能获取到锁,执行park方法(在操作系统层面执行)
bbfc6ade7fada64520c4815f173663cd.jpeg

ReentrantLock加锁过程分析

1、自旋?如何实现一把自旋锁

通俗的讲,自旋就是不断的判断条件触发自己执行的功能,很多线程同步的思想都来源于自旋,我们以两个线程抢占资源来理解下自旋:
image.png

我们看到,当线程t1和线程t2共同抢占资源时,假如线程t1抢占到了资源,这时t1需要加锁并设置状态state=1,线程t2过来后会先判断状态state是否为0,如果不为0则一直循环判断state,直到线程t1解锁并设置state=0,线程t2才会继续抢占资源,线程t2不断循环判断的过程就是自旋。

伪代码①

volatile int state=0;//state标识,设置为原子操作
void lock(){
 while(!compareAndSet(0,1)){
 }
}
//逻辑代码
void unlock(){
 state=0;
}
boolean compareAndSet(int except,int newValue){
 //cas操作,修改status成功则返回true

}

我们分析下这个伪代码,这段代码存在一个原子变量state初始值为0,当线程t1拿到锁后,会先利用compareAndSet(0,1)方法进行判断,compareAndSet(0,1)的作用是比较传入的值是否为1,当传入的值为0时,则设置为1并返回true,传入的值为1时则返回false,在代码中,如果state=0,就将state设置为1并返回true,如果state=1则返回false。假设线程t1抢占锁时state=0,则!compareAndSet(0,1)就为false,则线程t1跳过while循环执行自己的逻辑代码;当线程t2想要获取锁时,因为此时state=1,则!compareAndSet(0,1)为true,线程t2就进入while循环内不断的进行循环判断,直到线程t1执行解锁方法并设置state为0,线程t2才能继续参与下一轮抢占锁。

NOTE:没有获取到锁的线程会一直进行while循环判断,这样做非常耗费CPU资源,所有这种方法并不可取。

因为很多锁的实现都是在自旋方法上的改进,所以在原伪代码的基础上加入睡眠和唤醒方法来提高代码的执行效率

伪代码②

volatile int state=0;
Queue parkQueue;//队列

void lock(){
 while(!compareAndSet(0,1)){

  park();
 }
 //逻辑代码
   unlock()
}

void unlock(){
 lock_notify();
}

void park(){
 //将当前线程加入到等待队列
 parkQueue.add(currentThread);
 //将当期线程释放cpu 
 releaseCpu();
}
void lock_notify(){
 //获取要唤醒的线程
 Thread t=parkQueue.header();
 //唤醒线程
 unpark(t);

}

伪代码②是在伪代码①的基础上加入了睡眠和唤醒操作,这样可以保证在队列中的线程不占用CPU资源,park和unpark是java.util.concurrent.locks包下的方法,用于睡眠和唤醒。这样,我们就可以手动实现锁来保证线程的同步了,事实上,很多的锁的编写都是基于这个思路的。下面,就可以引入我们要学的锁–ReentrantLock,它的加锁/解锁就类似于伪代码②

2、ReentrantLock的提出

在jdk1.6之前,我们使用锁实现同步使用的是synchronized关键字,但是synchronized的实现原理是调用操作系统函数来实现加锁/解锁,我们都知道一旦涉及操作系统的函数,那么代码执行的效率就会变低,因此,使用synchronized关键字来实现加/解锁就被称为重量级锁,为了改善这一情况,Doug Lea就写了ReentrantLock锁,这种锁分情况在jvm层面和操作系统层面完成加锁/解锁的过程,因此代码执行效率显著提高,后来sun公司在jdk1.6以后也改进了synchronized,使得synchronized的执行效率和reentrantLock差不多,甚至更好,但是由于ReentrantLock可以直接代码操作加锁/解锁,可中断获取锁等特性,因此使用的比较多。

3、ReentrantLock加锁分析

3.1、AQS简介

在学习ReentrantLock加锁之前,我们先了解下队列同步器AbstractQueueedSynchronizer的概念,简称为AQS,它是用来构建锁的基础框架,通过内置的FIFO队列来完成线程队列中的排队工作

AQS提供了一个node结点类,主要有以下属性

volatile Node prev;//执行前一个线程
volatile Node next;//执行下一个线程
volatile Thread thread;//结点中的当前线程

除此之外,AQS为了维护好线程队列,它还定义了两个结点用于指向队列头部和队列尾部,定义了了state用于修饰锁的状态

private transient volatile Node head;//指向队列头
private transient volatile Node tail;//指向队列尾
private volatile int state;//锁状态,默认为0,加锁成功则为1,重入+1 解锁则为0

private transient Thread exclusiveOwnerThread;//独占锁的线程

volatile Node prev;//执行前一个线程
volatile Node next;//执行下一个线程
volatile Thread thread;//结点中的当前线程

除此之外,AQS为了维护好线程队列,它还定义了两个结点用于指向队列头部和队列尾部,定义了了state用于修饰锁的状态

private transient volatile Node head;//指向队列头
private transient volatile Node tail;//指向队列尾
private volatile int state;//锁状态,默认为0,加锁成功则为1,重入+1 解锁则为0

private transient Thread exclusiveOwnerThread;//独占锁的线程

队列线程图示
image.png

AQS中有很多操作锁的方法,我们会以ReentrantLock的加锁过程来讲解这些方法,在这里就不单独讲解。

3.2、ReentrantLock加锁总体分析

为了方便分析,我们先编写一个Demo,分别以线程1、线程2抢占锁的步骤来学习ReentrantLock

登录后复制 
/**
 * @Author: Simon Lang
 * @Date: 2020/5/8 16:19
 */
public class TestReentrantLock {

    public static void main(String[] args){
        final ReentrantLock lock=new ReentrantLock(true);
        Thread t1=new Thread("t1"){
            @Override
            public void run() {
                lock.lock();
                lockTest();
                lock.unlock();
            }
        };
        Thread t2=new Thread("t2"){
            @Override
            public void run() {
                lock.lock();
                lockTest();
                lock.unlock();
            }
        };
        t1.start();
        t2.start();
    }

    public static void lockTest(){

        System.out.println(Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
            System.out.println(" -------end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这个测试Demo里,锁对象就是ReentrantLock对象,加锁过程就是调用lock对象里的lock方法,即lock.lock()。

lock方法是reentrantLock类中提供的方法,Sync是同步器(AQS)提供的实施加锁的方法,AQS提供了两种加锁方法,分别为公平锁和非公平锁。

//同步器提供的加锁方法
private final Sync sync;
 public void lock() {
        sync.lock();
    }

为了方便后序加锁流程的分析,我们先简要说明下的公平锁与非公平锁的区别。

公平锁的源码

final void lock() {
    acquire(1);//1------标识加锁成功之后改变的值
}

非公平锁的源码

final void lock() {
 if (compareAndSetState(0, 1))//cas判断
  setExclusiveOwnerThread(Thread.currentThread());//设置当前前线程抢占
 else
   acquire(1);
} 

我们看到非公平锁比公平锁多了个判断,非公平锁在在执行lock方法时,会先进行cas判断,如果为0直接抢占锁成功,如果state=1,则进行acquire(1)方法判断,而公平锁是直接进行acquire(1)判断,事实上,公平锁公平的原因是因为它考虑队列中线程的排队顺序,保证的依次进行加锁执行,而非公平锁则是直接判断状态state的值进行抢占。

为了使得分析代码的时候不容易绕晕,我们先从逻辑层面上分析ReentrantLock的加锁的流程,具体的细节放在每个线程执行的流程上讲解。
image.png

结合上面的伪代码②,大家可能会对这个流程图会有疑问:state=0不应该直接加锁么?为什还要判断是否加入队列呢?

其实这和线程间的并发执行有关,释放锁的过程也是并发执行的,释放锁执行顺序可能是①设置state=0②unpark③唤醒下一个线程。如果获取当前锁的线程进行步骤②操作时,另一个线程就进来判断了,如果这个线程不进行是否需要排队判断则会引发线程安全问题。

我们以公平锁为例学习reentrantLock的加锁过程

3.3、线程1执行流程

当线程1执行公平锁的过程中,会首先执行acquire(1)方法,我们来分析下线程1的执行步骤

acquire(int arg)方法是独占式的获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则将会进入同步队列等待。

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

线程1会首先执行tryAcquire(arg)方法,

tryAcquire(int arg)是独占式获取锁,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后在进行cas设置同步状态

 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
     //获取锁的状态
            int c = getState();
     //如果c=0,则判断是否需要排队
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
     //否则,判断当前线程是否是正在持有锁的线程
            else if (current == getExclusiveOwnerThread()) {
                //如果是,则判断重入次数
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
     //否则,返回false
            return false;
}

线程1会先获取当前的锁的状态,假设忽略主线程,线程t1是第一个进来的,所以state=0,继续判断是否需要排队(调用hasQueuedPredecessors)

登录后复制 
  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());

•    }

因为线程t1是第一个,所以线程队列为空,tail和head均为null,所以条件h!=t不成立,hasQueuedPredecessors方法返回为false,所以在tryAcquire方法中第一个判断条件成立,又因为此时的state=0,所以执行compareAndSetState返回为true,第二个判断条件成立。执行setExclusiveOwnerThread(current)将线程1上锁成功并返回true,acquire()也正常返回,一直返回到我们编写的逻辑代码内。

线程1执行流程图
image.png

3.4、线程2执行流程

在线程t1执行的过程中,假设线程2来试图获取锁,它首先还是会先执行acquire方法

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();

•    }

在acquire方法中先执行tryAcquire方法进行条件判断

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
     //获取锁的状态
            int c = getState();
     //如果c=0,则判断是否需要排队
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
     //否则,判断当前线程是否是正在持有锁的线程
            else if (current == getExclusiveOwnerThread()) {
                //如果是,则判断重入次数
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
     //否则,返回false
            return false;

•        } 

因为此时的state=1且当前持有锁的线程为线程t1,所以线程t2执行tryAcquire()方法直接返回false给acquire方法。

在acquire()方法内,!tryAcquire()为true,所以要进行第二个判断acquireQueued(addWaiter(Node.EXCLUSIVE),arg)

我们先分析addWaiter(Node.EXCLUSIVE)方法

> 将新加入的线程结点加入到队列尾部

private Node addWaiter(Node mode) {
     //将当前线程设置为线程结点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
     //将队列的尾节点赋给pred
        Node pred = tail;
     //判断pred是否为空结点
        if (pred != null) {
            //将当前线程(t2线程结点)结点的前驱结点设为pred
            node.prev = pred;
            //将node结点cas操作
            if (compareAndSetTail(pred, node)) {
                //建立连接关系
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;

•    }

这段代码首先会将线程t2设置成线程结点,判断队列中是否存在线程结点,如果不存在,则执行enq(node)先构造一个空的线程结点

 private Node enq(final Node node) {
        //死循环
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //构造一个空节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {//将线程t2结点加入队列
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }

•    }

图解构造线程结点

第一次循环构造空线程结点
image.png

第二次循环将线程t2结点加入队列
image.png

将t2结点加入到队列中并返回addWaiter方法,addWaiter返回t2线程结点到acquire方法中执行acquireQueued方法

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //死循环
            for (;;) {
                //获取当前线程结点的上一个结点p
                final Node p = node.predecessor();
                //判断p是否为头结点,并尝试这再次获取锁
                if (p == head && tryAcquire(arg)) {
                    //将当前结点设置为头结点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //否则,让线程t2结点睡眠
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }

•    }

这段代码主要是判断线程t2结点的前驱结点是否为头结点,如果为头结点就尝试再次获取锁,否则就直接睡眠,如果不能获取锁就一直睡眠停留在这里,否则就会返回执行用户编写的代码

线程2执行流程图
image.png

前面提到,ReentrantLock可以分情况在jvm层面和操作系统层面执行,我们将线程执行分为以下几种情况

  • 只有一个线程:直接进行CAS操作,不需要队列(jvm层面)
  • 线程交替执行:直接进行CAS操作,不需要队列(jvm层面)
  • 资源竞争

Android进阶技术手册+Android2022面试大纲一份;需要点击免费领取

进阶笔记汇总图册.png

pdf大全进阶资料.png

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_71524094/article/details/126126400