专栏首页做不甩锅的后端多线程基础(八):ReentrantLock的使用及与synchronized的区别

多线程基础(八):ReentrantLock的使用及与synchronized的区别

前面部分,我们着重讨论了synchronized的使用和wait、notify及notifyAll方法等并发的基础部分。今天,我们来学习另外一种解决方案。

1.交替打印数字和阻塞队列

还记得,前面一篇文章《什么?面试官让我用ArrayList实现一个阻塞队列?》中,描述了一个关于实现两个线程交替打印以及实现阻塞队列的例子,那么今天,我们来看看另外一种解决办法—ReentrantLock。

代码如下:

package com.dhb.reentrantlocktest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {


	private static final ReentrantLock lock = new ReentrantLock(true);

	private static int count = 0;

	public static final int MAX = 100;

	public static void main(String[] args) {

		new Thread(() -> {
			while (count <= MAX) {
				try {
					lock.lock();
					System.out.println(Thread.currentThread().getName()+" "+count++);
					TimeUnit.SECONDS.sleep(1);
				} catch (Exception e) {
					e.printStackTrace();
				}finally {
					lock.unlock();
				}
			}
		},"T1").start();


		new Thread(() -> {
			while (count <= MAX) {
				try {
					lock.lock();
					System.out.println(Thread.currentThread().getName()+" "+count++);
					TimeUnit.SECONDS.sleep(1);
				} catch (Exception e) {
					e.printStackTrace();
				}finally {
					lock.unlock();
				}
			}
		},"T2").start();
	}
}

上述代码执行之后如下:

T1 0
T2 1
T1 2
T2 3
T1 4
T2 5
T1 6
T2 7
T1 8
T2 9
T1 10
T2 11
T1 12
T2 13
T1 14
T2 15
T1 16
T2 17
T1 18
T2 19
T1 20
T2 21
T1 22
T2 23
T1 24
T2 25
T1 26
T2 27
T1 28
T2 29
T1 30
T2 31
T1 32
T2 33
T1 34
T2 35
T1 36
T2 37
T1 38
T2 39
T1 40
T2 41
T1 42
T2 43
T1 44
T2 45
T1 46
T2 47
T1 48
T2 49
T1 50
T2 51
T1 52
T2 53
T1 54
T2 55
T1 56
T2 57
T1 58
T2 59
T1 60
T2 61
T1 62
T2 63
T1 64
T2 65
T1 66
T2 67
T1 68
T2 69
T1 70
T2 71
T1 72
T2 73
T1 74
T2 75
T1 76
T2 77
T1 78
T2 79
T1 80
T2 81
T1 82
T2 83
T1 84
T2 85
T1 86
T2 87
T1 88
T2 89
T1 90
T2 91
T1 92
T2 93
T1 94
T2 95
T1 96
T2 97
T1 98
T2 99

可以看到用两个线程交替打印的功能很快就实现了。

那么阻塞队列,貌似也可以这样实现:

package com.dhb.reentrantlocktest;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class BlockQuene {

	private static final int CAPACITY = 10;

	private static final ReentrantLock lock = new ReentrantLock(true);

	private static final List<Integer> queue = new ArrayList<>();

	private static int number = 0;

	public static void main(String[] args) {
		new Thread(new Producer(),"P1").start();
		new Thread(new Consumer(),"C1").start();
	}

	private static class Producer implements Runnable{
		@Override
		public void run() {
			while (true) {
				lock.lock();
				try {
					if(queue.size() < CAPACITY) {
						int num = number++;
						queue.add(num);
						System.out.println(Thread.currentThread().getName()+" Producer : "+num);
						TimeUnit.SECONDS.sleep(1);
					}
				}catch (InterruptedException e){
					e.printStackTrace();
				}finally {
					lock.unlock();
				}
			}

		}
	}

	private static class Consumer implements Runnable {
		@Override
		public void run() {
			while (true) {
				lock.lock();
				try {
					if(queue.size() > 0) {
						int num = queue.remove(0);
						System.out.println(Thread.currentThread().getName()+" Consumer : "+num);
					}
				}finally {
					lock.unlock();
				}
			}
		}
	}
}

输出:

P1 Producer : 0
C1 Consumer : 0
P1 Producer : 1
C1 Consumer : 1
P1 Producer : 2
C1 Consumer : 2
P1 Producer : 3
C1 Consumer : 3
P1 Producer : 4
C1 Consumer : 4

这就是本文今天要介绍的主角,ReentrantLock。

