浅谈Java并发中ReentrantLock锁应该怎么用
作者:Shockang
重入锁可以替代关键字 synchronized 。
在 JDK5.0 的早期版本中,重入锁的性能远远优于关键字 synchronized ,
但从 JDK6.0 开始, JDK 在关键字 synchronized 上做了大量的优化,使得两者的性能差距并不大。
重入锁使用 ReentrantLock 实现
1、重入锁
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo implements Runnable { public static ReentrantLock lock = new ReentrantLock(); public static int i = 0; @Override public void run() { for (int j = 0; j < 10000000; j++) { lock.lock(); lock.lock(); try { i++; } finally { lock.unlock(); lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { ReentrantLockDemo tl = new ReentrantLockDemo(); Thread t1 = new Thread(tl); Thread t2 = new Thread(tl); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
控制台打印
20000000
说明
一个线程连续两次获得同一把锁是允许的。
如果不允许这么操作,那么同一个线程在第 2 次获得锁时,将会和自己产生死锁。
程序就会“卡死”在第 2 次申请锁的过程中。
但需要注意的是,如果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。
如果释放锁的次数多了,那么会得到一个 java.lang.IllegalMonitorStateException 异常,反之,如果释放锁的次数少了,那么相当于线程还持有这个锁,因此,其他线程也无法进入临界区。
2、中断响应
对于关键字 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。
而使用重入锁,则提供另外一种可能,那就是线程可以被中断。
也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。
有些时候,这么做是非常有必要的。
比如,你和朋友约好一起去打球,如果你等了半个小时朋友还没有到,你突然接到一个电话,说由于突发情况,朋友不能如约前来了,那么你一定扫兴地打道回府了。
中断正是提供了一套类似的机制。
如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须等待,可以停止工作了。
这种情况对于处理死锁是有一定帮助的。
下面的代码产生了一个死锁,但得益于锁中断,我们可以很轻易地解决这个死锁。
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.locks.ReentrantLock; public class IntLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; /** * 控制加锁顺序,方便构造死锁 * * @param lock */ public IntLock(int lock) { this.lock = lock; } @Override public void run() { try { if (lock == 1) { lock1.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { } lock2.lockInterruptibly(); } else { lock2.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { } lock1.lockInterruptibly(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) lock1.unlock(); if (lock2.isHeldByCurrentThread()) lock2.unlock(); System.out.println(Thread.currentThread().getId() + ":线程退出"); } } public static void main(String[] args) throws InterruptedException { IntLock r1 = new IntLock(1); IntLock r2 = new IntLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); Thread.sleep(1000); //中断其中一个线程 t2.interrupt(); } }
控制台输出
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.shockang.study.java.concurrent.lock.IntLock.run(IntLock.java:35)
at java.lang.Thread.run(Thread.java:748)
11:线程退出
12:线程退出
说明
线程 t1 和 t2 启动后, t1 先占用 lock1 ,再占用 lock2。
t2 先占用 lock2 ,再请求 lock1。
因此,很容易形成 t1 和 t2 之间的相互等待。
在这里,对锁的请求,统一使用 lockInterruptibly() 方法。
这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。
在代码第 56 行,主线程 main 处于休眠状态,此时,这两个线程处于死锁的状态。
在代码第 58 行,由于 t2 线程被中断,故 t2 会放弃对 lock1 的申请,同时释放已获得的 lock2 。
这个操作导致 t1 线程可以顺利得到 lock2 而继续执行下去。
3、锁申请等待限时
除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。
依然以约朋友打球为例,如果朋友退退不来,又无法联系到他,那么在等待 1 到 2 个小时后,我想大部分人都会扫兴离去。
对线程来说也是这样。
通常,我们无法判断为什么一个线程退迟拿不到锁。
也许是因为死锁了,也许是因为产生了饥饿。
如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的。
我们可以使用 tryLock() 方法进行一次限时的等待。
tryLock(long, TimeUnit)
下面这段代码展示了限时等待锁的使用。
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class TimeLock implements Runnable { public static ReentrantLock lock = new ReentrantLock(); @Override public void run() { try { if (lock.tryLock(5, TimeUnit.SECONDS)) { Thread.sleep(6000); } else { System.out.println("get lock failed"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) { TimeLock tl = new TimeLock(); Thread t1 = new Thread(tl); Thread t2 = new Thread(tl); t1.start(); t2.start(); } }
控制台打印
get lock failed
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
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.shockang.study.java.concurrent.lock.TimeLock.run(TimeLock.java:20)
at java.lang.Thread.run(Thread.java:748)
说明
在这里, tryLock() 方法接收两个参数,一个表示等待时长,另外一个表示计时单位。
这里的单位设置为秒,时长为 5 ,表示线程在这个锁请求中最多等待 5 秒。
如果超过 5 秒还没有得到锁,就会返回 false 。
如果成功获得锁,则返回 true 。
在本例中,由于占用锁的线程会持有锁长达 6 秒,故另一个线程无法在 5 秒的等待时间内获得锁,因此请求锁会失败。
tryLock()
ReentrantLock.tryLock() 方法也可以不带参数直接运行。
在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回 true 。
如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回 false 。
这种模式不会引起线程等待,因此也不会产生死锁。
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.locks.ReentrantLock; public class TryLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; public TryLock(int lock) { this.lock = lock; } @Override public void run() { if (lock == 1) { while (true) { if (lock1.tryLock()) { try { try { Thread.sleep(500); } catch (InterruptedException e) { } if (lock2.tryLock()) { try { System.out.println(Thread.currentThread() .getId() + ":My Job done"); return; } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } } else { while (true) { if (lock2.tryLock()) { try { try { Thread.sleep(500); } catch (InterruptedException e) { } if (lock1.tryLock()) { try { System.out.println(Thread.currentThread() .getId() + ":My Job done"); return; } finally { lock1.unlock(); } } } finally { lock2.unlock(); } } } } } public static void main(String[] args) throws InterruptedException { TryLock r1 = new TryLock(1); TryLock r2 = new TryLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); } }
控制台输出
11:My Job done
12:My Job done
说明
上述代码采用了非常容易死锁的加锁顺序。
也就是先让 t1 获得 lock1 ,再让 2 获得 lock2 ,接着做反向请求,让 t1 申请 lock2 , t2 申请 lock1 。
在一般情况下,这会导致 t1 和 2 相互等待。
待,从而引起死锁。
但是使用 tryLock() 方法后,这种情况就大大改善了。
由于线程不会傻傻地等待,而是不停地尝试,因此,只要执行足够长的时间,线程总是会得到所有需要的资源,从而正常执行(这里以线程同时获得 lock1 和 lock2 两把锁,作为其可以正常执行的条件)。
在同时获得 lock1 和 lock2 后,线程就打印出标志着任务完成的信息“ My Job done”。
4、公平锁
在大多数情况下,锁的申请都是非公平的。
也就是说,线程 1 首先请求了锁 A ,接着线程 2 也请求了锁 A 。
那么当锁 A 可用时,是线程 1 可以获得锁还是线程 2 可以获得锁呢?
这是不一定的,系统只是会从这个锁的等待队列中随机挑选一个。
因此不能保证其公平性。
这就好比买票不排队,大家都围在售票窗口前,售票员忙得焦头烂额,也顾不及谁先谁后,随便找个人出票就完事了。
而公平的锁,则不是这样,它会按照时间的先后顺序,保证先到者先得,后到者后得。
公平锁的一大特点是:它不会产生饥饿现象。
关于线程饥饿请参考我的博客——死锁、活锁和饥饿是什么意思?
只要你排队,最终还是可以等到资源的。
如果我们使用 synchronized 关键字进行锁控制,那么产生的锁就是非公平的。
而重入锁允许我们对其公平性进行设置。
它的构造函数如下:
/** * 使用给定的公平策略创建一个 ReentrantLock 的实例。 * * @param fair 如果此锁应使用公平排序策略为 true */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
当参数 fair 为 true 时,表示锁是公平的。
公平锁看起来很优美,但是要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能却非常低下,因此,在默认情况下,锁是非公平的。
如果没有特别的需求,则不需要使用公平锁。
公平锁和非公平锁在线程调度表现上也是非常不一样的。
下面的代码可以很好地突出公平锁的特点。
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.locks.ReentrantLock; public class FairLock implements Runnable { public static ReentrantLock fairLock = new ReentrantLock(true); @Override public void run() { while (true) { try { fairLock.lock(); System.out.println(Thread.currentThread().getName() + " 获得锁"); } finally { fairLock.unlock(); } } } public static void main(String[] args) throws InterruptedException { FairLock r1 = new FairLock(); Thread t1 = new Thread(r1, "Thread_t1"); Thread t2 = new Thread(r1, "Thread_t2"); t1.start(); t2.start(); } }
控制台输出
获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t1 获得锁
Thread_t1 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t2 获得锁
Thread_t1 获得锁
Thread_t1 获得锁
# 省略
说明
由于代码会产生大量输出,这里只截取部分进行说明。
在这个输出中,很明显可以看到,两个线程基本上是交替获得锁的,几乎不会发生同一个线程连续多次获得锁的可能,从而保证了公平性。
如果设置了 false,则会根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。
源码(JDK8)
/**
* 一种可重入互斥锁,其基本行为和语义与使用同步方法和语句访问的隐式监视锁(即 synchronized)相同,但具有扩展功能。
*
* 可重入锁属于上次成功锁定但尚未解锁它的线程。
*
* 当锁不属于另一个线程时,调用锁的线程将返回,并成功获取锁。
*
* 如果当前线程已经拥有锁,则该方法将立即返回。这可以使用 isHeldByCurrentThread 和 getHoldCount 方法进行检查。
*
* 此类的构造函数接受可选的公平性参数。
*
* 当设置为 true 时,在竞争状态下,锁有利于向等待时间最长的线程授予访问权限。否则,此锁不保证任何特定的访问顺序。
*
* 使用由多线程访问的公平锁的程序可能显示较低的总吞吐量
*
* (即,较慢;通常比使用默认设置的要慢得多,但是在获得锁和保证不饥饿的时间上有较小的差异。
*
* 但是请注意,锁的公平性并不能保证线程调度的公平性。
*
* 因此,使用公平锁的多个线程中的一个线程可以连续多次获得公平锁,而其他活动线程则没有进行并且当前没有持有该锁。
*
* 还要注意,untimed tryLock() 方法不支持公平性设置。
*
* 如果锁可用,即使其他线程正在等待,它也会成功。
*
* 建议的做法是总是在调用之后立即使用try块锁定,最典型的是在构建之前/之后,例如:
*
* class X {
* private final ReentrantLock lock = new ReentrantLock();
* // ...
*
* public void m() {
* lock.lock(); // block until condition holds
* try {
* // ... method body
* } finally {
* lock.unlock()
* }
* }
* }}
*
* 除了实现锁接口之外,这个类还定义了许多公共和受保护的方法来检查锁的状态。
*
* 其中一些方法只对 instrumentation 和 monitoring 有用。
*
* 此类的序列化与内置锁的行为相同:反序列化的锁处于未锁定状态,而与序列化时的状态无关。
*
* 此锁最多支持同一线程的2147483647个递归锁。尝试超过此限制会导致锁定方法抛出错误。
*
* @since 1.5
* @author Doug Lea
*/
public class ReentrantLock implements Lock, java.io.Serializable
到此这篇关于浅谈Java并发中ReentrantLock锁应该怎么用的文章就介绍到这了,更多相关ReentrantLock锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!