ReentrantLock加锁解锁的过程

简介

    ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种保证线程并发安全的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全.

与synchronized相比

    相同点

  1. 都是独占锁的实现
  2. 都是可重入锁;同一线程可以多次获得同一个锁
  3. 都保证了可见性和互斥性
  4. 阻塞等待中的线程来实现同步:也就是说当一个线程获得了锁,进入了同步代码块,其他访问的线程都必须阻塞在同步代码块外面进行等待,而线程阻塞和唤醒的代价是很高的(线程阻塞以后进入内核态,我们的JVM运行在用户态,当我们唤醒线程操作系统需要通过CPU要在用户态与内核态进行一轮切换,是一个非常重型的操作);

    不同点

比较的点 synchronized ReentrantLock
获得锁的方式 隐式锁,jvm控制加锁解锁 显式锁,手动加锁解锁
是否公平 非公平锁 既可以公平锁,也可以非公平锁,通过构造方法传入的boolean判断
锁的对象 锁的是对象,锁是保存在Mark Word里面,根据Mark Word数据标识锁当前的状态 锁的是线程,根据进入的线程和AQS内部维护的state同步器标识锁当前的状态

构造方法

在这里插入图片描述
在这里插入图片描述

    /**
     * 使用内部类Sync来保证同步
     * Sync继承AQS框架,有两个子类
     * NonfairSync:非公平锁
     * FairSync:公平锁
     */
    private final Sync sync;
    
    /**
     * 无参构造无参构造默认使用非公平锁
     */
    public ReentrantLock() {
    
    
        sync = new NonfairSync();
    }

    /**
     * 带参构造
     * @param fair true:公平锁
     * 			   false:非公平锁
     */
    public ReentrantLock(boolean fair) {
    
    
        sync = fair ? new FairSync() : new NonfairSync();
    }

在这里插入图片描述
ReentrantLock通过内部类Sync来保证线程安全,Sync继承AbstractQueuedSynchronized,还对该抽象类的部分方法做了实现;并且还定义了两个子类:

  1. FairSync 公平锁
  2. NonfairSync 非公平锁

    这两个类都继承Sync,相当于间接的继承了AbstractQueuedSynchronized,所以ReentrantLock同时具备公平与非公平两个特性

     AbstractQueuedSynchronized这个类里面维护了一个state属性,这个属性是int类型的,被volatile关键字修饰,记录的当前锁的状态,这个类里面还有一个内部类Node,Node是一个双向链表,AbstractQueuedSynchronized这个类里面记录着等待队列中的头节点与为节点.主要用于来实现等待队列.
在这里插入图片描述

在这里插入图片描述

    在上面的继承关系图上面还有一个类是非常重要的就是AbstractOwnableSynchronizer,这个类里面维护了一个属性:exclusiveOwnerThread,这个属性是Thread类型的,记录的就是当前拿到锁的线程;
在这里插入图片描述

加锁

/**
 * @Classname ReentrantLockDemo
 * @Description
 * @Date 2021/8/31 22:01
 * @Author fanqiechaodan
 */
@Slf4j
public class ReentrantLockDemo {
    
    

    public static void main(String[] args) {
    
    
        // 公平锁
        ReentrantLock lock = new ReentrantLock(true);
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
               try {
    
    
                    lock.lock();
                    log.info("加锁成功");
                } finally {
    
    
                    lock.unlock();
                }
            }).start();
        }
    }
}
  1. 会先调用ReentrantLock.lock()
    在这里插入图片描述
  2. 由于在创建ReentrantLock对象时,参数传的是true,所以下一步就会调用公平锁FairSync.lock()
    在这里插入图片描述
  3. 方法里面有接着调用AbstractQueuedSynchronizer.acquire(int arg);并且传了一个固定的参数1
    在这里插入图片描述
    3.1 acquire方法里面第一步就是调用tryAcquire(arg),并把参数1传了进去,看到本类中的tryAcquire方法并没有任何的实现,他的实现都是在子类中完成的,这个类中只负责定义具体的行为;到最后还是会调用FairSync.tryAcquire(),这个方法是进行尝试加锁,整个加锁的逻辑都在这里
    在这里插入图片描述
		// FairSync.tryAcquire()公平锁尝试加锁方法
        protected final boolean tryAcquire(int acquires) {
    
    
        	// 拿到当前线程
            final Thread current = Thread.currentThread();
            // 获取state当前锁的状态,
            int c = getState();
            if (c == 0) {
    
    
            	// 如果c为0,就代表当前可以获取锁
            	// 因为现在是公平锁,hasQueuedPredecessors():要先去查询有没有线程在排队,如果有当前线程是不是第一个
            	//compareAndSetState(0, acquires):使用cas的方式将state设置为acquires(这个参数就是1)
            	// 然后将AbstractOwnableSynchronizer这个类中的exclusiveOwnerThread属性设置为当前线程
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
    
    
                    setExclusiveOwnerThread(current);
                    // 所有操作都做完以后加锁成功
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
    
    
           		 // 如果c不等于0但是此时占用锁的线程就是当前线程
           		 // 对state+1然后设置state,代表ReentrantLock是具备可重入性的
           		 // 这段代码直接使用加号,本身就是线程安全的,
           		 // 因为有前面的判断会保证只有一个线程能进来
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                // 加锁成功
                return true;
            }
            // 当前锁已经被占用并且不是当前线程占用的,加锁失败
            return false;
        }