上面两个案例实际上都是利用了ReentrantLock的公平锁来完成了业务需求,所谓公平锁,就是说被阻塞的队列中,获取锁的顺序不是随机的,而是依赖于进入等待队列的顺序,满足先进先出。这一点比较像我们之前学习的WaitSet中notify之后出队的状态。因此notify也是具有公平性的。

以上就是对ReentrantLock第一个特性的应用。这个地方在构造函数中,我们传入了true。如果不传入true,则默认创建非公平锁,这就与synchronized的方式类似了。

//公平锁
ReentrantLock lock1 = new ReentrantLock(true);
//非公平锁
ReentrantLock lock2 = new ReentrantLock();

2.可以被Interrupt

我们前面聊过,当线程在获取锁的时候,如果此时锁被其他线程获取,那就进入cxq和EntryList的阻塞队列,此时处于BLOCK状态,除非其他线程释放锁,否则这个状态是不能被Interrupt的。我们可以通过如下方式实验:

package com.dhb.reentrantlocktest;

import java.util.concurrent.TimeUnit;

public class SynchronizedInterruptTest {

	public static final Object lock = new Object();

	public static void main(String[] args) throws InterruptedException{
		long start = System.currentTimeMillis();
		Thread t1 = new Thread(() -> {
			try {
				synchronized (lock) {
					TimeUnit.SECONDS.sleep(60);
				}
			}catch (InterruptedException e){
				System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
				e.printStackTrace();
			}
		},"T1");

		Thread it1 = new Thread(() -> {
			try {
				synchronized (lock) {
					TimeUnit.SECONDS.sleep(60);
				}
			}catch (InterruptedException e){
				System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
				e.printStackTrace();
			}
		},"IT1");

		t1.start();
		it1.start();

		TimeUnit.SECONDS.sleep(1);
		it1.interrupt();
		long cost = System.currentTimeMillis()-start;
		t1.join();
		it1.join();
	}
}

执行结果:

IT1 被打断Interrupted。 cost:60061
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.dhb.reentrantlocktest.SynchronizedInterruptTest.lambda$main$1(SynchronizedInterruptTest.java:25)
	at java.lang.Thread.run(Thread.java:748)

可以看到,IT1到60秒之后才被打断。这说明,在T1没有释放锁的时候,IT1处于BLOCK状态是不能被打断的。

我们再来试试ReentrantLock。ReentrantLock提供了两种方式,当使用lockInterruptibly的时候,可以被打断。

package com.dhb.reentrantlocktest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockInterruptTest {

	public static final ReentrantLock lock = new ReentrantLock();


	public static void main(String[] args) throws InterruptedException{
		long start = System.currentTimeMillis();

		Thread t1 = new Thread(() -> {
			try {
				lock.lock();
				TimeUnit.SECONDS.sleep(60);
			}catch (InterruptedException e){
				System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
				e.printStackTrace();
			}finally {
				lock.unlock();
			}
		},"T1");

		Thread it1 = new Thread(() -> {
			try {
				lock.lock();
				TimeUnit.SECONDS.sleep(60);
			}catch (InterruptedException e){
				System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
				e.printStackTrace();
			}finally {
				lock.unlock();
			}
		},"IT1");
		Thread it2 = new Thread(() -> {
			try {
				lock.lockInterruptibly();
				TimeUnit.SECONDS.sleep(60);
			}catch (InterruptedException e){
				System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
				e.printStackTrace();
			}finally {
				lock.unlock();
			}
		},"IT2");


		t1.start();
		it1.start();
		it2.start();

		TimeUnit.SECONDS.sleep(1);
		it1.interrupt();
		it2.interrupt();

	}
}

上述代码执行结果:

"D:\Program Files\Java\jdk1.8.0_231\bin\java.exe" "-javaagent:D:\Program Files\ideaIU-2020.2.1.win\lib\idea_rt.jar=58930:D:\Program Files\ideaIU-2020.2.1.win\bin" -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_231\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\rt.jar;D:\workspace-git\MyProject\target\classes;D:\m2\log4j\log4j\1.2.17\log4j-1.2.17.jar;D:\m2\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\m2\org\slf4j\slf4j-log4j12\1.7.25\slf4j-log4j12-1.7.25.jar;D:\m2\org\apache\commons\commons-lang3\3.9\commons-lang3-3.9.jar;D:\m2\org\openjdk\jol\jol-core\0.10\jol-core-0.10.jar" com.dhb.reentrantlocktest.ReentrantLockInterruptTest
IT2 被打断Interrupted。 cost:1061
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.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$2(ReentrantLockInterruptTest.java:39)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "IT2" 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.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$2(ReentrantLockInterruptTest.java:45)
	at java.lang.Thread.run(Thread.java:748)
