重入锁ReentrantLock

时间 2019/6/20 21:45:03 加载中...

背景

在Java中,我们常用 synchronized 关键字来实现线程同步。虽然它提供了一定的线程同步功能,但是也有一些问题。
synchronized 的同步块不提供 线程等待队列 的任何机制,并且在任何一个线程退出后,任何线程都有可以获得锁,这就有可能会导致一些线程长时间获取不到锁,无法获取资源。
另外,下面的情况也实现不了:

无法中断等待获取锁的线程
不能尝试获取锁
无法实现非块结构锁定规则,因为必须在获取它们的同一块中释放内部锁。

ReentrantLock

Reentrantlock是在Java 5.0中加入的,用来增强内部锁的功能。

ReentrantLock类实现了Lock接口,并提供了在访问共享资源时的同步方法。
ReentrantLock类通过 lock 和 unlock方法来访问共享资源。
当调用 lock 时,当前线程就会获取到锁,其它访问此共享资源的线程就会阻塞。
当调用 unlock 时,就会释放资源。

顾名思义,ReentrantLock允许线程不止一次地锁定资源。 当线程首次进入锁定状态时,计数器设置为1。
在解锁之前,线程可以再次重新进入锁,并且每次计数器增加1。 对于每个解锁请求,计数器减1,当计数器为0时,资源解锁。

ReentrantLock可以指定是否是公平锁,当是公平锁时,在线程解锁资源之后,锁将转到已经等待最长时间的线程。
但公平锁会降低程序吞吐量。

ReentrantLock的方法

lock():
调用lock方法,当前线程会获得锁,并且计数器加1。

unlock():
调用unlock方法,计数器减1,当计数器为0时,资源释放。

tryLock():
如果资源未被锁定,调用tryLock方法会返回true,并且计数器加1。如果资源已被锁定,那么方法会返回false,但线程还会被阻止,但会退出。

tryLock(long timeout, TimeUnit unit):
试图获取锁,并会在退出前等待一段时间。

lockInterruptibly():
如果资源是空闲的,则此方法会获取到锁,同时允许线程在获取资源时被某个其他线程打断。 这意味着如果当前线程正在等待锁但其他一些线程请求锁,则当前线程将会被中断并立即返回而不获取锁定

getHoldCount():
返回资源上锁的数量

isHeldByCurrentThread():
如果资源是当前线程拥有,则返回true

ReentrantLock的使用

下面时 ReentrantLock 的一般使用方法。

  1. class X {
  2. private final ReentrantLock lock = new ReentrantLock();
  3. // ...
  4. public void m() {
  5. lock.lock(); // block until condition holds
  6. try {
  7. // ... method body
  8. } finally {
  9. lock.unlock();
  10. }
  11. }
  12. }

重要: unlock 操作一定要放在 finally 里执行,保证不会死锁。

举例子

下面我们写一个有并发问题的例子。

有一个 Account 类,只有一个 num 字段。

  1. public class Account {
  2. private int num = 0;
  3. public int getNum() {
  4. return num;
  5. }
  6. public void setNum(int num) {
  7. this.num = num;
  8. }
  9. }

