天天看点

手撕Java类 ReentrantLock

一种可重入互斥锁,具有与使用同步方法和语句访问的隐式监视器锁相同的基本行为和语义,但具有扩展功能。

可重入锁归最后一个成功锁定的线程所有。当锁不属于其它线程时,请求锁将会立即返回,并成功获取锁。如果当前线程已拥有该锁,该方法立即返回(可重入性)。可以使用

isHeldByCurrentThread()

getHoldCount()

方法来检测。

该类的构造函数接受一个可选的公平性参数。当设置为true时,在争用下,锁倾向于授予对等待时间最长的线程的访问权。否则,此锁不保证任何特定的访问顺序。使用许多线程访问的公平锁的程序可能会表现为较低的总体吞吐量(即更慢;通常比那些使用默认设置的要慢得多),但是在获得锁和保证不会饿的时间上只有较小的差异。但是请注意,锁的公平性并不保证线程调度的公平性。因此,使用公平锁的多个线程中的一个可能会在其他活动线程没有进展或当前没有持有锁的情况下连续多次获得该锁。还要注意,非定时tryLock()方法不支持公平设置。如果锁可用,即使其他线程正在等待,它也会成功。

建议在调用

lock

后立即跟

try

代码块

class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    public void m() {
 		lock.lock();  // block until condition holds
        try {
            // ... method body
        } finally {
            lock.unlock();
        }
    }
}
           

该类的序列化与内置锁的行为方式相同:反序列化锁处于解锁状态,而与序列化时的状态无关。

此锁最多支持同一线程的2147483647个递归锁。试图超过此限制将导致锁定方法抛出错误。

1. 简单使用

情形:多个线程更新同一数据