IT1 被打断Interrupted。 cost:60059
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$1(ReentrantLockInterruptTest.java:29)
	at java.lang.Thread.run(Thread.java:748)

可以看到,IT1被打断需要61秒,而IT2被打断只需要1秒。这说明,对于ReentrantLock而言。等待锁的过程中,是支持Interrupt的。

3.支持超时

对,Reentrant还支持超时。可以通过tryLock方法返回是否能获得锁的状态。

package com.dhb.reentrantlocktest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTimeOutTest {


	private static final ReentrantLock lock = new ReentrantLock(true);


	public static void main(String[] args) throws Exception{
		long start = System.currentTimeMillis();

		new Thread(() -> {
			lock.lock();
			try {

				TimeUnit.SECONDS.sleep(60);
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				lock.unlock();
				System.out.println(Thread.currentThread().getName()+" 执行完毕。 cost:"+(System.currentTimeMillis()-start));
			}
		},"T1").start();


		new Thread(() -> {
			boolean isLock = false;
			try {
				isLock = lock.tryLock(10,TimeUnit.SECONDS);
				System.out.println(Thread.currentThread().getName()+" 执行完毕。 cost:"+(System.currentTimeMillis()-start));
			} catch (Exception e) {
				System.out.println(Thread.currentThread().getName()+" 出现异常。 cost:"+(System.currentTimeMillis()-start));
				e.printStackTrace();
			}finally {
				if(isLock) {
					lock.unlock();
				}
			}
		},"T2").start();
	}
}

上述代码执行结果:

T2 执行完毕。 cost:10059
T1 执行完毕。 cost:60057

T2等待了10秒之后,没有获得锁,就结束执行了。这也是synchronized无法提供的功能。

4.配合Condition

我们再回到一开始的面试题,两个线程交替打印的问题,现在将题目改一下,每个线程分别各自打印10次。不再用之前公平锁的方式怎么实现?对,就是Condition可以解决这个问题:

package com.dhb.reentrantlocktest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;

public class ConditionTest {

	public static final ReentrantLock lock = new ReentrantLock();

	public static final Condition con1 = lock.newCondition();

	public static final Condition con2 = lock.newCondition();

