ReentrantLock可以完全替代synchronized关键字,在Java1.5之前,ReentrantLock的性能远远好与synchronized,在1.5之后,Java1.6开始,JDK在synchronized上做了大量的优化,使得两者的性能差距并不大,但是在使用的灵活性上,ReentrantLock更加的灵活可控。这篇文章将于synchronized关键字比较,若不了解请看我上篇文章 synchronized。
ReentrantLock的几个重要的方法:
❤ lock():获得锁,如果锁被占用,则等待。
❤ lockInterruptibly():获得锁,但优先响应中断。
❤ tryLock():尝试获得锁,如果成功,返回true;失败返回false。该方法不会等待,立即返回。
❤ tryLock(long time,TimeUnit unit):在给定时间内获得锁;如果在时间内成功获得锁,返回true,反之返回false。
❤ unlock():释放锁。
来一个简单案例,示例怎么使用ReentrantLock:
1 public class ReentrantLockDemo implements Runnable{
2 public static ReentrantLock lock = new ReentrantLock();
3 public static int i = 0;
4
5 @Override
6 public void run() {
7 for (int j = 0;j < 10000;j++){
8 lock.lock();//加锁
9 try {
10 i++;
11 }finally {
12 lock.unlock();//释放锁
13 }
14 }
15 }
16
17 //测试
18 public static void main(String[] args) throws InterruptedException {
19 ReentrantLockDemo demo = new ReentrantLockDemo();
20 Thread t1 = new Thread(demo);
21 Thread t2 = new Thread(demo);
22 t1.start();
23 t2.start();
24 t1.join();
25 t2.join();
26 System.out.println(i);
27 }
28 }
输出结果:
20000
从输出看出,ReentrantLock保证了多线程的安全性。
从上面代码可以看出,ReentrantLock相比于synchronized,ReentrantLock有着显示的操作过程,使用人员必须手动指定何时加锁,何时释放锁。正因为是这样,ReentrantLock对逻辑控制的灵活性要远远好于synchronized,但必须注意,退出临界区时,必须释放锁,否则,其他线程就没有机会访问到临界区了。
lockInterruptibly()
对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么就继续等待。而对于ReentrantLock来说,它可以使线程在等待的过程中使其中断,这种情况对死锁从处理是有一定的帮助的。
来看下面的例子:
1 public class ReentrantDeadLock implements Runnable {
2
3 //定义两个全局锁
4 public static ReentrantLock lock1 = new ReentrantLock();
5 public static ReentrantLock lock2 = new ReentrantLock();
6 //方便构造死锁
7 int lock;
8
9 private ReentrantDeadLock(int lock){
10 this.lock = lock;
11 }
12
13 @Override
14 public void run() {
15 try {
16 if (lock == 1){
17 try {
18 lock1.lockInterruptibly();
19 Thread.sleep(500);
20 } catch (InterruptedException e) {
21 e.printStackTrace();
22 }
23 lock2.lockInterruptibly();
24 System.out.println("lock == 1 完成任务");
25 }else{
26 try {
27 lock2.lockInterruptibly();
28 Thread.sleep(500);
29 } catch (InterruptedException e) {
30 e.printStackTrace();
31 }
32 lock1.lockInterruptibly();
33 System.out.println("lock == 2 完成任务");
34 }
35 }catch (InterruptedException e){
36 e.printStackTrace();
37 }finally {
38 if (lock1.isHeldByCurrentThread()){
39 System.out.println(Thread.currentThread().getName() + "释放 lock1");
40 lock1.unlock();
41 }
42 if (lock2.isHeldByCurrentThread()){
43 System.out.println(Thread.currentThread().getName() + "释放 lock2");
44 lock2.unlock();
45 }
46
47 System.out.println(Thread.currentThread().getName() + ":线程退出");
48 }
49 }
50
51 public static void main(String[] args) throws InterruptedException {
52 ReentrantDeadLock deadLock = new ReentrantDeadLock(1);
53 ReentrantDeadLock deadLock1 = new ReentrantDeadLock(2);
54
55 Thread t1 = new Thread(deadLock,"t1");
56 Thread t2 = new Thread(deadLock1,"t2");
57 t1.start();
58 t2.start();
59 Thread.sleep(1000);
60 System.out.println("中断前");
61 t2.interrupt();//中断t2线程
62 }
63 }
输出结果:
中断前
t2释放 lock2
t2:线程退出
lock == 1 完成任务
t1释放 lock1
t1释放 lock2
t1:线程退出
上面代码执行时,t1线程先占用lock1,再请求占用lock2;t2线程先占用lock2,再请求占用lock1。因此如果不加 t2.interrupt();这段代码,这段程序将会造成死锁。在上述代码中,采用了lockInterruptibly()方法请求锁资源,这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。
从输出结果来看,在中断前,两个线程处于死锁状态,在 t2.interrupt();后,t2线程被中断,故放弃了对lock1的申请,同时释放了lock2,这样t1就可以获取到lock2,完成任务,完成后,释放所有锁,最后退出。
tryLock(long time,TimeUnit unit):
除了上述用lockInterruptibly()方法,通过外部通知解决死锁的方式外,还有一种方法,就是限时等待;通常来说,我们无法判断为什么一个线程迟迟拿不到锁,也许是因为死锁,也许是因为饥饿。但是如果给定一个等待时间,时间过后自动放弃,那么总的来说还是对系统有意义的。下面展示tryLock(long time,TimeUnit unit)的使用:
1 public class TimeLock implements Runnable {
2
3 public static ReentrantLock lock = new ReentrantLock();
4
5 @Override
6 public void run() {
7 try {
8 if (lock.tryLock(5, TimeUnit.SECONDS)){
9 System.out.println(Thread.currentThread().getName() + "获取锁成功!");
10 Thread.sleep(6000);
11 }else{
12 System.out.println(Thread.currentThread().getName() + "获取锁失败!");
13 }
14 } catch (InterruptedException e) {
15 e.printStackTrace();
16 }finally {
17 if (lock.isHeldByCurrentThread()){
18 lock.unlock();
19 }
20 }
21 }
22 //测试
23 public static void main(String[] args){
24 TimeLock timeLock = new TimeLock();
25 Thread t1 = new Thread(timeLock,"t1");
26 Thread t2 = new Thread(timeLock,"t2");
27
28 t1.start();
29 t2.start();
30 }
31 }
输出:
t2获取锁成功!
t1获取锁失败!
由输出结果看出,在t2获取锁成功后,需要等待6S,但是设置了线程在获取锁请求最多等待5S,所以t1就获取失败了。
tryLock():
使用tryLock()去获取锁资源,如果当前锁未被其他线程占用,则会申请成功,并立即返回true,如果锁被其他线程占用,申请锁的线程也不会等待,而是立即返回false,这种方式不会引起线程等待,因此不会产生死锁。
修改上面第一个例子来演示这个方法:
1 public class TryLock implements Runnable{
2
3 public static ReentrantLock lock1 = new ReentrantLock();
4 public static ReentrantLock lock2 = new ReentrantLock();
5 int lock;
6
7 public TryLock(int lock){
8 this.lock = lock;
9 }
10 @Override
11 public void run() {
12 if (lock == 1){
13 while (true){
14 if (lock1.tryLock()){
15 System.out.println(Thread.currentThread().getName() + ": 获取到了lock1");
16 try {
17 try {
18 Thread.sleep(300);
19 } catch (InterruptedException e) {
20 e.printStackTrace();
21 }
22 //获取lock2资源
23 if (lock2.tryLock()){
24 try {
25 System.out.println(Thread.currentThread().getName() + ": Done!");
26 System.out.println("lock == 1 完成!");
27 return;
28 }finally {
29 lock2.unlock();
30 }
31 }
32 }finally {
33 lock1.unlock();
34 }
35 }
36 }
37 }else{
38 while (true){
39 if (lock2.tryLock()){
40 System.out.println(Thread.currentThread().getName() + ": 获取到了lock2");
41 try {
42 try {
43 Thread.sleep(100);
44 } catch (InterruptedException e) {
45 e.printStackTrace();
46 }
47 //获取lock1资源
48 if (lock1.tryLock()){
49 try {
50 System.out.println(Thread.currentThread().getName() + ": Done!");
51 System.out.println("lock == 2 完成!");
52 return;
53 }finally {
54 lock1.unlock();
55 }
56 }
57 }finally {
58 lock2.unlock();
59 }
60 }
61 }
62 }
63 }
64 //测试
65 public static void main(String[] args){
66 TryLock tryLock = new TryLock(1);
67 TryLock tryLock1 = new TryLock(2);
68
69 Thread t1 = new Thread(tryLock,"t1");
70 Thread t2 = new Thread(tryLock1,"t2");
71 t1.start();
72 t2.start();
73 }
74 }
输出结果:
1 t1: 获取到了lock1
2 t2: 获取到了lock2
3 t2: 获取到了lock2
4 t2: 获取到了lock2
5 t1: 获取到了lock1
6 t2: 获取到了lock2
7 t2: 获取到了lock2
8 t2: 获取到了lock2
9 t1: 获取到了lock1
10 t2: 获取到了lock2
11 t2: 获取到了lock2
12 t2: 获取到了lock2
13 t1: 获取到了lock1
14 t2: 获取到了lock2
15 ..........
16 t2: 获取到了lock2
17 t1: 获取到了lock1
18 t2: Done!
19 lock == 2 完成!
20 t1: 获取到了lock1
21 t1: Done!
22 lock == 1 完成!
从结果可看出,t1,t2一直在不停的尝试获取锁,只要执行的时间足够长,线程总是会获取到需要的资源,完成相应的任务。
公平锁和非公平锁
在大多数的情况下,锁的申请都是非公平的。也就是说线程A首先申请了锁A,接着线程B也申请了锁A,那么当锁A可用时,是线程A获得锁还是线程B获得锁呢?这是不一定的。系统只是会从这个锁的等待队列中随机挑选一个,这样就不能保证获得锁的公平性,这就是非公平锁。而公平锁不是这样的,它会按照时间先后顺序,保证先到先得,后到后得。公平锁的一大特点就是:它不会产生饥饿。只要你排队,最终都会得到资源的。若使用synchronized关键字进行锁控制,那么产生的锁就是非公平锁。而ReentrantLock允许我们对其设置公平性。它有一个构造函数签名如下:
public ReentrantLock(boolean fair)
当参数fair为true时,表示锁就是公平锁。公平锁看起来很优美,但是要实现公平锁必然要求维护一个有序队列,因此公平锁的实现成本比较高,性能也相对非常低下,因此,ReentrantLock默认情况下,锁是非公平的。如果没有什么特别的要求,不要使用公平锁。
代码来展示一下,公平锁的特点:
1 public class FairLock implements Runnable{
2
3 public static ReentrantLock fairlock = new ReentrantLock(true);
4
5 @Override
6 public void run() {
7 while (true){
8 try {
9 fairlock.lock();
10 System.out.println(Thread.currentThread().getName() + "获得锁!");
11 }finally {
12 fairlock.unlock();
13 }
14 }
15 }
16 //测试
17 public static void main(String[] args){
18 FairLock fairLock = new FairLock();
19 Thread t1 = new Thread(fairLock,"t1");
20 Thread t2 = new Thread(fairLock,"t2");
21 t1.start();
22 t2.start();
23 }
24 }
输出:
t1获得锁!
t2获得锁!
t1获得锁!
t2获得锁!
t1获得锁!
t2获得锁!
t1获得锁!
t2获得锁!
t1获得锁!
t2获得锁!
........
从输出就可以看出,两个线程是交替执行的。
将公平锁修改为非公平锁,输出:
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t1获得锁!
t2获得锁!
t2获得锁!
t2获得锁!
t2获得锁!
t2获得锁!
......
可以看出,根据系统随机调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是没用公平性。
ReentrantLock是可重入锁
1 public class LockLock implements Runnable {
2
3 public static ReentrantLock lock = new ReentrantLock();
4
5 @Override
6 public void run() {
7 try {
8 lock.lock();
9 System.out.println("第一次!");
10 lock.lock();
11 System.out.println("第二次!");
12 }finally {
13 lock.unlock();
14 lock.unlock();
15 }
16 }
17 //测试
18 public static void main(String[] args){
19 LockLock lockLock = new LockLock();
20 Thread thread = new Thread(lockLock);
21 thread.start();
22 }
23 }
输出结果:
第一次!
第二次!
从结果,可以看出,一个线程连续获得两次锁,这种操作是允许的。所以ReentrantLock是可重入锁。
ReentrantLock的继承属性
1 public class Father {
2
3 private ReentrantLock lock = new ReentrantLock();
4
5 public void subOpt() throws InterruptedException {
6 try {
7 lock.lock();
8 System.out.println("Father 线程进入时间:" + System.currentTimeMillis());
9 Thread.sleep(5000);
10 System.out.println("Father!" + Thread.currentThread().getName());
11 }finally {
12 lock.unlock();
13 }
14 }
15 }
16
17 class SonOverRide extends Father{
18 @Override
19 public void subOpt() throws InterruptedException {
20 System.out.println("SonOverRide 线程进入时间:" + System.currentTimeMillis());
21 Thread.sleep(3000);
22 System.out.println("SonOverRide!" + Thread.currentThread().getName());
23 }
24 }
25
26 class Son extends Father{
27 public void subOpt() throws InterruptedException {
28 super.subOpt();
29 }
30 }
31
32 class Test{
33 public static void main(String[] args){
34 //测试重写父类中方法类
35 SonOverRide sonOverRide = new SonOverRide();
36 for (int i= 0;i < 5;i++){
37 new Thread(){
38 @Override
39 public void run() {
40 try {
41 sonOverRide.subOpt();
42 } catch (InterruptedException e) {
43 e.printStackTrace();
44 }
45 }
46 }.start();
47 }
48 //测试未重写父类方法的类
49 Son son = new Son();
50 for (int i = 0;i < 5;i++){
51 new Thread(){
52 @Override
53 public void run() {
54 try {
55 son.subOpt();
56 } catch (InterruptedException e) {
57 e.printStackTrace();
58 }
59 }
60 }.start();
61 }
62 }
63 }
输出结果:
SonOverRide 线程进入时间:1537518717790
SonOverRide 线程进入时间:1537518717790
SonOverRide 线程进入时间:1537518717790
SonOverRide 线程进入时间:1537518717790
Father 线程进入时间:1537518717791
SonOverRide 线程进入时间:1537518717791
SonOverRide!Thread-0
SonOverRide!Thread-4
SonOverRide!Thread-2
SonOverRide!Thread-1
SonOverRide!Thread-3
Father!Thread-5
Father 线程进入时间:1537518722791
Father!Thread-6
Father 线程进入时间:1537518727792
Father!Thread-9
Father 线程进入时间:1537518732792
Father!Thread-8
Father 线程进入时间:1537518737792
Father!Thread-7
观察线程进入时间,可以看出重写父类的方法并且没有加锁时,没有同步效果,线程进入时间几乎为同一时间;没有重写父类方法,有同步效果,上一个线程进入到下一个线程进入,间隔刚好5S。与synchronized关键字是一致的。