J.U.C之重入锁原理分析之AQS同步队列

什么是J.U.C

java.util.concurrent 是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器、并发集合等等;

Lock

Lock简介

锁是用来解决 并发安全问题,在Lock接口之前,java中对于解决多线程的并发安全问题只能用基于synchronized 关键字来解决,但是synchronized 关键字存在一些短板,也就不适应于并发场景,在JAVA5后,Lock的出现可以解决并发问题

Lock应用

Lock是一个接口,定义了一个获取锁和释放锁的方法;不同的实现方法:

ReentrantLock表示重入锁:它是唯一一个实现LOCK接口的类,重入锁是指一个线程在获取锁后,再次进入时不需要阻塞,而是关联一次计数器增加重入次数;
ReentrantReadWriteLock::重入读写锁,它实现了ReadWriteLock接口,在这个 类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock 接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则 是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的 操作都会存在互斥。
StampedLock: stampedLock是JDK8引入的新的锁机制,可以简单认为是读写 锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全 并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。 stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程

锁的类图关系

最常见的为例,ReentrantLock重入锁,
在这里插入图片描述

ReentrantLock 重入锁

当线程1获取锁后,再次调用lock方法时,直接增加重入次数就行;

重入锁的设计目的

 public synchronized void demo(){
        System.out.println("begin:demo");
        demo2();
    }
    public void demo2(){
        System.out.println("begin:demo2");
        synchronized (this){

        }
    }
    public static void main(String[] args) {
        RetranLockDemo retranLockDemo=new RetranLockDemo();
        new Thread(retranLockDemo::demo).start();
    }

调用demo方法获得了当前的对象锁,然后在这个方法中再去调用 demo2,demo2中的存在同一个实例锁,这个时候当前线程会因为无法获得 demo2的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死 锁。

重入锁的使用

package com.pattern.volatileTest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by chenli on 2019/10/27.
 * J.U.C
 */
public class RetranLockDemo {
    static Lock lock=new ReentrantLock();

    private static int count=0;

    public static void  incr() {
        lock.lock();
        try {
            Thread.sleep(1);
            count++;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<100;i++){
            new Thread(()->RetranLockDemo.incr()).start();
        }
        Thread.sleep(3000);
        System.out.println(count);
    }
}

ReentrantReadWriteLock 读写锁

以前的基本都是排他锁,也就是这些锁在同一时刻只允许一个线程进 行访问,而读写所在同一时刻可以允许多个线程访问,但是在写线程访问时,所有 的读线程和其他写线程都会被阻塞。读写锁维护了一对锁,一个读锁、一个写锁; 一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读 多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.

public class LockDemo { 
 
    static Map<String,Object> cacheMap=new HashMap<>(); 
    static ReentrantReadWriteLock rwl=new 
ReentrantReadWriteLock(); 
    static Lock read=rwl.readLock(); 
    static Lock write=rwl.writeLock(); 
 
    public static final Object get(String key) { 
        System.out.println("开始读取数据"); 
        read.lock(); // 读锁
 
        try { 
            return cacheMap.get(key); 
        }finally { 
            read.unlock(); 
        } 
    } 
    public static final Object put(String key,Object value){ 
        write.lock(); 
        System.out.println("开始写数据"); 
        try{ 
            return cacheMap.put(key,value); 
        }finally { 
            write.unlock(); 
        } 
    } 
} 

在这个案例中,通过hashmap来模拟了一个内存缓存,然后使用读写所来保证这 个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时 候,读锁不会被阻塞,因为读操作不会影响执行结果。 在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线 程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。使用读写锁提升 读操作的并发性,也保证每次写操作对所有的读写操作的可见性 ⚫ 读锁与读锁可以共享 ⚫ 读锁与写锁不可以共享(排他) ⚫ 写锁与写锁不可以共享(排他)

ReentrantLock 的实现原理

锁的基本原理是基于多线程执行一个任务,通过串行的方式执行,从而达到线程安全的目的。在synchronized 中,有偏向锁,轻量级锁,重量级锁,乐观锁。通过线程的阻塞和唤醒来达到线程的同步和唤醒;

AQS介绍

在LOCK中用的了同步队列AQS,全名AbstractQueuedSynchronizer,是LOCK实现线程同步的核心组件;

AQS的两种实现

