首页 > 技术文章 > JAVA多线程(七) ReentrantLock原理分析

hlkawa 原文

JUC是JDK中提供的并发工具包,里面提供了很多并发编程中很常用的实用工具类,比如atomic原子操作、比如lock同步锁、fork/join、CountDownLatch(信号量)、Semaphore (计数器)等。

Lock锁基本的实现

void lock() ---获取锁 如果没有获取到锁则阻塞等待。
​
void lockInterruptibly() --- 和lock一样 但是可以阻塞线程 可以中断
​
tryLock() ---非阻塞式获取锁,如果获取到锁则返回true,没有获取到锁返回false
​
tryLock(timeout, TimeUnit timeUnit) --- 带有超时时间获取锁
​
void unlock() ---释放锁

锁具有的特性

悲观锁

悲观锁比较悲观,当多个线程对同一行数据实现修改的时候,最后只有一个线程才能修改成功,只要谁能够对获取该到行锁则其他线程时不能够对该数据
做任何修改操作,且是阻塞状态。 比如for update 或者事务开启了事务但是没有提交事务。

乐观锁

乐观锁比较乐观,通过预值或者版本号比较,如果不一致性的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,
效率比较高。

公平与非公平锁

公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到, 采用队列存放 
类似于吃饭排队。 非公平锁:不是根据根据请求的顺序排列, 通过争抢的方式获取锁。 ​
new ReentramtLock(true) --- 公平锁 new ReentramtLock(false) --- 非公平锁

独占锁与共享锁

独占锁:在多线程中,只允许有一个线程获取到锁,其他线程都会等待。
共享锁:多个线程可以同时持有锁,例如ReentrantLock读写锁。读读可以共享、写写互斥、读写互斥、写读互斥。

锁的可重入性

在同一个线程中锁可以不断传递的,可以直接获取。

lock与synchronized区别

1.Lock基于AQS封装的锁 结合CAS + LockSupport实现 ,锁的升级为轻量锁和重量锁
2.Synchronized是基于C++虚拟机封装 结合Monitor C++对象实现,所得升级为偏向锁,轻量级锁,短暂自旋,重量级锁

Lock公平锁与非公平锁

FairSync --- 公平锁
NonfairSync --- 非公平锁
父类都是 AbstractQueuedSynchronizer

Lock底层实现原理

AQS + LockSupport + CAS实现

LockSupport用法

LockSupport可以挂起或者唤醒线程,是UnSafe类实现的
 public static void park() --- 阻塞当前线程
 public static void unpark(Thread thread) --- 恢复当前线程

LockSupport与Wait/notify区别

1.park()与unpark()方法:只要线程存在许可证,不管是在park()方法前或者调用park()方法后拥有,线程都会继续执行。
2.wait()与notify()方法:wait()方法必须要在notify()方法之前执行,否则wait()方法错失信号,进入无限等待状态,
从而产生死锁

基于CAS + LockSupport手写lock锁

public class BrianLock {

    private transient Thread currentThread;

    private ConcurrentLinkedDeque<Thread> waitThreads = new ConcurrentLinkedDeque<>();
    
    private AtomicInteger state = new AtomicInteger(0);

    public void lock() {
        if (compareAndSetState(0, 1)){
            this.currentThread = Thread.currentThread();
        } else {
            acquire(1);
        }
    }

    private void acquire(int acquires) {
        // 将当前线程放入队列
        Thread c = Thread.currentThread();
        waitThreads.add(c);
        // retry 再尝试获取锁
        if(compareAndSetState(0,acquires)){
            // 获取到锁,则从队列移除
            waitThreads.removeLastOccurrence(c);
            return;
        }

        // 将当前线程挂起
        LockSupport.park();
    }

    public void unlock() {
        if(Thread.currentThread() != currentThread){
            throw  new RuntimeException("释放锁异常,不是当前线程");
        }
        if(compareAndSetState(1,0)){
            // 从等待队列唤醒一个线程
            Thread nextThread = waitThreads.poll();
            if(nextThread != null) {
                LockSupport.unpark(nextThread);
            }
        }
    }

    private boolean compareAndSetState(int expect, int update) {
        return state.compareAndSet(expect,update);
    }

    public static void main(String[] args) throws InterruptedException {
        BrianLock lock = new BrianLock();
        Thread t1 = new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName());
            lock.unlock();
        }, "A1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName());
            lock.unlock();
        }, "A2");

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

AQS基本的概念

