【java并发编程】ReentrantLock源码分析

1.为什么使用锁,不使用锁会有什么影响?

public class Test {
    public static int count=0;
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        for(int i = 0 ;i<10000;i++){
            new Thread(()->{
                try{
                    lock.lock();
    				//count++不是原子操作
                    count++;
                }finally {
                    //unlock一定要在finally中,避免死锁
                    lock.unlock();
                }
            }).start();
        }
        try {
            Thread.sleep(2000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

​ 当count++lock.lock()/lock.unlock()中间时,输出的count就是想要得到的结果;

​ 不加lock的时候多执行几次,经常会比预期值小。因为count++不是原子操作,这里就体现了lock的作用。

2.ReentrantLock源码分析

2.1思考实现原理

ReentrantLock作用如此强大,他主要完成了几个功能点。

  1. 记录锁是否被占用
  2. 记录占用锁的线程(处理锁重入)
  3. 通过某种方式记录锁的顺序(要考虑公平锁)
  4. 所有的锁进入阻塞状态。

大概完成了这么几个功能点。如此就可以主要考虑ReentrantLock是如何实现这些功能的。

ReentrantLock类图
在这里插入图片描述
​ 如此所示,很明显,ReentrantLock有三个内部类,公平锁类FairSync和非公平锁类NonfairSync而且这两个类都继承了Sync类,重写了Sync的lock方法。

Sync类继承AbstractQueuedSynchronizer类,AbstractQueuedSynchronizer类通常说AQS,实现线程排队阻塞的一个机制。AQS主要是维护了互斥变量,维护双向链表,线程阻塞等。

new ReentrantLock()是非公平锁,new ReentrantLock(true)是公平锁。

ReentrantLock构造器源码:

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

2.2ReentrantLock源码

在这里插入图片描述

2.2.1 NonfairSync.lock()方法

ReentrantLock 中有抽象类Sync,根据构造方法区分执行FairSync还是NonfairSync的实现。

NonfairSync实现为例。

  final void lock() {
	//在state预期值为0的时候,修改值为1,此方法线程安全
      if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

compareAndSetState方法类似数据库中的乐观锁,预计值为0的时候修改值为1,如果修改成功,则成功获取锁,返回true,并通过setExclusiveOwnerThread方法记录获取锁的线程;如果修改失败,说明锁已经被占用,通过acquire(1)方法尝试获得锁。

扫描二维码关注公众号,回复: 11452782 查看本文章

2.2.2 compareAndSetState(0,1)方法

 protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
     //如果内存中的值为expect则修改为update,修改成功返回true,修改失败返回false
     return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    // stateOffset 等于state的值
    //state=0表示没有线程获取锁,state=1表示有线程获取锁 state>1表示锁重入
  stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));

compareAndSwapInt属于CAS相关(乐观锁。ConcurrentHashMapConcurrentLinkedQueue也有用CAS来实现乐观锁),Unsafe类提供了手动管理内容的能力,可以直接对内容进行处理。

​ 至此,已经了解了ReentrantLock通过AbstractQueuedSynchronizer.state判断是否占用锁,state=0表示没有线程获取锁,state=1表示有线程获取锁 state>1表示锁重入;

​ 通过AbstractOwnableSynchronizer.exclusiveOwnerThread记录占用锁的线程。

2.2.3 acquire(1)方法

    public final void acquire(int arg) {
        //判断是否是重入锁
        if (!tryAcquire(arg) &&
            //addWaiter加入队列
            //acquireQueued线程阻塞
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //响应线程中断
            selfInterrupt();
    }

2.2.4 tryAcquire(arg)方法

  final boolean nonfairTryAcquire(int acquires) {
      		// 获取当前线程
            final Thread current = Thread.currentThread();
     		 // 通过state状态
            int c = getState();
      		// 如果没有线程获取锁
            if (c == 0) {
                //通过CAS操作获取锁,如果获取成功,则设置占用锁的线程
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
      		// 判断是否是锁重入getExclusiveOwnerThread 方法返回占用锁的线程
            else if (current == getExclusiveOwnerThread()) {
                //state++
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //设置state的值
                setState(nextc);
                return true;
            }
      		//如果锁已经被其他线程占用,返回false
            return false;
        }

2.2.5 addWaiter(Node.EXCLUSIVE)方法

既然获取锁失败,那么就要将线程记录起来,如下所示,通过链表的形式将所有线程保存起来(通过附录查看Node属性)。

   /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */ 
// mode 传入Node.EXCLUSIVE 表示互斥锁   Node.SHARED 表示共享锁(读写锁中的读锁)
	private Node addWaiter(Node mode) {
    	// 创建一个节点保存当前线程
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
    	//获取节点的尾节点。
    	//如果是线程B是第一个阻塞的节点,这里是空值,通过enq方法进行设置head节点和tail节点。
        Node pred = tail;
        if (pred != null) {
            //如果线程C在线程B之后,线程C执行到这里,那么这里的pred是B
            node.prev = pred;
            //这时C为最后一个节点,设置尾节点为C
            if (compareAndSetTail(pred, node)) {
                //如果设置成功,设置B节点的next节点为C
                pred.next = node;
                return node;
            }
        }
    	//第一个阻塞的线程进入这里
    	//当然这里可能B和C同时进入enq方法
        enq(node);
        return node;
    }

2.2.5 enq(node)方法

//这里可能很多线程一起进入。
private Node enq(final Node node) {
    //for(;;)和while(true)效果是一样的,但是一般是用for(;;)因为指令少
        for (;;) {
            //获取尾节点
            Node t = tail;
            if (t == null) { // Must initialize
                //这里又是CAS操作,预计head节点为空时,修改值为new Node()
                //不管几个线程执行此方法,但是只有一个线程能执行成功。也就是第一个阻塞的线程
                if (compareAndSetHead(new Node()))
                    //创建头节点成功之后,因为此时只有一个节点,所以这个节点即使头节点也是尾节点。
                    tail = head;
            } else {
                //如果只有B线程进入enq方法,第一次循环设置头节点,尾节点。
              	//第二次循环将线程B设置为尾节点,并且把头节点的next节点设置程B
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

这里比较复制,如下所示:
在这里插入图片描述
通过A,B,C同时执行lock.lock()方法,A最快,成功获取锁,线程B,C阻塞;

​ B和C都会执行addWaiter方法,假如B的速度比C的速度快,而且在B已经执行完addWaiter方法之后,C才进入addWaiter方法;

​ 那么B会执行到enq方法中进行for循环,第一次循环创建头节点,尾节点,第二次循环将B节点和头节点进行关联;
在这里插入图片描述

2.2.6 acquireQueued方法

// node为当前线程节点的前一个节点
// arg为1,表示想要修改state=1进行抢占锁
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取当前节点的prev节点
                final Node p = node.predecessor();
                //如果当前节点的prev节点是head节点,证明当前线程马上就要获取到锁。
                // tryAcquire 方法与2.2.4方法效果一致
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //判断是否阻塞线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                // 清理状态
                cancelAcquire(node);
        }
    }

2.2.7 shouldParkAfterFailedAcquire方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
    	// SIGNAL状态说明线程准备好阻塞,等待唤醒
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            // wx>0说明放弃获取锁,通过循环将这些节点从链表中剔除。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //通过CAS尝试修改pred状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

2.2.8 parkAndCheckInterrupt方法

    private final boolean parkAndCheckInterrupt() {
        //阻塞当前线程 this表示当前线程
        LockSupport.park(this);
        // 清理中断状态
        return Thread.interrupted();
    }

附录:

AbstractQueuedSynchronizer与Node的关系。

 public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    	    /**
    			省略其他代码
 		   **/
 		   //记录头节点
 	      private transient volatile Node head;
 	      //记录尾节点
          private transient volatile Node tail;
          //记录锁状态
          private volatile int state;
          static final class Node {
          	//记录前一个节点
             volatile Node prev;
             // 记录后一个节点
             volatile Node next;
             //取消获取锁
             static final int CANCELLED =  1;
             //准备好阻塞,等待线程唤醒
             static final int SIGNAL    = -1;
             // 指示线程正在等待条件
             static final int CONDITION = -2;
             // 共享锁中用到
             static final int PROPAGATE = -3;
          }
    }

数据库乐观锁简单展示:

场景:商品表中有有一个商品,库存为1,如果有N个线程同时查到还有一个商品,进行下单,就会有超卖等现象,必须加锁进行控制。

id goodsname goodsnum version
1 手机 1 1
  1. 线程A,B,C同时查到商品表中查到还有手机一部。
  2. 三个线程减库存操作。
  3. 线程A通过执行 update t_goods set goodsnum=goodsnum-1,version=version+1 where id=1 and version=1 and goodsnum>0 修改成功。这时线程B,C修改的时候version已经变成了2,不满足where条件,所以修改失败。
  4. 线程A修改成功,可以进行支付操作,线程B,C修改库存失败,报异常说明库存不足。

LockSupport.park/unpark

​ 关于线程阻塞并唤醒,最快会想到wait/notify组合。但是在此场景有缺陷,notify只能随机唤醒一个线程,notifyAll唤醒所有线程,park/unpark可以精准的实现唤醒某一个线程。

猜你喜欢

转载自blog.csdn.net/qq_30285985/article/details/103949682