有独占和共享,独占是每次只能一个线程持有锁,也就是互斥锁。共享锁是允许多个线程获取锁,并发访问共享资源(ReentrantReadWriteLock );

AQS的内部实现

AQS内部维护了一个FIFO的双向链表,特点是每个数据结构都有两个指针,分别直接指向前驱节点和后继节点;每个节点有线程组装而成,当线程争抢锁失败会封装成一个NODE节点加入到AQS队列中,当获取锁的线程从中释放锁则会唤醒AQS中的一个阻塞的节点;
在这里插入图片描述

NODE的组成

在这里插入图片描述

释放锁以及添加线程对于队列的变化

当出现锁竞争时,AQS同步对列中的节点会发生变化
1.新加一个节点新的线程封装成Node节点追加到同步队列中,设置prev节点以及修改当前节 点的前置节点的next节点指向自己
2. 通过CAS讲tail重新指向新的尾部节点
在这里插入图片描述
2.当释放锁时,队列的变化
在这里插入图片描述

  1. 修改head节点指向下一个获得锁的节点
  2. 新的获得锁的节点,将prev的指针指向null
    设置Head节点时不需要cas操作,设置head节点是有获取锁的线程来完成的,同步锁只能由一个线程获得,所以不需要cas操作

设置Head节点时不需要cas操作,设置head节点是有获取锁的线程来完成的,同步锁只能由一个线程获得,所以不需要cas操作,只需要把head节点的设置为原来节点的后继节点,并断开原head节点的next引用即可

ReentrantLock 的源码分析

ReentrantLock时序图

在这里插入图片描述
sync实际上是一个抽象的静态内部类,它继承了AQS来实现重入锁的逻辑,它能够实现线程的阻塞以及唤醒,但它并不具备 业务功能,所以在不同的同步场景中,会继承AQS来实现对应场景的功能 Sync有两个具体的实现类,分为
NofairSync:表示可以存在抢占锁的功能,没有公平性可言,新线程都有机会抢占锁
FailSync: 表示所有线程严格按照FIFO来获取锁

NofairSync

1.非公平锁和公平锁最大的区别在于不管有没有线程排队,则直接尝试cas获取锁
2.cas成功表示获取锁
3. CAS失败,调用acquire(1)走锁竞争逻辑

final void lock() {     
      if (compareAndSetState(0, 1))
         setExclusiveOwnerThread(Thread.currentThread());   
         else        
          acquire(1); 
          } 

cas的实现原理

