Java源码解读系列4—ReentrantLock(JDK1.8 )

1 概述

Java程序提供两种方法来实现锁功能1)synchronized关键字和2)lock接口,两者最大区别是synchronized是Java内置的关键字,是JVM层面的锁;而lock是java.util.concurrent包下面提供的工具,由Java代码编写的。在JDK1.6之前,synchronized是重量级锁,每次都会调用OS使线程进入阻塞状态;ReentrantLock是轻量锁,如果只是当单纯线程交替等场景,是不会触发OS的,性能优于synchronized关键字。现在两者性能已经差不多,未来随着JVM不断优化,synchronized性能不断提高,很大程度会超过lock接口。

2 AQS简介

队列同步器AbstractQueuedSynchronizer(AQS)使用一个int类型成员变量state表示同步状态,通过内置的FIFO队列实现多线程排队,是ReentrantLock框架的基础。

//同步队列容器
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    
    //内部节点
    static final class Node {
         //独占式
        static final Node EXCLUSIVE = null;
static final int SIGNAL    = -1;
        //等待状态,初始值为0;当waitStatus为-1时,说明后继节点得线程处于等待状态
        volatile int waitStatus;
        //前驱节点
        volatile Node prev;
         //后继节点
        volatile Node next;
        //当前节点存储的线程
        volatile Thread thread
        //线程状态
         volatile int waitStatus;
         ...
    }
    
    
    //AQS头指针
     private transient volatile Node head;
     
     //AQS尾指针
     private transient volatile Node tail;
     
     //同步状态,默认为0
     private volatile int state;
    
     //独占线程,用于锁重入场景,判读是否当前线程持有锁
     private transient Thread exclusiveOwnerThread;
    }

AQS通过一个FIFO队列,使没有抢占到锁的线程在队列中进行排队。head指向的node节点1是头节点,里面的线程永远为空,是因为代表的线程已经抢占到锁,不会被放在等待队列中
节点2才是真正意义上第一个在老老实实排队的线程。尾节点指向节点3,代表最后一个排队的节点。

在这里插入图片描述

3 加锁用例

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockApp {

   private static ReentrantLock reentrantLock = new ReentrantLock(true);

    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                testLock1();
            }
        };
        t1.setName("thread1");

        Thread t2 = new Thread(){
            @Override
            public void run() {
                testLock1();
            }
        };
        t2.setName("thread2");

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

    public static void testLock1(){
        reentrantLock.lock();
        System.out.println(Thread.currentThread().getName() + "获取锁了");
        try {

            Thread.sleep(2000);
            //锁重入
            testLock2();
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "释放锁了");
        reentrantLock.unlock();

    }

    public static void testLock2(){
        reentrantLock.lock();
        System.out.println(Thread.currentThread().getName() + "再次获取锁了");
        try {

            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "再次释放锁了");
        reentrantLock.unlock();

    }

}

打印结果:

thread1获取锁了
thread1再次获取锁了
thread1再次释放锁了
thread1释放锁了
thread2获取锁了
thread2再次获取锁了
thread2再次释放锁了
thread2释放锁了

从控制台可以看出,只有获取锁的线程执行完,才会轮到下一个线程。这是因为ReentrantLock是个独占锁,每次只允许一个线程拿到锁。
thread1在持有锁的线程同时,还可以直接再次获取锁,是因为发生了锁重入

4 构造函数

ReentrantLock由两种实现方式,无参构造函数默认是非公平锁,线程获锁的方式是随机的。而有参构造方法的入参为true时表示公平锁,是按照线程加锁的顺序获得锁,即FIFO方式。

 public ReentrantLock() {
        sync = new NonfairSync();
    }

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

5 公平锁的加锁机制

调用lock方法进行加锁,这里的1是次数

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

