Java多线程:可重入锁RentrantLock Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock

  Java中通过synchronized关键字来进行同步,实现对竞争资源的互斥访问的锁。Java 1.0版本中就已经支持同步锁了。

  同步锁的原理是,对于每一个对象,有且仅有一个同步锁;不同的线程能共同访问该同步锁。但是,在同一个时间点,该同步锁能且只能被一个线程获取到。这样,获取到同步锁的线程就能进行CPU调度,从而在CPU上执行;而没有获取到同步锁的线程,必须进行等待,直到获取到同步锁之后才能继续运行。这就是,多线程通过同步锁进行同步的原理!

  Java中除了同步锁还在concurrent包中提供了一些功能更强大、用法更灵活的锁,这些锁都继承自Lock接口。

Lock接口

  Lock是一个接口提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显式的。包路径是:java.util.concurrent.locks.Lock。核心方法是lock(),unlock(),tryLock(),实现类有ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock。Lock 接口支持那些语义不同(重入、公平等)的锁规则。所谓语义不同,是指锁可是有"公平机制的锁"、"非公平机制的锁"、"可重入的锁"等等。"公平机制"是指"不同线程获取锁的机制是公平的",而"非公平机制"则是指"不同线程获取锁的机制是非公平的","可重入的锁"是指同一个锁能够被一个线程多次获取。

