ReenTrantLock可重入锁
ReentrantReadWriteLock可重入读写锁
之前学习了线程同步和线程间通信的相关技能,我们主要通过synchronized关键字和通知等待机制去实现上述功能的,除此之外,java提供了功能更强大,使用更方便的Lock接口及实现类,常见的如ReenTrantLock和ReentrantReadWriteLock。
可重入:代表某一线程在持有锁是可以再次进入到同一对象监视器锁定的代码中,之前学过的synchronized也是一个可重入锁,ReenTrantLock基本上可以实现synchronized的所有功能,而且使用上更加灵活,请看如下例子:
public class ReenTrantLockDemo {
public Lock lock=new ReentrantLock();
public void methodA(){
lock.lock();//加锁
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+i);
}
lock.unlock();//释放锁
// 上面方法体内的代码就等同于如下这段代码
// synchronized(this){
// for(int i=0;i<5;i++){
// System.out.println(Thread.currentThread().getName()+i);
// }
// }
}
}
线程分组
上面这段简单的代码就展示了ReentrantLock的一个基本用法,之前我们通过synchronized和wait()和notify()/notifyAll()方法结合使用实现等待通知,ReentrantLock也可以实现同样的功能,就是与Condition()类结合使用。
Condition()有 await()/signal()/signalAll()方法,作用相当于wait()/notify()/notifyAll()方法,我们可以使用它来实现等待通知模式;
看如下例子是使用ReentrantLock和Condition来实现生产者消费者模式的例子。
//定义生产者消费者行为的类
public class ReenTrantLockPC{
private Lock lock=new ReentrantLock();
private Condition conditionA=lock.newCondition();
private Condition conditionB=lock.newCondition();
//定义了两个Condition的实例,
private List list;
private static final int MAX_SIZE=5;
public ReenTrantLockPC(List list) {
this.list = list;
}
//生产方法
public void product(String str){
try{
lock.lock();
if(list.size()0){
list.remove(0);
System.out.println(Thread.currentThread().getName()+"仓库目前商品个数为:"+list.size());
conditionB.signal();
}else{
System.out.println(Thread.currentThread().getName()+"仓库空了");
conditionA.signal();
conditionB.await();
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
//测试方法
public void test(){
List list=new LinkedList<>();
ReenTrantLockPC reenTrantLockPC=new ReenTrantLockPC(list);
for(int i=0;i<10;i++){
new Thread(()->{
while(true)reenTrantLockPC.product("*");
},"生产线程"+i).start();
new Thread(()->{
while (true)reenTrantLockPC.consume();
},"消费线程"+i).start();
}
}
可以看到程序一直在正常进行下去,对如上代码进行简单分析吧,思考如下两个问题:
一、Condition类的使用有哪些要注意的地方?
答:1.不能new(),而要使用lock.newCondition()来获取一个Condition的实例,这意味着每一个Condition实例必须要和一个锁绑定在一起。
2.Condition的await()/signal()/signalAll()方法等方法必须在该实例所绑定的锁的lock.lock()和 lock.unlock()之间被调用。
举一反三:回想之前wait()/notify()/notifyAll()方法,也是在被synchronized修饰的方法或代码块内被调用,且必须由这段代码所对应的对象监视器来进行调用。
二、本例中为什么要有两个Condition的实例呢?各有什么作用?
答:为了进行线程分组,如上我们可以看到conditionA在list长度达到最大长度时让生产线程等待,其它情况下则唤醒生产线程,conditionB在list长度为0时让消费线程等待,其它情况下则唤醒消费线程,这就相当于对线程进行了分组,在需要通知生产线程时通知生产线称,反之在需要通知消费线程时通知消费线程。
回想我们使用notify()/notifyAll()来实现等待通知模式时,唤醒生产线称还是消费线程完全随机,而且在某些情况下由于唤醒的一直是同类线程导致程序陷入假死状态。而使用Condition来进行线程分组则永远不用担心这个问题。
公平锁/非公平锁
线程分组只是ReentrantLock的一个优势,除此之外,它还可以设置公平锁/非公平锁,公平锁就是获取锁的顺序是公平的,先来的先得到锁。而非公平锁就是之前我们一直看到的那种锁的抢占机制,得到锁完全是随机的,synchronized实现的锁就是非公平锁,ReentrantLock默认也是非公平锁,大部分情况下我们使用的都是非公平锁,因为一般而言效率高于公平锁。但 ReentrantLock同样提供了一个public ReentrantLock(boolean fair)构造方法,在实例化一个ReentrantLock时可以new ReentrantLock(true)来得到一个公平锁。
ReentrantLock其它常用方法
getHoldCount();当前线程保持锁定的次数,也就是当前线程调用lock()次数减去调用unlock()的次数,例如A方法的lock.lock()和 lock.unlock()之间调用B方法,B方法的lock.lock()和 lock.unlock()之间调用了C方法,C方法也使用了lock.lock(),当某个线程运行到C方法的锁定代码块时调getHoldCount()方法将返回3。(这个例子也详细说明了啥叫可重入)
getQueueLength();返回等待该锁的估计数;
getWaitQueueLength(Condition condition);返回等待中的与给定condition相关的线程数。
hasQueuedThread(Thread thread);查询制定的线程是否在等待此锁定;
hasQueuedThreads();查询是否有线程正在等待此锁定;
hasWaiters(condition);是否有线程等待给定condition。
isFair();返回是否是公平锁
isHeldByCurrentThread();是否被当前线程所持有;
isLocked();是否有线程锁定。
lockInterruptibly();若当前线程未被中断,则锁定,若已中断,则抛出异常。
tryLock();它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。拿不到锁也会执行后面的代码。
while (true){
if(lock.tryLock()){
//这里写要被锁定后执行的代码
lock.unlock();
}
tryLock(long timeout, TimeUnit unit);在给定时间之前去等待获取该锁,如果获取失败(即锁已被其他线程获取),则返回false。
condition.awaitUninterruptibly();等待直到中断,与await()方法的区别在于await()在等待时若当前线程中断则抛异常,而该方法不会抛出异常,只是退出等待。
特点:写锁独占,读锁共享。
读-读共享,读-写、写-读、写-写互斥。
如下是一个使用读写锁来对map进行封装的例子,可以用来实现高效的缓存等等功能。
public class ReentrantReadWriteLockDemo {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
private final Map map;
public ReentrantReadWriteLockDemo(Map map) {
this.map = map;
}
public V put(K key, V value){
writeLock.lock();
try {
return map.put(key, value);
}finally {
writeLock.unlock();
}
}
public V get(K key){
readLock.lock();
try {
return map.get(key);
}finally {
readLock.unlock();
}
}
}
就不进行测试了,只需要知道改变值是用写锁,读取值时用读锁,就能利用该读写锁的特点来提高我们代码的运行效率。
注意上面我们总结的可重入锁的特点和功能,基本上读写锁也是具备的,比如condition,公平锁等。
以上所有内容均总结自高洪岩的《Java编程多线程核心技术》一书
多线程学习总结(一):基础知识
多线程学习总结(二):同步与并发
多线程学习总结(三):线程间通信
多线程学习总结(四):可重入锁和读写锁