AQS全称为AbstractQueuedSynchronizer 是一个抽象同步队列,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,
常见的有:ReentrantLock、CountDownLatch等。 AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。 AQS底层的实现 结合CAS
+ compareAndSwapInt实现 

ReentrantLock源码解读

 

NonfairSync 非公平锁图解

FairSync 公平锁图解

公平锁与非公平锁最大的区别:就是在做cas操作的是时候,多了一步 hasQueuedPredecessors() 方法的判断,拿到Node链表中的头节点的next节点来带到公平竞争排队的效果

 

AQS核心参数

1. 内部类的Node链表,用来存放等待的线程

2. head 头结点 等待队列的头结点

3. tail 尾结点

4. state 锁的状态 0 无锁、1获取到锁, 当前线程重入则state不断加1,当调用unlock方法时 state会减1

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {

    private static final long serialVersionUID = 7373984972572414691L;
    
    protected AbstractQueuedSynchronizer() { }
    
    static final class Node {...省略简写...}

   
    private transient volatile Node head;

  
    private transient volatile Node tail;

  
    private volatile int state;
  
  //...省略简写...
  }

Node链表采用双向链表的形式存放正在等待的线程

static final class Node {
        
        static final Node SHARED = new Node();
       
        static final Node EXCLUSIVE = null;
volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter;

     //...省略简写...
}

 waitStatus状态

     /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
  
// 状态说明

  CANCELLED 值为1,表示当前的线程被取消;

  SIGNAL 值为-1,释放资源后需唤醒后继节点;

  CONDITION 值为-2, 等待condition唤醒;

  PROPAGATE 值为-3,工作于共享锁状态,需要向后传播,比如根据资源是否剩余,唤醒后继节点;

  值为0,表示当前节点在sync队列中,等待着获取锁。

thread等到锁的线程

lock方法实现原理

以非公平锁为例,当线程t1最先调用lock()方法获取锁时,是通过compareAndSetState() 方法加锁,cas(0,1),state状态值修改为1

 compareAndSetState()方法底层调用unsafe.compareAndSwapObject()实现加锁,此时t1线程获取锁成功则调用setExclusiveOwnerThread(Thread.currentThread())方法,标记获得锁的线程为t1

假如此时又来了t2线程也来调用lock()获取锁,很显然现在state状态值为1, compareAndSetState(0, 1)获取锁失败,走方法acquire(1)

 acquire(1)方法会调用tryAcquire()再次尝试获取下锁,最终走到方法nonfairTryAcquire(), 显然 现在current线程为t2,同时state值为1,所以尝试获取锁失败,返回false

返回false后,紧接者调用方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先走方法addWaiter(Node.EXCLUSIVE) ,EXCLUSIVE表示线程独占

 如果compareAndSetTail() 通过cas方式绑定节点失败,则调用enq() 不断自旋重新节点的双向绑定

 此时addWaiter() 方法执行完,则走外部方法acquireQueued()将当前线程t2 加入到等待锁的链表中,也是通过 for(;;) 不断自旋,通过查看predecessor()方法可以知道p节点,时当前节点的prev节点,如果p==head节点则表示当前链表为空,所以后面紧跟tryAcquire()方法直接尝试加索

 显然现在链表不为空(因为在addWaiter()方法里面已经实现了t2线程加入到链表中),所以继续走到方法shouldParkAfterFailedAcquire(Node pred, Node node)

 

现在因为waitStatus现在还都是默认值0,所有走到compareAndSetWaitStatus(),将perd waitSattus更改为-1,返回false, 然后继续自旋走shouldParkAfterFailedAcquire(Node pred, Node node)方法,因为此时pred的waitStatus 为 -1(即阻塞),直接返回ture, 然后继续走到parkAndCheckInterrupt()方法

parkAndCheckInterrupt()方法直接调用LockSupport.park(this)方法挂起当前线程,假如此时线程1调用unlock释放锁,即下面讲到的unlock()方法

unlock方法实现原理

 t1线程,unlock()方法直接走到AQS里面的release(int arg)方法

通过走tryRelease(arg)将锁状态改为0,同时重置获取锁的thread为空并返回true

 

 此时t2线程还在node链表中排队等待获取锁,所以h不为空(head=tail),所以继续走到方法unparkSuccessor(h)

 通过head节点的next,获取到下一个几点,并调用LockSupport.unpark(s.thread),唤醒该线程也就是t2线程

AQS中为什么头结点是为空的

头结点可以简单理解就是可以不用参加排队,表示已经获取到锁的线程,已经在aqs类中已经记录绑定获取到锁的线程,
所以head结点直接设置为null,防止浪费空间内存。

推荐阅读