前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >多线程基础(八):ReentrantLock的使用及与synchronized的区别

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

作者头像
冬天里的懒猫
发布2020-09-14 14:26:53
4850
发布2020-09-14 14:26:53
举报

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

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

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

代码如下:

代码语言:txt
复制
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();
	}
}

上述代码执行之后如下:

代码语言:txt
复制
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

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

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

代码语言:txt
复制
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();
				}
			}
		}
	}
}

输出:

代码语言:txt
复制
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的方式类似了。

代码语言:txt
复制
//公平锁
ReentrantLock lock1 = new ReentrantLock(true);
//非公平锁
ReentrantLock lock2 = new ReentrantLock();

2.可以被Interrupt

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

代码语言:txt
复制
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();
	}
}

执行结果:

代码语言:txt
复制
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的时候,可以被打断。

代码语言:txt
复制
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();

	}
}

上述代码执行结果:

代码语言:txt
复制
"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方法返回是否能获得锁的状态。

代码语言:txt
复制
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();
	}
}

上述代码执行结果:

代码语言:txt
复制
T2 执行完毕。 cost:10059
T1 执行完毕。 cost:60057

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

4.配合Condition

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

代码语言:txt
复制
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();


	}

}

上述代码执行结果:

代码语言:txt
复制
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进行重入性测试:

代码语言:txt
复制
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();
	}
}

执行结果:

代码语言:txt
复制
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进行过专门的优化,可能效率会更好。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-09-10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前面部分,我们着重讨论了synchronized的使用和wait、notify及notifyAll方法等并发的基础部分。今天,我们来学习另外一种解决方案。
  • 1.交替打印数字和阻塞队列
  • 2.可以被Interrupt
  • 3.支持超时
  • 4.配合Condition
  • 5.可重入性
  • 6.总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档