【读书笔记】《Java并发编程实战》第十三章 显式锁

1.Lock与ReentrantLock

1.1Lock接口

Lock与synchronized的区别:Lock接口中增加了一些synchronized关键字不具备的特性,并且加锁和解锁都是显式的。

如下表为,Lock接口提供的synchronized关键字不具备的主要特性:

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁 与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

下面介绍一下Lock接口都含有哪些API以及API的功能

如下为Lock接口:

//Lock接口
public interface Lock {
	//获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回
	void lock();
	
	//可中断地获取锁,该方法可以响应中断,即在锁获取中可以中断当前线程
	void lockInterruptibly() throws InterruptedException;
	
	//尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false
	boolean tryLock();

	//超时的获取锁(有时间限制的获取锁的方法并且允许被中断)
	boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;

	//释放锁
	void unlock();

	//获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁
	Condition newCondition();
}

1.2ReentrantLock重入锁

ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。在获取ReentrantLock时,有着与进入同步代码块相同的内存语义,在释放ReentrantLock时,有着与退出同步代码块相同的内存语义(Java内存模型JMM后面我会有一篇专门的文章记录)。

下面代码展示了Lock的标准使用方式:

//使用ReentrantLock来保护对象状态

Lock lock = new ReentrantLock();
...
lock.lock();
try {
	...
}
finally {
	lock.unlock();
}

注意:必须在finally块中释放锁,否则,如果在被保护的代码中抛出了异常,那么这个锁永远都无法释放。

ReentrantLock重入锁,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁还支持获取锁时的公平和非公平的选择。

这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁时公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的,但是公平锁机制往往没有非公平的效率高。

1.3显式锁的优点1——轮询锁

可轮询的锁的获取模式可以使用tryLock方法来实现。

如果不能获得所有需要的锁,那么可以使用可轮询或可定时的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁(或者至少会将这个失败记录到日志,并采取其他措施)。

下面的程序中使用tryLock来获取两个锁,如果不能同时获得,那么就回退并重新尝试。

	 //通过tryLock来避免锁顺序死锁
     public boolean transferMoney(Account fromAcct,
                                  Account toAcct,
                                  DollarAmount amount,
                                  long timeout,
                                  TimeUnit unit)
          throws InsufficientFundsException, InterruptedException {
         long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
         long randMod = getRandomDelayModulusNanos(timeout, unit);
         long stopTime=System.nanoTime()+unit.toNanos(timeout);
         //使用tryLock来获取两个锁,如果不能同时获得,那么就回退并重新尝试
         while(true){
             if(fromAcct.lock.tryLock()){  //使用tryLock来获取锁
                 try{
                     if(toAcct.lock.tryLock()){
                         try{
                             if(fromAcct.getBalance().compareTo(amount)<0)
                                 throw new InsufficientFundsException();
                             else{
                                 fromAcct.debit(amount);
                                 toAcct.credit(amount);
                                 return true;
                             }
                         }finally{
                             toAcct.lock.unlock();
                         }
                     }
                 }finally{
                     fromAcct.lock.unlock();  //无论成功与否都会释放所有锁
                 }
             }
             //如果在指定时间内不能获得所有需要的锁,那么transferMoney将返回一个失败状态,从而使该操作平缓地失败。
             if(System.nanoTime()<stopTime)
                 return false;      
             //在休眠时间中包含固定部分和随机部分,从而降低发生活锁的可能性。
             NANOSECONDS.sleep(fixedDelay+rnd.nextLong()%randMod);
         }
     }

1.4显式锁的优点2——定时锁

在实现具有时间限制的操作时,定时锁非常有用。 当在带有时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个时限。如果操作不能在指定时间内给出结果,那么程序就会提前结束。 当使用内置锁时,在开始请求锁后,这个操作将无法取消,因此内置锁很难实现带有时间限制的操作

下面代码中,定时的tryLock能够在这个带有时间限制的操作中实现独占加锁行为:

//带有时间限制的加锁
public class TimedLocking {
    private Lock lock = new ReentrantLock();
    //定时的tryLock能够在这个带有时间限制的操作中实现独占加锁行为。
    public boolean trySendOnSharedLine(String message,
                                       long timeout,TimeUnit unit)
                                  throws InterruptedException{
        long nanosToLock=unit.toNanos(timeout)
                -estimatedNanosToSend(message);
        if(!lock.tryLock(nanosToLock,NANOSECONDS)) //如果不能再指定时间内获得锁,就失败
            return false;
        try{
            return sendOnSharedLine(message);
        }finally {
            lock.unlock();
        }
    }
    private boolean sendOnSharedLine(String message) {
        //传送信息
        return true;
    }
    long estimatedNanosToSend(String message) {
            return message.length();
    }   
}