有一个 AccountService 类。有一个add方法,是对account中的num进行了加1操作。

  1. public class AccountService {
  2. public void add(Account account) {
  3. //当前线程停顿一下,保证有多个线程可以同时运行下面的加1操作
  4. try {
  5. Thread.sleep(1000);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. account.setNum(account.getNum() + 1);
  10. }
  11. }

还有一个 Program 类,创建了多个线程来执行上面的add方法。

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class Program {
  4. public static void main(String[] args) {
  5. int initCount = Thread.activeCount();
  6. System.out.println("当前活动的线程数:" + initCount);
  7. AccountService accountService = new AccountService();
  8. Account account = new Account();
  9. ExecutorService executorService = Executors.newCachedThreadPool();
  10. for (int i = 0; i < 10; i++) {
  11. executorService.execute(()->{
  12. accountService.add(account);
  13. });
  14. }
  15. executorService.shutdown();
  16. //保证上面的线程全部执行完毕
  17. while(Thread.activeCount() > initCount){
  18. Thread.yield();
  19. }
  20. System.out.println("当前活动的线程数:" + Thread.activeCount());
  21. System.out.println(account.getNum());
  22. }
  23. }

上面的三个类写完后,运行。

预期情况下,最后的结果应该是10,但由于并发问题的存在,结果可能是9或者8之类的,总之,结果不确定。

如果用 ReentrantLock 怎么改呢?

错误写法

ReentrantLock写出了局部对象(不对的)

将AccountService改成如下代码

  1. public class AccountService {
  2. public void add(Account account) {
  3. //sleep一下,保证多个线程同时运行下面的方法
  4. try {
  5. Thread.sleep(1000);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. gl>ReentrantLock lock = new ReentrantLock();
  10. gl>lock.lock();
  11. gl>try {
  12. account.setNum(account.getNum() + 1);
  13. gl>} catch (Exception e) {
  14. gl>e.printStackTrace();
  15. gl>} finally {
  16. gl>lock.unlock();
  17. gl>}
  18. }
  19. }

改好之后,多运行几次,发现有时候结果还是9或者8。所以这种写法是错误的。

正确写法

正确写法怎么写呢,我们将 ReentrantLock 提升为 AccountService 的成员变量

  1. import java.util.concurrent.locks.ReentrantLock;
  2. public class AccountService {
  3. ReentrantLock lock = new ReentrantLock();
  4. public void add(Account account) {
  5. //sleep一下,保证多个线程同时运行下面的方法
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. lock.lock();
  12. try {
  13. account.setNum(account.getNum() + 1);
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. } finally {
  17. lock.unlock();
  18. }
  19. }
  20. }

改好之后,多运行几次,结果总是10,这次保证了正确性。

再变动

我们再次改动一下代码,在 Program 中,有这样一段代码

  1. AccountService accountService = new AccountService();
  2. Account account = new Account();
  3. ExecutorService executorService = Executors.newCachedThreadPool();
  4. for (int i = 0; i < 10; i++) {
  5. executorService.execute(()->{
  6. accountService.add(account);
  7. });
  8. }

也就是说,每个线程操作的都是同一个 AccountService 的add方法。
如果我们把 AccountService 的创建放在每个线程中会怎么样呢?

我们修改成如下的样子:

  1. public class Program {
  2. public static void main(String[] args) {
  3. int initCount = Thread.activeCount();
  4. System.out.println("当前活动的线程数:" + initCount);
  5. Account account = new Account();
  6. ExecutorService executorService = Executors.newCachedThreadPool();
  7. for (int i = 0; i < 10; i++) {
  8. executorService.execute(() -> {
  9. AccountService accountService = new AccountService();
  10. accountService.add(account);
  11. });
  12. }
  13. executorService.shutdown();
  14. //保证上面的线程全部执行完毕
  15. while(Thread.activeCount() > initCount){
  16. Thread.yield();
  17. }
  18. System.out.println("当前活动的线程数:" + Thread.activeCount());
  19. System.out.println(account.getNum());
  20. }
  21. }

修改完毕,我们再次运行,运行几次后,发现结果又出现了9或8之类的结果,有不对了。

那么问题出现了

ReentrantLock的lock到底是锁住了谁?

ReentrantLock的lock到底是锁住了谁,或者说这个锁到底是谁给的。

保护资源的本质其实是:当前线程获取到锁,然后执行某段代码,保证了在此期间其他线程不会执行相同的代码,从而保护了共享资源。

所以从哪里拿这个锁也很重要

ReentrantLock的锁是自己给的,或者说锁定的是自己。

回顾一下上面的改动

第一次我们在 add 方法内新建的 ReentrantLock,那每个线程过来都新建一个 ReentrantLock,获取的是自己的锁,
而其他线程过来之后,也会新建一个 ReentrantLock,每一个lock都和其他线程没有关系,所以不能保证只有一个线程执行此段代码。

第二次我们将 ReentrantLock 升级为成员变量后,ReentrantLock 属于了 AccountService,又由于 AccountService 只有一个,
每次线程都是操作的同一个 AccountService,所以每个线程都是从 这一个 AccountService 的 ReentrantLock 来获取锁。
每个线程拿锁的地方都一样,所以保证了只有一个线程执行此段代码。

第三次修改我们是每个线程都创建一个 AccountService,所以相当于每个线程都是从
自己的 AccountService 里面的 ReentrantLock 拿锁,都不是同一个把锁,所以,最后这个也是有问题的。

ReentrantLock与synchronized

synchronized 使用的是内部锁或监视器来实现同步的。

Java中的每个对象都有一个与之关联的内部锁。 每当线程尝试访问同步块或方法时,它就会获取该对象上的内部锁或监视器。在静态方法的情况下,线程获取的是对类对象的锁定。
因此,在编码方面,内部锁机制是一个不错的选择,适合大部分情况。

因此如果不是需要 ReentrantLock 的特有的优势的话,没有必要使用 ReentrantLock 。

扫码分享
版权说明
作者:SQBER
文章来源:http://www.sqber.com/articles/reentrant-lock-in-java.html
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。