在Lock中定义了如下的几个方法

 1 public interface Lock
 2 {
 3     /**
 4      * 获取锁。
 5      * 如果该锁没有被其他线程持有,则获得该锁并返回,将锁的保持计数设置为1
 6      * 如果当前线程已经持有该锁,将保持计数加1,并立即返回
 7      * 如果锁不可用,出于线程调度目的,将禁用当前线程,
 8      * 并且在获得锁之前,该线程将一直处于休眠状态,此时锁的保持计数设置为1
 9      */
10     public abstract void lock();
11 
12     /**
13      * 如果当前线程未被中断,则获取锁。如果锁可用,则获取锁,并立即返回
14      * 如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下两种情况之一以前,
15      * 该线程将一直处于休眠状态:
16      * 锁由当前线程获得;或者其他某个线程中断当前线程,并且支持对锁获取的中断
17      * 如果当前线程:在进入此方法时已经设置了该线程的中断状态;
18      * 或者在获取锁时被中断,并且支持对锁获取的中断,
19      * 则将抛出InterruptedException,并清除当前线程的已中断状态
20      */
21     public abstract void lockInterruptibly() throws InterruptedException;
22 
23     /**
24      * 仅在调用时锁为空闲状态才获取该锁
25      * 如果锁可用,则获取锁,并立即返回值true,如果锁不可用,则此方法将立即返回值false 
26      * 通常对于那些不是必须获取锁的操作可能有用
27      */
28     public abstract boolean tryLock();
29 
30     /**
31      * 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
32      * 如果锁可用,则此方法将立即返回值 true 。如果锁不可用,出于线程调度目的,
33      * 将禁用当前线程。并且在发生以下三种情况之一前,该线程将一直处于休眠状态:
34      * 1.当前线程获得了锁 2.其他某个线程中断当前线程,并且支持对锁获取的中断 3.等待时间到了
35      *
36      * @param paramLong 等待时间
37      * @param paramTimeUnit 时间单位
38      */
39     public abstract boolean tryLock(long paramLong , TimeUnit paramTimeUnit) throws InterruptedException;
40 
41     /**
42      * 释放锁。对应于lock()、tryLock()、tryLock(xx)、lockInterruptibly()等操作,
43      * 如果成功的话应该对应着一个unlock(),这样可以避免死锁或者资源浪费
44      */
45     public abstract void unlock();
46 
47     /**
48      * 返回用来与此 Lock 实例一起使用的 Condition 实例
49      */
50     public abstract Condition newCondition();

Lock与synchronized 的比较:

1:Lock使用起来比较灵活,但是必须有释放锁的动作;

2:Lock必须手动释放和开启锁,synchronized 不需要;

3:Lock只适用于代码块锁,而 synchronized 锁的是对象;

ReentrantLock

  ReentrantLock 是 Lock 的实现类,通常称之为可重入的互斥锁,又被称为“独占锁”。顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。

  ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

  在竞争条件下,ReentrantLock 的实现要比现在的 synchronized 实现更具有可伸缩性。(有可能在 JVM 的将来版本中改进 synchronized 的竞争性能)这意味着当许多线程都竞争相同锁定时,使用 ReentrantLock 的吞吐量通常要比 synchronized 好。换句话说,当许多线程试图访问 ReentrantLock 保护的共享资源时,JVM 将花费较少的时间来调度线程,而用更多个时间执行线程。虽然 ReentrantLock 类有许多优点,但是与同步相比,它有一个主要缺点 — 它可能忘记释放锁定。ReentrantLock实在工作中对方法块加锁使用频率最高的。

ReentrantLock类中包含的方法有

// 创建一个 ReentrantLock ,默认是"非公平锁"。
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
ReentrantLock(boolean fair)

// 查询当前线程持有此锁的次数。
int getHoldCount()
// 返回目前持有此锁的线程,如果此锁不被任何线程持有,则返回 null。
protected Thread getOwner()
// 返回一个线程集合,它包含可能正等待获取此锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正等待获取此锁的线程估计数。
int getQueueLength()
// 返回一个线程集合,它包含可能正在等待与此锁相关给定条件匹配的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待与此锁相关的且匹配给定条件的线程估计数。
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果是"公平锁"返回true,否则返回false。
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持。
boolean isLocked()

通常它的使用方法为

 1 class X {
 2 
 3     private static final ReentrantLock lock = new ReentrantLock();
 4 
 5     // …
 6 
 7     main() {
 8 
 9         lock.lock(); // 获得锁
10 
11         try {
12 
13             // … 方法体
14 
15         } finally {
16 
17             lock.unlock();//解锁
18 
19         }
20     }
21 }

我们通过几个例子来介绍重入锁与同步锁有什么区别

1. 没有锁的情况

 1 import java.util.concurrent.locks.ReentrantLock;
 2 
 3 public class Demo
 4 {
 5     public static void main(String[] agrs)
 6     {
 7         final Count ct = new Count();
 8         for(int i = 0; i < 2; i++)
 9         {
10             new Thread()
11             {
12                 @Override
13                 public void run()
14                 {
15                     ct.get();
16                 }
17 
18             }.start();
19         }
20 
21         for(int i = 0; i < 2; i++)
22         {
23             new Thread()
24             {
25                 @Override
26                 public void run()
27                 {    
28                     ct.put();
29                 }
30             }.start();
31         }
32     }
33 }
34 
35 class Count
36 {
37     public void get()
38     {
39         try
40         {
41             System.out.println(Thread.currentThread().getName() + " get begin");
42             Thread.sleep(1000);
43             System.out.println(Thread.currentThread().getName() + " get end");
44         }
45         catch(InterruptedException e)
46         {
47             e.printStackTrace();
48         }
49     }
50 
51     public void put()
52     {
53         try
54         {
55             System.out.println(Thread.currentThread().getName() + " put begin");
56             Thread.sleep(1000);
57             System.out.println(Thread.currentThread().getName() + " put end");
58         }
59         catch(InterruptedException e)
60         {
61             e.printStackTrace();
62         }
63     }
64 }

结果如下:每次运行结果都不同

Thread-1 get begin
Thread-2 put begin
Thread-0 get begin
Thread-3 put begin
Thread-2 put end
Thread-0 get end
Thread-1 get end
Thread-3 put end

说明:如果不加锁,每个线程都可以任意访问,前提只要能抢占到 cpu 资源,所以每次打印结果都不同。

2. 多把锁,不同方法不同锁

 1 class Count
 2 {
 3     public void get()
 4     {
 5         final ReentrantLock lock = new ReentrantLock();
 6         try
 7         {
 8             lock.lock();
 9             System.out.println(Thread.currentThread().getName() + " get begin");
10             Thread.sleep(1000);
11             System.out.println(Thread.currentThread().getName() + " get end");
12         }
13         catch(InterruptedException e)
14         {
15             e.printStackTrace();
16         }
17         finally
18         {
19             lock.unlock();
20         }
21     }
22 
23     public void put()
24     {
25         final ReentrantLock lock = new ReentrantLock();
26         try
27         {
28             lock.lock();
29             System.out.println(Thread.currentThread().getName() + " put begin");
30             Thread.sleep(1000);
31             System.out.println(Thread.currentThread().getName() + " put end");
32         }
33         catch(InterruptedException e)
34         {
35             e.printStackTrace();
36         }
37         finally
38         {
39             lock.unlock();
40         }
41     }
42 }

结果为:每次打印都不同

Thread-2 put begin
Thread-3 put begin
Thread-0 get begin
Thread-1 get begin
Thread-0 get end
Thread-2 put end
Thread-1 get end
Thread-3 put end

说明:每个线程访问get或put方法时,都会新创建一个 ReentrantLock 实例,通过这个实例获得到的锁与其它的锁不同。相当于每个线程的锁都与其它的不同,所以他们之间不会存在任何影响或关联。只要谁获得了cpu资源谁就执行方法。

3. 只有一把全局锁

 1 class Count
 2 {
 3     final ReentrantLock lock = new ReentrantLock();
 4     public void get()
 5     {
 6         try
 7         {
 8             lock.lock();
 9             System.out.println(Thread.currentThread().getName() + " get begin");
10             Thread.sleep(1000);
11             System.out.println(Thread.currentThread().getName() + " get end");
12         }
13         catch(InterruptedException e)
14         {
15             e.printStackTrace();
16         }
17         finally
18         {
19             lock.unlock();
20         }
21     }
22 
23     public void put()
24     {
25         try
26         {
27             lock.lock();
28             System.out.println(Thread.currentThread().getName() + " put begin");
29             Thread.sleep(1000);
30             System.out.println(Thread.currentThread().getName() + " put end");
31         }
32         catch(InterruptedException e)
33         {
34             e.printStackTrace();
35         }
36         finally
37         {
38             lock.unlock();
39         }
40     }
41 }

结果如下:并且结果不变

Thread-0 get begin
Thread-0 get end
Thread-1 get begin
Thread-1 get end
Thread-2 put begin
Thread-2 put end
Thread-3 put begin
Thread-3 put end

说明:每个线程访问方法时,获得的锁时使用的 ReentrantLock 实例是同一个,所以当某条线程获得了锁的时候,其他线程就不能再得到锁了,必须等到当前获得锁的线程释放了当前锁的时候他才能重新获取锁进而执行相应的方法,所以每次打印的结果都是一样的。

ReentrantLock 扩展功能

1. 实现可轮询的请求

  在内部锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错。而可轮询的锁获取模式具有更完善的错误恢复机制,可以规避死锁的发生。

如果你不能获得所有需要的锁,那么使用可轮询的获取方式使你能够重新拿到控制权。可轮询的锁获取模式,由tryLock()方法实现。此方法仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。此方法的典型使用语句如下:

 1 Lock lock = new ReentrantLock();
 2 if(lock.tryLock())
 3 {
 4     try
 5     {
 6         //do something
 7     }
 8     finally
 9     {
10         lock.unlock();
11     }
12 }
13 else
14 {
15     // perform alternative actions
16 }

2. 实现可定时的锁求情

  当使用内部锁时,一旦开始请求,锁就不能停止了,所以内部锁给实现具有时限的活动带来了风险。为了解决这一问题,可以使用定时锁。当具有时限的活动调用了阻塞方法,定时锁能够在时间预算内设定相应的超时。如果活动在期待的时间内没能获得结果,定时锁能使程序提前返回。可定时的锁获取模式,由tryLock(long, TimeUnit)方法实现。

3. 实现可中断的锁获取请求

  可中断的锁获取操作允许在可取消的活动中使用。lockInterruptibly()方法能够使你获得锁的时候响应中断。

ReentrantLock 与 同步锁的区别

1. ReentrantLock功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性。

2. ReentrantLock 的性能比synchronized会好点。

3. ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。

使用 ReentrantLock 时需要注意的地方:

1. lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放。

2. 当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。

可重入锁指的是在一个线程中可以多次获取同一把锁,比如:

一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;synchronized也是重入锁

自旋锁重入锁不同,若一个线程两次调用lock()方法时,会导致第二次调用lock()位置进行自旋,产生死锁。而避免死锁正是重入锁的一个重要作用

参考资料:

Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock

《Java并发编程从入门到精通》显示锁Lock和ReentrantLock

Java多线程(九)之ReentrantLock与Condition