protected final boolean compareAndSetState(int expect, int update) {     
// See below for intrinsics setup to support this     return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }

通过乐观锁的方式来做比较替换,如果当内存中state的值和预期值一样,则替换update,更新成功,否则更新失败;
这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作, 以及涉及到state这个属性的意义。

AQS.accquire

  1. 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
  2. 如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node添加 到AQS队列尾部
  3. acquireQueued,将Node作为参数,通过自旋去尝试获取锁。
public final void acquire(int arg) {     if (!tryAcquire(arg) &&         acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
 selfInterrupt(); }

NonfairSync.tryAcquire

是尝试获取锁,如果成功返回true,不成功返回false

protected final boolean tryAcquire(int acquires) {     return nonfairTryAcquire(acquires); } 

ReentrantLock.nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//获取当前状态
            if (c == 0) {//表示无锁状态
                if (compareAndSetState(0, acquires)) {//表示获取锁成功
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//如果是独占锁,则重入次数+1;
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

AQS.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;//tail是AQS中的队尾
        if (pred != null) {//队尾不为空,则AQS队列中存在节点
            node.prev = pred;//把当前的节点添加到tail后面
            if (compareAndSetTail(pred, node)) {//从过cas操作把该节点加入到AQS中,
                pred.next = node;//把原来的队尾指针指向新添加进来的节点
                return node;
            }
        }
        enq(node);//tail为空时,把node节点添加到同步队列
        return node;
    }

enq

enq通过自旋把当前节点加入到同步对列中

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

假如3个线程来抢占锁

AQS中的链表图
在这里插入图片描述

AQS.acquireQueued

通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给 acquireQueued方法,去竞争锁
1.获取当前节点的prev节点
2.如果prev节点为head节点,有机会去抢占锁,调用tryAcquire
3.抢占成功后,把获得锁的节点设置为head节点,并移除原来初始话的节点
4.如果获取锁失败,则根据waitStatus状态看是否要挂起线程

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//获取当前节点的前驱节点
                if (p == head && tryAcquire(arg)) {//如果是head节点,则去尝试获取锁
                    setHead(node); //获取锁成功,也就是 ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权 限
                    p.next = null; // 把原来的节点从队列中移除
                    failed = false;
                    return interrupted;
                }
                //ThreadA 可能还没释放锁,使得 ThreadB 在执 行 tryAcquire 时会返回 false 
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;//返回当前线程在等待过程中有没有被中断过
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire

如果ThreadA的锁还没有释放的情况下,ThreadB和ThreadC来争抢锁肯定是会 失败,那么失败以后会调用shouldParkAfterFailedAcquire方法 Node 有 5 中状态,分别是:CANCELLED(1),SIGNAL(-1) 、CONDITION(2)、PROPAGATE(-3)、默认状态(0) CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取 消该Node的结点, 其结点的waitStatus为CANCELLED,即结束状态,进入该状 态后的结点将不会再变化 SIGNAL: 只要前置节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程
CONDITION: 和Condition有关系,后续会讲解 PROPAGATE:共享模式下,PROPAGATE状态的线程处于可运行状态 0:初始状态 这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是 否应该被挂起。

  1. 如果ThreadA的pred节点状态为SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把CANCELLED状态的节点移除
  3. 修改pred节点的状态为SIGNAL,返回false.
    返回false不需要挂起该线程,返回true时,则需要调用parkAndCheckInterrupt 挂起该线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//前驱节点
        if (ws == Node.SIGNAL)//如果前置节点为 SIGNAL,意 味着只需要等待其他前置节点的线程被释放, 
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {//表示prev节点取消排队,直接移除该节点,
            /*
             * 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.
             */
             //利用cas操作把前驱节点设置为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt

使用LockSupport.park挂起当前线程编程WATING状态
Thread.interrupted,返回当前线程是否有被其他线程触发过中断请求,也就是 thread.interrupt();如果有中断请求,则该方法会返回中断表示true,并且对中断标识进行复位已经响应过中断请求,。如果返回true,意味 着在acquire方法中会执行selfInterrupt()

private final boolean parkAndCheckInterrupt() {     LockSupport.park(this);     
return Thread.interrupted();
 } 

selfInterrupt 标识如果当前线程在acquireQueued被中断过,则需要产生一个中断请求,原因是线程在调用acquireQueued方法时不会响应中断请求;

通过acquireQueued方法来竞争锁,如果ThreadA还在执行中没有释放锁的话, 意味着ThreadB和ThreadC只能挂起了。
在这里插入图片描述

锁的释放流程

ReentrantLock.unlock

public final boolean release(int arg) {     
if (tryRelease(arg)) { //释放锁成功        
 Node h = head; //得到 aqs 中 head 节点        
  if (h != null && h.waitStatus != 0)//如果 head 节点不 为空并且状态!=0.调用 unparkSuccessor(h)唤醒后续节点              unparkSuccessor(h);         
  return true;     
  }     
  return false; } 

ReentrantLock.tryRelease

这个方法可以认为是一个设置锁状态的操作,通过将state状态减掉传入的参数值 (参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它
的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时 候减掉1,同一个锁,在可以重入后,可能会被叠加为2、 3、 4这些值,只有unlock() 的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下 才会返回true。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;//获取head节点的下一个节点
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);//设置head的节点的状态为0

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {//如果下一个节点为 null 或者 status>0 表示 cancelled 状态
            s = null;
            //通过从尾部节点开始扫描,找到距离 head 最近的一个 waitStatus<=0 的节点 
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread); //next 节点不为空,直接唤醒这个线程即可 
    }

为什么在释放锁的时候是从 tail 进行扫描

  1. 将新的节点的prev指向tail
  2. 通过cas将tail设置为新的节点,因为cas是原子操作所以能够保证线程安全性
  3. t.next=node;设置原tail的next节点指向新的节点
    在这里插入图片描述
    在cas操作之后, t.next=node操作之前。 存在其他线程调用unlock方法从head 开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。 就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问 题。
    通过锁的释放,原本的结构就发生了一些变化。head节点的waitStatus变成了0, ThreadB被唤醒
    在这里插入图片描述
    设置新head节点的prev=null
    . 设置原head节点的next节点为null
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值