3.2 经过上一步加锁的方法,加锁成功的会去执行我们的业务代码,没有加锁成功的就会执行AbstractQueuedSynchronizer.acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

     //把当前线程创建一个节点,传了一个Node参数Node.EXCLUSIVE,上面有讲过是独占模式
    private Node addWaiter(Node mode) {
    
    
    	// 把当前线程当作构造参数新建一个Node
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 把尾节点的指针给pred
        Node pred = tail;
        // tail指针从来都没有被赋值过,
        // pred是一定为null的!=null里面的逻辑永远都不会走
        if (pred != null) {
    
    
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
    
    
                pred.next = node;
                return node;
            }
        }
        // 将节点插入等待队列
        enq(node);
        return node;
    }
    // 将节点插入等待队列的方法
    private Node enq(final Node node) {
    
    
     // 死循环(自旋)保证一定可以将当前节点插入到等待队列中
        for (;;) {
    
    
            Node t = tail;
            if (t == null) {
    
     // Must initialize
				// 如果队列没有进行初始化,就对队列进行初始化
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
    
    
             	// 自旋逻辑,队列初始化后一直会走else
                node.prev = t;
                // cas方式插入队列
                if (compareAndSetTail(t, node)) {
    
    
                    t.next = node;
                    return t;
                }
            }
        }
    }
	// 将创建好的节点传进来
    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)) {
    
    
                	// 加锁成功
                	// 把当前节点设置为头部节点,方法里面会把当前节点置为null
                	// 相当于从队列中删除掉当前节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire(p, node):判断一下当前节点是不是可以被唤醒的
                // 判断逻辑会执行两次
                    	// 首先第1轮循环、修改head的状态,修改成sinal=-1标记处可以被唤醒.
    					//第2轮循环,阻塞线程,并且需要判断线程是否是有中断信号唤醒的!
                // parkAndCheckInterrupt():将当前节点阻塞住,底层调用的LockSupport.park(this)实现的
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
    
    
            if (failed)
                cancelAcquire(node);
        }
    }

解锁

在这里插入图片描述
在这里插入图片描述

    public final boolean release(int arg) {
    
    
        if (tryRelease(arg)) {
    
    
        	// 解锁成功
            Node h = head;
            // 判断一下还有没有在等待的线程
            // 如果有看一下当前线程是不是可以唤醒的
            if (h != null && h.waitStatus != 0)
            // 如果有等待的线程并且可以被唤醒就唤醒线程
            // 底层调用的LockSupport.unpark(s.thread);
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

在这里插入图片描述

  • 解锁不管公平还是非公平使用的都是Sync.tryRelease(int releases),调用的时候参数传的都是1
        protected final boolean tryRelease(int releases) {
    
    
        	// 把state减掉1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
    
    
                free = true;
                // 把当前占有锁的线程置为null
                setExclusiveOwnerThread(null);
            }
            //将c设置为state
            setState(c);
            return free;
        }

公平锁和非公平锁的区别

在这里插入图片描述
在这里插入图片描述
通过对比公平锁与非公平锁的加锁代码,可以看得出来两个内部类的加锁方法基本上来说是一摸一样的,唯一不同的是,当发现锁没有被占用时:

  • 非公平锁:直接采用cas的方式设置锁的状态,并且将占用锁的线程设置为当前线程,然后返回加锁成功
  • 公平锁: 先执行!hasQueuedPredecessors()代码,这段代码主要是为了看一下等待队列中有没有正在等待的线程,如果有,那么判断一下当前线程是不是排在最前面的线程,如果是最前面的线程,就使用cas的方式设置锁的状态,并且将占用锁的线程设置为当前线程然后返回加锁成功
    可以获取锁的时候,非公平锁会马上获取锁,不会考虑等待队列中有没有比当前线程排队更靠前的线程,直接进行加锁;公平锁则会考虑等待队列中有没有比当前队列更靠前的线程,如果有当前线程就不能加锁.

猜你喜欢

转载自blog.csdn.net/qq_43135259/article/details/119898123