ReentrantLock lock = new ReentrantLock();
        for (int i = 0; i < 10; i++) {
            final int tempI = i;
            executorService.execute(() -> {
                lock.lock();
                try {
                    // 更新数据
                    data = data + 1;
                    System.out.println("i 值为:" + tempI + ", data 值为:" + data);
                } catch (Exception e){
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
        }

// 输出结果
i 值为:0, data 值为:1
i 值为:1, data 值为:2
i 值为:3, data 值为:3
i 值为:4, data 值为:4
i 值为:5, data 值为:5
i 值为:6, data 值为:6
i 值为:7, data 值为:7
i 值为:2, data 值为:8
i 值为:9, data 值为:9
i 值为:8, data 值为:10
           

通过运行结果可以发现,最终能够保证数据的正确性。

这里也提供了一些方法来监控锁的状态,但需要注意有些方法需要在持有锁的情况下调用;可以通过文档抛出的异常说明察觉到这些不同。

2. 复杂情形 

  1. 公平与非公平的

    简单的修改上面代码:

    ReentrantLock lock = new ReentrantLock(true);
    ...
        
    // 输出结果
    i 值为:0, data 值为:1
    i 值为:1, data 值为:2
    i 值为:2, data 值为:3
    i 值为:3, data 值为:4
    i 值为:4, data 值为:5
    i 值为:5, data 值为:6
    i 值为:6, data 值为:7
    i 值为:7, data 值为:8
    i 值为:8, data 值为:9
    i 值为:9, data 值为:10
               
    使用公平锁输出的结果 i 值能够保持同样的顺序,但是使用不公平锁会得到不同的顺序。
  2. 可中断与不可中断的请求锁

    请求锁:

    • 如果锁不被其他线程持有,则获取锁,并立即返回,将锁持有计数设置为1。
    • 如果当前线程已经持有锁,那么持有计数将增加1,方法立即返回。
    • 如果锁由另一个线程持有,则当前线程将出于线程调度目的而禁用,并处于休眠状态,直到获得锁,此时锁持有计数被设置为1。
    • (针对可中断的请求锁)如果当前线程在请求锁时,设置其中断状态或在获取锁时中断,然后会抛出

      InterruptedException

      并清除当前线程的中断状态。由于可中断的请求锁方法是一个显式的中断点,所以优先响应中断而不是正常的或可重入的锁获取。
    情形一:A 线程一直持有锁,当 B 线程通过不可中断来请求锁,尝试中断 B 线程,观察 锁的 等待队列中是否还有该线程;
    ReentrantLock lock = new ReentrantLock();
            executorService.execute(() -> {
                lock.lock();
                try {
                    while (true){
                        // 由于获取等待队列中的线程是 `protected`, 所以只能使用该方法来判断
                        System.out.println("一直持有锁,等待队列长度:" + lock.getQueueLength());
                        Thread.sleep(10000);
                    }
                } catch (Exception e){
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            Thread thread = new Thread(() -> {
                lock.lock();
                try {
                    while (true){
                        System.out.println("持有锁,等待队列长度:" + lock.getQueueLength());
                        Thread.sleep(100000);
                    }
                } catch (Exception e){
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            thread.start();
            System.out.println("中断前的中断标志:" + thread.isInterrupted());
            thread.interrupt();
            System.out.println("中断后的中断标志:" + thread.isInterrupted());
    
    // 输出结果
    一直持有锁,等待队列长度:0
    中断前的中断标志:false
    中断后的中断标志:true
    一直持有锁,等待队列长度:1
    一直持有锁,等待队列长度:1
    一直持有锁,等待队列长度:1
    ...
               

    可以看出,线程中断以后,还是不会放弃请求锁。

    情形二:A 线程一直持有锁,当 B 线程通过可中断来请求锁,尝试中断 B 线程,观察 锁的 等待队列中是否还有该线程;

    稍微修改一下上面的代码:

    ReentrantLock lock = new ReentrantLock();
            executorService.execute(() -> {
                lock.lock();
                try {
                    while (true){
                        System.out.println("一直持有锁,等待队列长度:" + lock.getQueueLength());
                        Thread.sleep(10000);
                    }
                } catch (Exception e){
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            Thread thread = new Thread(() -> {
                try {
                    lock.lockInterruptibly();
                    while (true){
                        System.out.println("持有锁,等待队列长度:" + lock.getQueueLength());
                        Thread.sleep(100000);
                    }
                } catch (Exception e){
                    System.out.println("捕获到异常:" + e.getClass().getName());
                    System.out.println("响应中断后的中断标志:" + Thread.currentThread().isInterrupted());
                } finally {
                    lock.unlock();
                }
            });
            thread.start();
            System.out.println("中断前的中断标志:" + thread.isInterrupted());
            thread.interrupt();
            System.out.println("中断后的中断标志:" + thread.isInterrupted());
    // 输出结果
    Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
    一直持有锁,等待队列长度:0
    中断前的中断标志:false
    中断后的中断标志:true
    捕获到异常:java.lang.InterruptedException
    响应中断后的中断标志:false
    	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
    	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
    	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
    	at com.duofei.thread.ReentrantLockStu.lambda$interruptedLock$6(ReentrantLockStu.java:156)
    	at java.lang.Thread.run(Thread.java:748)
    一直持有锁,等待队列长度:0
    ...    
               
    可以看出,当响应中断后,不会再请求锁(该锁的等待队列长度为 1);线程的中断标志值也非常有意思,在中断线程后,该值为 true ,在捕获到中断异常后,该值又被重新置为 false 。
    调用

    lockInterruptibly

    方法,并不适合在 finally 块中 释放锁;因为当你捕获到异常后(实际上并未获得锁),仍然会调用

    unlock

    方法。所以在这里的

    unlock

    方法需要在 try catch 块内处理。
  3. 请求锁与尝试请求锁

    lock

    tryLock

    的区别在于,

    tryLock

    方法在调用时,发现锁被其它线程持有,该方法立即返回

    false

    值。
    有趣的是,调用 tryLock 会破坏公平策略,表现为如果锁可用,则立即获得锁,不管其它线程当前是否在等待锁;如果想为这个锁执行公平设置,那么使用 tryLock(0, TimeUnit.SECONDS),这几乎是等价的(它还可以检测中断)。
  4. 尝试请求锁与给定时间内尝试请求锁

    tryLock(long timeout, TimeUnit unit)

    不同于

    tryLock()

    1. 如果这个锁被设置为使用公平的排序策略,那么如果其他线程正在等待这个锁,则不会获得可用的锁;
      如果你想使用定时的 tryLock 并且破坏公平锁,可以把定时和非定时的形式结合在一起:
      if (lock.tryLock() ||
          lock.tryLock(timeout, unit)) {
        ...
      }
                 
    定时的

    tryLock

    会响应线程中断,这和可中断请求锁相同。

3. Condition 

newCondition

方法返回用于此锁实例的

Condition

实例。

返回的

Condition

实例当与内置的监视器锁一起使用,支持与

Object

监视器方法(wait、notify和notifyAll) 相同的用法。

  • 如果在调用任何

    Condition

    await

    (等待)或

    signal

    (发出信号) 的方法时未持有此锁,则抛出IllegalMonitorStateException。这和使用对象的

    wait

    notify

    一样。
  • 当调用条件等待方法时,锁被释放,在它们返回之前,锁被重新获得,锁持有计数恢复到调用方法时的值。
  • 如果线程在等待期间被中断,那么等待将终止,抛出InterruptedException,并清除线程的中断状态。
  • 等待线程按FIFO顺序发出信号。
  • 对于从等待方法返回的线程,重新获取锁的顺序与初始获取锁的线程相同(在默认情况下没有指定),但是对于公平锁,优先使用那些等待时间最长的线程。

Condition

文档中有个有趣的列子:

假设我们有一个支持put和take方法的有界缓冲区。如果在空缓冲区上尝试获取,则线程将阻塞,直到某项可用为止;如果在一个满缓冲区上尝试put,那么线程将阻塞,直到空间可用为止。我们希望将put线程和take线程放在单独的等待集中,这样我们就可以优化每次当缓冲区中的项或空间可用时只通知一个线程。这可以通过使用两个条件实例来实现。

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }
           

通过这个列子,对于

Condition

的用法应该有所了解了。更详细的,例如中断等需要特别注意了。当然,在多线程编程时,这是必须的。