1.5显式锁的优点3——锁获取操作可中断

可中断的锁获取操作能在可取消的操作中使用加锁。

如下代码中,InterruptibleLocking 使用了lockInterruptibly来实现sendOnSharedLine,以便在一个可取消的任务中调用它。 定时的tryLock同样能响应中断,因此当需要一个定时的和可中断的锁获取操作时,可以使用tryLock方法

//可中断的锁获取操作
public class InterruptibleLocking {
    private Lock lock = new ReentrantLock();
    public boolean sendOnSharedLine(String message)
            throws InterruptedException {
        lock.lockInterruptibly();
        try {
            return cancellableSendOnSharedLine(message);
        } finally {
            lock.unlock();
        }
    }
    private boolean cancellableSendOnSharedLine(String message) throws InterruptedException {
        /* send something */
        return true;
    }
}

1.6显式锁的优点4——非块结构加锁

在内置锁中,锁的获取和释放等操作都是基于代码块的——释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块,可以避免可能的编码错误,但有时候需要更加灵活的加锁规则

2.性能考虑

竞争性能是可伸缩性的关键要素:如果有越多的资源被消耗在锁的管理和调度上,那么应用程序可以得到的资源就越少。锁的实现方式越好,系统调用和上下文切换消耗的资源越少,在共享的内存总线的内存同步通信量也越少。

java5.0刚出显式锁时,ReentrantLock确实极大的与内置锁体现出吞吐率的差距,ReentrantLock能提供更高的吞吐量。但到了java6中,内置锁的性能得到极大改善,性能并不会由于竞争而急剧下降,并且与ReentrantLock可伸缩性基本相当。

在这里插入图片描述

3.在synchronized和ReentrantLock之间如何选用

在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。

内置锁相比ReentrantLock优点在于

  • 内置锁在线程转储中能给出哪些帧中获得了哪些锁,并能识别和检测发生死锁的线程。而ReentrantLock在java5.0时还不知道哪些线程持有ReentrantLock,但在java6.0中提供了一个接口,通过对接口注册可以访问ReentrantLock的加锁信息。
  • 内置锁自动加锁与释放锁,ReentrantLock需要在finally中手动释放锁。

ReentrantLock相比内置锁优点在于

  • ReentrantLock可以非阻塞的获取锁,但synchronized不行
  • ReentrantLock可以超时地获取锁,但synchronized不行
  • ReentrantLock可以被中断地获取锁,但synchronized不行

4.读-写锁

4.1什么是读写锁?

读写锁:读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。(读操作允许多线程同时访问共享变量,但是写操作线程独占共享变量)读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

在读多于写的情况下,读写锁能够提供比排他锁更好的并发性和吞吐量。Java并发包提供读写锁实现的是ReenrantReadWriteLock,它提供的特性如下表:

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入 该锁支持重进入,以读写线程为例:读线程在获取读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

下面为ReadWriteLock接口的API:

//ReadWriteLock接口
public interface ReadWriteLock {
	Lock readLock();
	Lock writeLock();
}

4.2锁降级的具体介绍

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的写锁),再获取到读锁,随后释放(先前拥有的)写锁的过程

下面为锁降级的示例:

public void processData() {
	readLock.lock();
	if (!update) {
		//必须先释放读锁
		readLock.unlock();
		//锁降级从写锁获取到开始
		writeLock.lock();
		try {
			if (!update) {
				...//准备数据的流程
				update = true;
			}
			readLock.lock();
		} finally {
			writeLock.unlock();
		}
		//锁降级完成,写锁降级为读锁
		try {
			...//使用数据的流程
		} finally {
			readLock.unlock();
		}
	}
}

上面示例中,当数据发生变更后,update变量(boolean类型且volatile修饰)被设置为false,此时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

ReentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。

Condition接口

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。

Condition接口也提供了类似Object的监视器方法与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

下面为Objecy的监视器方法和Condition接口的对比表:

对比项 Object Monitor Methods Condition
前置条件 获取对象的锁 调用Lock.lock()获取锁
调用Lock.newCondition()获取Condition对象
调用方式 直接调用
如:object.wait()
直接调用
如:condition.await()
等待队列个数 一个 多个
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并进入等待状态到将来的某个时间 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。

下面为Condition对象的使用示例:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

public void conditionWait() throws InterruptedException {
	lock.lock();
	try {
		condition.await();
	} finally {
		lock.unlock();
	}
}

public void conditionSignal() throws InterruptedException {
	lock.lock();
	try {
		condition.signal();
	} finally {
		lock.unlock();
	}
}

如上所示,一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

猜你喜欢

转载自blog.csdn.net/Handsome_Le_le/article/details/107982307
今日推荐