显式锁

11.1Lock和ReentrantLock

Lock接口源码:

  1. //Lock提供了无条件的、可轮询的、定时的、可中断的锁获取操作。
  2. public interface Lock{
  3. void lock();
  4. void locaInterruptibly() throws InterruptedException;
  5. boolean tryLock();
  6. boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
  7. void unlock();
  8. Condition newCondition();
  9. }

ReentrantLock实现了Lock接口,提供了与synchronized相同的互斥和内存可见性的保证。

获得ReentrantLock的锁与进入synchronized块有着相同的内存语义。

释放ReentrantLock的锁与退出synchronized块有着相同的内存语义。

ReentranLock提供了与synchronized一样的可重入加锁的语义。(可重入锁:可以重复获取相同的锁,但是需要释放,加锁次数一定要与释放次数保持一致,否则会死锁。synchronized会自动释放,ReentranLock需要手动释放)

ReentranLock支持Lock接口定义的所有获取锁的模式,与synchronized相比,ReentrantLock为处理不可用的锁提供了更多灵活性。

可重入演示

  1. public void test(){
  2. ReentrantLock lock = new ReentrantLock();
  3. new Thread(new Runnable(){
  4. public void run(){
  5. try{
  6. lock.lock();
  7. System.out.println("第一次获取锁");
  8. int index - 1;
  9. while(true){
  10. try{
  11. lock.lock();
  12. System.out.println("第"+ (++index) +"次获取锁");
  13. if(index == 10){
  14. break;
  15. }
  16. }finally{
  17. lock.unlock();
  18. }
  19. }
  20. }finally{
  21. lock.unlock();
  22. }
  23. }
  24. }).start();
  25. }

Lock接口使用的规范形式

  1. //锁必须在finally块中释放,如果锁守护的代码在try块之外抛出了异常,它将永远都不会被释放了。
  2. Lock lock = new ReentrantLock();
  3. ...
  4. lock.lock();
  5. try{
  6. //更新对象的状态
  7. //捕获异常,必要时恢复到原来的不变约束
  8. }finally{
  9. lock.unlock();
  10. }

11.1.1可轮询的和可定时的锁请求

可定时的与可轮询的锁获取模式,是由tryLock方法实现,与无条件的锁获取相比,它具有更完善的错误恢复机制。

内部锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错

可定时的与可轮询的锁提供了另一个选择:可以规避死锁的发生

如果你不能获得所有需要的锁,那么使用tryLock使你能重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试休眠时间由一个特定的组件管理,并由一个随机组件减小活锁发生的可能性,如果一定时间内,没有能获得所有需要的锁,就会返回失败状态

示例

  1. public boolean transferMoney(Account fromAcct,
  2. Account toAcct,
  3. DollarAmount amount,
  4. TimeUnit unit)
  5. throws InsufficientFundsException,InterruptedException{
  6. long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
  7. long randMod = getRandomDelayModulusNanos(timeout, unit);
  8. long stopTime = System.nanoTime() + unit.toNanos(timeout);
  9. while(true){
  10. //使用tryLock避免锁顺序死锁
  11. //这里使用tryLock试图获得两个锁,如果不能同时获得两个,就回退,并重新尝试。
  12. if(fromAcct.lock.tryLock()){
  13. try{
  14. if(toAcct.lock.tryLock()){
  15. try{
  16. if(fromAcct.getBalance().compareTo(amount) < 0){
  17. throw new InsufficientFundsException();
  18. }else{
  19. fromAcct.debit(amount);
  20. toAcct.credit(amount);
  21. return true;
  22. }
  23. }finally{
  24. toAcct.lock.unlock();
  25. }
  26. }
  27. }finally{
  28. fromAcct.lock.unlock();
  29. }
  30. }
  31. if(System.nanoTime() < stopTime){
  32. return false;
  33. }
  34. NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
  35. }
  36. }

示例

  1. //具有预定时间的锁
  2. public boolean trySendOnSharedLine(String message,
  3. long timeout,
  4. TimeUnit unit)
  5. throws InterruptedException{
  6. long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
  7. if(!lock.tryLock(nanosToLock, NANOSECONDS)){
  8. return false;
  9. }
  10. try{
  11. return sendOnSharedLine(message);
  12. }finally{
  13. lock.unlock();
  14. }
  15. }