	public static  int count = 0;

	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		new Thread(() -> {
			while (true) {
				lock.lock();
				try {
					for(int i=0;i<10;i++) {
						System.out.println(Thread.currentThread().getName() +" "+count++);
						TimeUnit.SECONDS.sleep(1);
					}
					con2.signal();
					con1.await();
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		},"T1").start();

		new Thread(() -> {
			while (true) {
				lock.lock();
				try {
					for(int i=0;i<10;i++) {
						System.out.println(Thread.currentThread().getName() +" "+count++);
						TimeUnit.SECONDS.sleep(1);
					}
					con1.signal();
					con2.await();
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		},"T2").start();


	}

}

上述代码执行结果:

T1 0
T1 1
T1 2
T1 3
T1 4
T1 5
T1 6
T1 7
T1 8
T1 9
T2 10
T2 11
T2 12
T2 13
T2 14
T2 15
T2 16
T2 17
T2 18
T2 19
T1 20
T1 21
T1 22
T1 23
T1 24
T1 25
T1 26
T1 27
T1 28
T1 29
T2 30
T2 31
T2 32
T2 33
T2 34
T2 35
T2 36
T2 37
T2 38
T2 39
T1 40
T1 41
T1 42
T1 43
T1 44
T1 45
T1 46
T1 47
T1 48
T1 49
T2 50
T2 51
T2 52
T2 53
T2 54
T2 55
T2 56
T2 57
T2 58
T2 59
T1 60
T1 61
T1 62
T1 63
T1 64
T1 65
T1 66
T1 67
T1 68
T1 69

可以看到Condition可以根据lock产生条件变量,之后通过await进入等待队列,再通过signal可以唤醒。这与wait和notify机制非常像。但是Condition的优势就在于,可以指定多个条件变量。上面的案例中我们就指定了2个条件变量。我们可以根据需要设置我们所需要的条件。这就比wait和notify机制要灵活得多。

5.可重入性

现在对ReentrantLock和Synchronized进行重入性测试:

package com.dhb.reentrantlocktest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentTest {

	private static final ReentrantLock lock = new ReentrantLock();

	public static final Object lock1 = new Object();


	public static void main(String[] args) throws Exception{

		new Thread(() -> {
				try {
					lock.lock();
					TimeUnit.SECONDS.sleep(1);
					for(int i=0;i<5;i++) {
						lock.lock();
						TimeUnit.SECONDS.sleep(1);
						System.out.println(Thread.currentThread().getName()+"重入:" + i);
						lock.unlock();
					}
				} catch (Exception e) {
					e.printStackTrace();
				}finally {
					lock.unlock();
				}
		},"T1").start();

		TimeUnit.SECONDS.sleep(10);

		new Thread(() -> {
				try {
					synchronized (lock1){
						TimeUnit.SECONDS.sleep(1);
						for(int i=0;i<5;i++) {
							synchronized (lock1) {
								TimeUnit.SECONDS.sleep(1);
								System.out.println(Thread.currentThread().getName()+"重入:" + i);
							}
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
				}

		},"T2").start();
	}
}

执行结果:

T1重入:0
T1重入:1
T1重入:2
T1重入:3
T1重入:4
T2重入:0
T2重入:1
T2重入:2
T2重入:3
T2重入:4

可见,两种锁都是支持重入的。

6.总结

本文介绍了ReentrantLock的用法以及和synchronized进行比较,其对比如下:

  • 1.公平/非公平锁:ReentrantLock可以支持公平和非公平两种锁。而synchronized是非公平的。
  • 2.reentrantLock再BLOCK状态的时候可以被Interrupt,而synchronized处于BLOCK状态的时候不支持Interrupt。
  • 3.ReentrantLock可以通过tryLock设置获取锁的时间,而synchronized不支持。
  • 4.ReentrantLock可以配合Conditaion,实现多个条件变量。而synchronized只能够wait/notify实现一个条件。
  • 5.两种锁都是支持重入的。 以上就是对这两种锁的总结,总的来说,ReentrantLock会更加灵活。但是在通常情况下,如果我们并不需要这些灵活配置,那么使用synchronized,尤其是在jdk1.8之后,jvm进行过专门的优化,可能效率会更好。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 在java中notify和notifyAll的区别

    notify()和notifyAll()以及wait()方法用于线程间的通信。通过调用wait()方法进入WaitSet的线程会一直处于WAITING状态,直到...

    冬天里的懒猫
  • 多线程基础(九):守护线程、yield、join及线程组

    不经意间都已经在上一篇文章中聊到ReentrantLock了,但是回头一看,关于多线程基础的内容还有很多没涉及2到,而ReentrantLock却是属于...

    冬天里的懒猫
  • 多线程基础(七):关于HotSpot中notify方法不具备随机性的证明

    在前面关于wait/notify及notifyAll方法的时候,notify在源码的注释中说到notify选择唤醒的线程是任意的,但是依赖于具体实现的j...

    冬天里的懒猫
  • 如何利用Nexus 5伪造一张门禁卡

    以前在社区看见各种关于RFID的帖子,但是一直没花时间尝试。 近期物业重新安装了小区大门,以前绕一绕,或者钻一下还是能进去的- -, 然而物业说,门禁卡最多...

    逸鹏
  • 【Herding HDU - 4709 】【数学(利用叉乘计算三角形面积)】

    题意:给出n个点的坐标,问取出其中任意点围成的区域的最小值! 很明显,找到一个合适的三角形即可。

    _DIY
  • 做手机的三星又要布局无人驾驶,还带来了3亿美元做投资

    镁客网
  • 顺序表常用使用方法

    复习了一些数据结构的东西,打算把常用的数据结构都实现一下,慢慢来,慢慢来 顺序表是用一组地址连续的存储单元依次存储线性表的数据元素。这里一般考虑的是有序的顺序表...

    张凝可
  • R包ropls的偏最小二乘判别分析(PLS-DA)和正交偏最小二乘判别分析(OPLS-DA)

    在代谢组学分析中经常可以见到主成分分析(PCA)、偏最小二乘判别分析(partial least-squares discrimination analysis...

    用户7585161
  • Python调用DLL

    C语言中的函数默认是__cdecl调用,C++中可用__stdcall来显示声明调用,但也可以用extern “C”

    py3study
  • ASP.NET Core里让NLog根据环境加载配置文件

    我们知道ASP.NET Core自带了appsettings.环境名.json,天生就能做到根据不同的环境选择不同的配置文件。但是NLog的官方例子里只有一份n...

    Edi Wang

扫码关注云+社区

领取腾讯云代金券