ReentrantLock 和 Condition

102 阅读4分钟

ReentrantLock

ReentrantLock 的基础使用

    public static void main(String[] args) throws InterruptedException {
        // 构造函数不传参数为非公平锁,传true为公平锁
        ReentrantLock reentrantLock = new ReentrantLock();

        Thread thread1 = new Thread(() -> {
            try {
                System.out.println("t1 start");
                // 正常加锁
                reentrantLock.lock();
                Thread.sleep(5000);
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 正常解锁
                reentrantLock.unlock();
            }
        });
        Thread thread2 = new Thread(() -> {
            boolean whether = false;
            try {
                System.out.println("t2 start");
                // 尝试加锁 5S, true 获取到所,false 就是没获取到
                whether  = reentrantLock.tryLock(5, TimeUnit.SECONDS);
                System.out.println("t2 end");
            } catch (InterruptedException e) {
                System.out.println("tryLock中断了");
            } finally {
                if (whether) {
                    reentrantLock.unlock();
                }
            }
        });
        thread1.start();
        thread2.start();
        Thread.sleep(100);
        // 中断 thread2 会抛出 InterruptedException,但是就不会继续执行 t2 end
        // 如果不中断五秒后会执行 t2 end
        thread2.interrupt();
    }

ReentrantLock 和 synchronized 的区别

  1. ReentrantLock 可以尝试获取锁,获取不到也可以继续往下执行
  2. ReentrantLock 可以为公平锁。
    • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。保证老的线程排队使用锁,新线程仍然排队使用锁。
    • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。
  3. ReentrantLock 可中断,通过 tryLock 和 lockInterruptibly 方法加锁,可以被中断,中断之后抛出 InterruptedException,可以捕获 InterruptedException 进行对应逻辑处理
  4. ReentrantLock 是 api 层面解决同步问题,而 synchronized 是关键字,在 JVM 层面实现的加解锁

ReentrantLock 结构

源码解析

  1. 在 new ReentrantLock 的时候,会判断创建公平锁还是非公平锁
    Sync 继承了 AbstractQueuedSynchronizerAQS):CAS + volatile + LockSuport。
 public ReentrantLock(boolean fair) {
     	// fair 为 true 就是公平锁,否则就是非公平锁
     	// 不同的锁加锁方式不一样
        sync = fair ? new FairSync() : new NonfairSync();
 }
  1. lock 方法就是执行 sync.lock(),而 sync 就是刚开始创建的非公平锁或者公平锁
  2. 加锁解锁其实就是对 state 属性的操作,state = 0 说明没锁,state > 0 就说明被线程抢到锁了。 state 是 AQS 十分重要的属性。

lock 方法

这里主要写 公平锁 FairSync 的 lock 方法。
非公平锁(NonfairSync)的 lock 方法大部分方法与 公平锁的 lock 方法一样
最大的区别就是:尝试获取锁(tryAcquire)的时候,如果这时候刚好锁被释放,那么不管队列里有没有线程,直接就获取到锁,如果也获取不到,才会放到队列里去。

主要就是两个方法:tryAcquire() 和 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

  • tryAcquire():尝试获取锁,修改 state 的值,大于 0 为已加锁
  • acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):让该线程入队,进入 park 状态,等待 unpark
 final void lock() {
     // 只执行 acquire 方法
     acquire(1);
 }

 public final void acquire(int arg) {
     	//1、尝试获取锁
     	//2、入队
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果线程被中断或别的情况,才会执行这个方法,调用线程的 interrupt 方法
            selfInterrupt();
 }

首先看 tryAcquire 方法

/**
* Fair version of tryAcquire.  Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 1.hasQueuedPredecessors() 就是判断当前线程是否需要排队,如果不需要,就继续往下执行
        // 2.compareAndSetState 是用 CAS 的方式设置 state(锁的同步状态),防止其他线程已经更改		 //	过这个状态
        // 3. setExclusiveOwnerThread 就是设置当前占用锁的线程是当前线程,到这就加锁成功
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 当前拿到锁的线程是这个线程,重新设置state 的值,一般是 state + 1,表示重入
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

如果拿到锁就返回了,如果拿不到执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

// 创建 Node
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;
        if (pred != null) {
            // *****
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
    	// AQS 里的 tail 和 head 本来都是空的,这个是创建 head 和tail,如果被别的线程创建了 head
    	// 就会执行上面 if (pred != null) 里同样的逻辑
        enq(node);
        return node;
}
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)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 上面那一步没有获取到锁,就尝试 park 线程,会执行这个方法一次,再重新执行 for 	       			 // 循环
                
                // 第一次:告诉前置节点释放锁的时候通知一下自己
                // 即 CAS 方式设置前置节点的 waitStatus = -1,也许前置节点刚释放锁
                // 当然把 waitStatus 设置成功之后,可能前置节点变为 head 节点了,所以接下来还会进				   // 行第二次操作,那样就不会在执行第二次这个方法了
                
                // 第二次:已经告诉前置节点释放锁的时候通知自己,那么可以安心 park,也不需要担心执				 // 行到这一步的时候,前置节点不仅变成了 head 而且执行完并且已经释放锁了
                // 因为 LockSuport.unpark执行后,这个线程处于 start 后的状态,就不会再 park 了
                // 具体见 unlock 方法,waitStatus 不为 0 会唤醒后一个节点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 调用 LockSuport.park 阻塞线程
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 线程被中断等情况、而且没获取到锁退出死循环执行的方法
            if (failed)
                cancelAcquire(node);
        }
    }

tryLock 方法

  • tryLock() 是非公平锁的 tryAcquire() 实现
  • tryLock(long timeout, TimeUnit unit) 非公平锁和公平锁是不同的,只是阻塞的时候调用的是 LockSuport.parkNanos 方法,到指定时间获取不到就返回了

unlock 方法

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

	// release 方法
   public final boolean release(int arg) {
       	// 尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            // 如果当前节点的 waitStatus != 0,唤醒后置节点
            // 如果没有后置节点入队列,那么waitStatus 就是0 这里只唤醒后置节点
            // 加锁的时候把 waitStatus 设置为 -1 ,在这里用上了
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

  tryRelease 尝试释放锁,就是 state - 1,所以如果连续两次 lock,那么释放的时候就得调用两次 unlock 不然释放失败,别的线程再次尝试加锁还是会被阻塞。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
}

Condition

  使用await需要先获取到了锁才可使用。
  Condition自己也维护了一个链表,如果await,那么当前线程释放锁,放入自己的链表中,等到singal之后,会释放,被唤醒,继续执行下面的代码。

Condition 源码解析

Condition 使用

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("我进入了T1!");
                // 阻塞
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println("释放T1锁!");
            }

        });

        Thread t3 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("我进入了T3!");
                // 阻塞
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println("释放T3锁!");
            }

        });
        t3.start();
        t1.start();

        Thread.sleep(2000);

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("我进入了T2!");
                // 唤醒调用过 condition.await 方法的一个线程
                condition.signal();
                System.out.println("我是T2,我唤醒了T1或T3,T1或T3进入等待状态!");
                // 唤醒全部线程
                //condition.signalAll();
            } finally {
                lock.unlock();
                System.out.println("释放T2锁!");
            }
        });
        t2.start();
    }

和 notify 的区别

  notify 唤醒的线程是随机的一个线程,如果有消费者和生产者,Condition 可以根据不同的 Condition 只唤醒消费者或者生产者。