public final void acquire(int arg) {
         //tryAcquire返回值为ture,线程获获取到锁,并更改同步状态;
       // tryAcquire返回值为false,添加到等待队列
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

同步状态有3种可能的值 1) 0 没有被占用 2)1 被占用 3)n(n >1)同一个线程锁重入了n -1 次。当前线程尝试获取同步状态,如果同步状态C为0,则当前线程获得锁,被设置为独占线程,或者当前线程已经是独占线程,那么就发生锁重入。两种情况都不是,则同步状态被其他线程改变,当前线程需要排队。

  
 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
           
            //获取同步状态
            int c = getState();
            //同步状态为0,说明锁没有被占用
            if (c == 0) {
            //判断等待队列中是否有其他线程在排队
                if (!hasQueuedPredecessors() &&
                //修改同步状态值为1
                    compareAndSetState(0, acquires)) {
                    //设置拥有独占模式的线程为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            
             //同步状态值不为0,说明锁被占用,但是当前线程为独占线程,可以锁重入
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                    
                   //更新同步状态值  
                setState(nextc);
                return true;
            }
            
            //同步状态被其他线程改变,当前线程可能需要排队
            return false;
        }

hasQueuedPredecessors判断线程需不需要排队,返回结果true表示需要排队,false表示不需要

 public final boolean hasQueuedPredecessors() {
       // 尾节点
        Node t = tail; 
        // 头节点
        Node h = head;
        //临时节点
        Node s;
        //h == t :说明此时没有线程在队列里面排队,h和t有可能没有初始化或者两者刚初始化后,共同指向一个包含的线程为null的node
        
       //  h != t  有可能是1)h刚初始化,t还没初始化 2)队列里面有其他线程在排队
       
        //(s = h.next)== null :为true说明有其他线程已经执行完AQS的头节点和尾节点初始化操作,根据FIFO原则,当前线程需要排队
        
        //s.thread != Thread.currentThread():为true说明头节点的后继节点中存储的线不是当前线程,当前线程需要排队
        //h指向第一个节点,s此时是队列中的第二个节点
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

通过CAS操作,修改同步状态

protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

将线程设置为独占线程,用于锁重入场景,可以判断线程是否持有锁

protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

调用addWaiter将线程添加到等待队列中

private Node addWaiter(Node mode) {
      //初始化一个独占模式的节点
        Node node = new Node(Thread.currentThread(), mode);
              
       //尾节点赋值给pred
        Node pred = tail;
        
        //末尾节点不为空,说明他线程已经初始化head节点和tail节点
        if (pred != null) {
            //将node的前驱节点指向tail节点
            node.prev = pred;
             //将node设置为尾节点
            if (compareAndSetTail(pred, node)) {
             //旧的tail节点的后继指针指向node节点
                pred.next = node;
                //返回当前节点
                return node;
            }
        }
        //1)tail节点为空 或者 2)taili节点不为空但compareAndSetTail操作失败时,调用enq
        enq(node);
        return node;
    }

enq方法是将节点插入到等待队列中

private Node enq(final Node node) {
        //无限循环
        for (;;) {
            Node t = tail;
            //首次添加时,head和tail都为null,需要初始化
            if (t == null) { 
            //实列化一个空节点,通过CAS操作添加成头节点
                if (compareAndSetHead(new Node()))
                   //赋值给尾节点,此时两者共同指向空的node节点
                    tail = head;
            } else {
                //将node的前驱节点指向tail节点
                node.prev = t;
                //将node设置为尾节点
                if (compareAndSetTail(t, node)) {
                    //旧的tail节点的后继指针指向node节点
                    t.next = node;
                    //返回旧的tail节点
                    return t;
                }
            }
        }
    }

通过自选尝试获取锁,自选2次失败则线程会进入阻塞。从这里可以看出ReentrantLock并没有让获取不到锁的线程直接阻塞,而是通过FIFI等待队列,自旋等操作,尽量避免线程进入阻塞状态,降低线程状态切换的代价。

 
 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋
            for (;;) {
                //p为node节点的前继节点
                final Node p = node.predecessor();                      
                // 队头head指向的node里面得thread永远为空,因此第二个节点才是实际排队第一位的节点
                //如果前继节点为头节点,且成功更新同步状态,说名当前线程获得锁
                if (p == head && tryAcquire(arg)) {
                //将第二个节点设为头节点
                    setHead(node);
                    //释放旧的头节点
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                
                
                //同个线程自选2次(调用两次shouldParkAfterFailedAcquire方法)才会返回true
             
                if (shouldParkAfterFailedAcquire(p, node) &&
                //将线程阻塞
                    parkAndCheckInterrupt())
                    
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
     

调用shouldParkAfterFailedAcquire修改前继节点的等待状态,连续调用两次,就会进入parkAndCheckInterrupt()使线程进入阻塞状态

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
       //线程首次进来,其前继节点的为等待状态为0
        int ws = pred.waitStatus;
        
        //线程第二次调用shouldParkAfterFailedAcquire时,ws为-1,,返回true
        //线程第二次调用shouldParkAfterFailedAcquire才返回ture,是为了让线程多自选一次,尽量避免进入阻塞状态
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) 
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
             //线程第一次调用shouldParkAfterFailedAcquire时,前继节点的ws=0,通过cas将ws修改为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
//线程进入阻塞状态
private final boolean parkAndCheckInterrupt() {
         //通过unsafe类的park方法阻塞线程
        LockSupport.park(this);
        return Thread.interrupted();
    }

6 非公平锁的加锁机制

调用默认无参构造函数,使用非公平锁

 public ReentrantLock() {
        sync = new NonfairSync();
    }
    
 public void lock() {
        sync.lock();
  }

非公平锁比公平锁的加锁过程对比,就是简单粗暴

 final void lock() {
     //全部线程都能抢占,由于CAS是原子操作,因此只有一个线程能成功执行状态更新操作
       if (compareAndSetState(0, 1))
       //将抢占到锁的线程
             setExclusiveOwnerThread(Thread.currentThread());
       else
           //获取锁失败的线程就走公平锁那套,老老实实去排队
             acquire(1);
 }

7 锁的解锁机制

加锁时候入参也是1,表示加锁1次,同理解锁时候也是1,表示解锁1次

 public void unlock() {
        sync.release(1);
    }

更新同步状态,如果没发送

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //h != null 说明头节点指向包含空线程的node节点1
            //waitStatus !=0 说明node节点2处于阻塞状态,需要被唤醒
            if (h != null && h.waitStatus != 0)
                 //唤醒node节点2
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

执行同步状态减1操作后,同步状态值为0返回ture;大于0返回false

  protected final boolean tryRelease(int releases) {
            //同步状态减1
            int c = getState() - releases;
            //判断是否独占线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //为0说明没有被占用
            if (c == 0) {
                free = true;
                //将独占线程指向空
                setExclusiveOwnerThread(null);
            }
            //修改同步状态值
            setState(c);
            //同步状态为0,free为true;同步状态大于0,说明发生了锁重入,当前线程还是持有锁,free返回false  
            return free;
        }

更新同步状态值

 protected final void setState(int newState) {
        state = newState;
    }

唤醒处于阻塞状态的第一个真正在排队的节点

 private void unparkSuccessor(Node node) {
       //获取等待状态
        int ws = node.waitStatus;
        //等待状态小于0,说明后继节点处于阻塞状态
        if (ws < 0)
           //将node节点的等待状态修改为初始值0
            compareAndSetWaitStatus(node, ws, 0);
 
         //获取当前节点的后继节点
        Node s = node.next;
       
      /后继节点为空或被取消
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从后往前找,找到最靠前的等待状态不大于0的节点
            for (Node t = tail; t != null && t != node; t = t.prev) 
                if (t.waitStatus <= 0)
                   //将找到的目标节点赋值s
                    s = t;
        }
           //后继节点不为空,唤醒后继节点
        if (s != null)
            LockSupport.unpark(s.thread);
    }

通过UNSAFE非安全类执行唤醒操作

 public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

8 参考文献

  1. JDK7在线文档
    https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
  2. JDK8在线文档
    https://docs.oracle.com/javase/8/docs/api/
  3. Bruce Eckel,Java编程思想 第4版. 2007, 机械工业出版社
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值