11.1.2可中断的锁获取操作

正如定时锁的获得操作允许在限时活动内部使用独占锁。

可中断的锁获取操作允许在可取消的活动中使用。

当你正在响应中断的时候,lockInterruptibly方法能够使你获得锁,并且由于它是内置于Lock的,因此不必再创建其他种类不可中断的阻塞机制。

一个可中断的锁获取操作的规范形式:需要两个try块。

定时的tryLock同样响应中断,因此当你再需要获取定时和可中断的锁时可以使用tryLock这个方法

示例

//可中断的锁获取请求
public boolean sendOnSharedLine(String message) throws InterruptedException{
    lock.lockInterruptibly();
    try{//第一个try块,防止抛出InterruptedException
        return cancellableSendOnSharedLine(message);
    }finally{
        lock.unlock();
    }
}

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();
    }
}

lock 与 lockInterruptibly比较区别在于:

  • lock 优先考虑获取锁,待获取锁成功后,才响应中断。
  • lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。

详细区别:
ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException

ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程

11.1.3非块结构的锁

在内部锁中,获取和释放这样成对的行为是块结构的。但是有时候需要更灵活的加锁规则。

例如:

在链表中,我们可以通过为每个链表节点应用相似的原则来减小锁的粒度,从而允许不同的线程独立地操作链表的不同部分。

如果要遍历或者修改链表,我们必须得得到响应的锁,并持有它直到获得了下一个节点的锁,之后,才能释放前一个节点的锁。这项技术的例子被称为**连锁式(锁联接)**。

11.2对性能的考量

java5.0中,ReentrantLock提供的竞争上的性能要远远优于内部锁。

java6.0中。改善了管理内部锁的算法,类似于ReentrantLock使用的算法,两者的性能非常接近。

竞争时的性能是可伸缩性的关键:如果有越多的资源花费在锁的管理和调度上,那么留给应用程序的就会越少

性能和可伸缩性是基于平台的:比如CPU、处理器数量、高速缓存大小、JVM特性。所有这些因素都会随时间而改变。

性能是一个不断变化的目标,昨天的基准限时X比Y更快,但现在可能已经过时了。

11.3公平性

ReentrantLock构造函数提供了两种公平性的选择:(Semaphore同样提供了公平和非公平的获取顺序)

  • 创建非公平锁(默认)

    • 非公平锁允许“闯入”,当请求这样的锁时,如果锁的状态变为可用线程的请求可以在等待线程的队列中向前跳跃,获得该锁
  • 创建公平锁

    • 线程按顺序请求获得公平锁。

在公平锁中,如果锁已经被其他线程占有,新的线程会加入到等待队列,或者已经有一些线程在等待锁了。(可轮询的tryLock还是会“闯入”)

在非公平锁中,线程只有当锁正在被占用时才会等待

为什么默认是非公平的锁

因为:当发生加锁的时候,**公平锁会因为挂起和重新开始线程的代价带来巨大的性能开销**。而且,承诺一个阻塞的线程最终能够获得锁——通常已经够用了。除了有一些算法依赖公平的队列外,大多数情况下,**非公平锁性能的优势超过了公平的排队**。

为什么非公平锁更快

因为:**挂起的线程重新开始,与它真正开始运行,两者之间会产生严重的延迟**。

例子:线程A持有一个锁,线程B请求该锁,此时锁正在使用,线程B会被挂起,当A释放锁后,B重新开始。与此同时,C请求该锁,那么C得到了很好的机会获得这个锁,使用它,并且甚至可能在B被唤醒前就已经释放了该锁。

B并没有比其他任何线程晚得到锁,C更早的得到了锁,吞吐量得到了改进。

与默认的ReentrantLock一样,内部锁没有提供确定的公平性保证。

11.4在synchronized和ReentrantLock之间进行选择

ReentrantLock与内部锁在加锁和内存语义上是相同的,在以下附加特性的语义上也是相同的,比如定时锁的等待、可中断锁的等待、公平性、以及实现非块结构的锁 。

在性能上,ReentrantLock的性能略微胜过synchronized。

但是内部锁相比于显式锁仍然具有很大的优势:

synchronized人们更熟悉,也更简洁。ReentrantLock是危险的,如果没有在finally块中调用unlock,你的代码将很可能看起来能够正常运行,但已经埋下了定时炸弹。
//只有当内部锁不能满足使用时,ReentrantLock才被作为更高级的工具。
//当你需要以下高级特性时,才应该使用:可定时的、可轮询的、可中断的锁获取操作、公平队列、或者非块结构的锁。否则,请使用synchronized。

ReentrantLock提供了一个管理和调试接口,锁可以使用这个接口进行注册,并通过其他管理和调试接口,从线程转储中得到ReentrantLock 的加锁信息。ReentrantLock的非块结构的特性仍然意味着获取锁不能依赖于特定的栈结构。

未来的性能改进可能更倾向于synchronized,因为syncrhonized是内置于JVM的,它能够进行优化,比如对线程限制的锁对象的锁省略,粗化锁来减小内部锁的同步性。

11.5读-写锁

ReentrantLock实现了标准的互斥锁:一次最多只有一个线程能够持有相同ReentrantLock。但是却过分地限制了并发性。(它甚至避免了“读/读”的情况)

读-写锁一个资源能够被多个读者访问,或者被一个写者访问,两者不能同时进行

ReadWriteLock接口源码

//看起来是两个分离的锁,读取的锁和写入的锁只不过是同一的读-写锁对象的两个视角。
public interface ReadWriteLock{
    Lock readLock();
    Lock writedLock();
}

读写锁实现的加锁策略允许多个同时存在的读者,但是只允许一个写者。

与Lock一样,ReadWriteLock允许多种实现,造成了性能、调度保证、获取优先、公平性、以及加锁语义等方面的不尽相同。

读写锁的涉及是用来改进性能的,使得特定情况下能够有更好的并发性。在多处理器系统中,频繁的访问主要为读取数据结构的时候,读写锁能够改进性能在其他情况下运行的情况比独占锁稍差一些,这归因于它更大的复杂性

使用读写锁是,最好通过系统进行剖析来判断。

读取和写入锁之间的互动可以有很多种实现。ReadWriteLock的一些实现选择如下:

  • 释放优先

    • 当写者释放写入锁,并且读者和写者都排在队列中,应该选择哪个——读者,写者,还是先请求的那个?
  • 读者闯入

    • 如果锁由读者获得,但是有写者正在等待,那么新到达的写者应该被授予读取的权力么?还是应该等待?允许读者闯入到写者之前提高了并发性,但是却带来了写者饥饿的风险。
  • 重进入

    • 读取锁和写入锁允许重入吗?
  • 降级

    • 如果线程持有写入锁,它能够在不是放该锁的情况下获得读取锁吗?这可能会造成写者“降级”为一个读者,同时不允许其他写者修改这个被守护的资源。
  • 升级

    • 读取锁能够优先于其他的读者和写者升级为一个写入锁吗?大多数读写锁的实现不支持升级,因为在没有显式的升级操作的情况下,很容易造成死锁。(如果有两个读者试图升级到同一个写入锁,并都不释放资源)

ReentrantReadWriteLock为两个锁提供了可重进入的加锁语义。ReentrantReadWriteLock能够被构造成非公平(默认)或者是公平的。

在公平的锁中,选择权交给等待时间最长的线程:如果锁由读者获得,而一个线程请求写入锁,那么不再允许读者获得读写锁,直到写者被受理,并且已经释放了写入锁

在非公平的锁中,线程允许访问的顺序是不定的

写者可以降级为读者,读者不能升级为写者(会造成死锁)

示例

public class ReadWriteMap<K,V>{
    private final Map<K,V> map;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock r = lock.readLock();
    private final Lock w = lock.writeLock();

    public ReadWriteMap(Map<K,V> map){
        this.map = map;
    }

    public V put(K key, V value){
        w.lock();
        try{
            return map.put(key, value);
        }finally{
            w.unlock();
        }
    }

    public V get(Object key){
        r.lock();
        try{
            return map.get(key);
        }finally{
            r.unlock();
        }
    }
}

总结

显式的Lock与内部锁相比提供了一些可扩展的特性:

处理不可用的锁时更好的灵活性,对队列行为更好的控制。

Reentrant Lock不能完全替代synchronized,只有当你需要synchronized没有提供的特性时才能使用。

读写锁允许多个读者并发访问被守护的对象,当访问多为读取数据结构的时候,它具有改进可伸缩性的潜力。