《java并发编程实战》 第十三章 显示锁的使用

第十三章 显示锁

java5.0以前访问共享对象使用的机制只有synchronized和volatile。java5.0后提供了一种新的机制:ReentrantLock,ReentrantLock并不是代替内置加锁方法,而是当内置锁满足不了需求时,作为一种可高端的选择。
      synchronized 英 ['sɪŋkrənaɪzd]
      volatile 英 [ˈvɒlətaɪl]
      reentrant 英 [ri:'entrənt]

ReentrantLock实现了Lock接口

  Lock接口中定义了一种无条件、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。其中tryLock();是轮询锁通过释放已获得的锁,并退回重新尝试获取所有锁,tryLock(long timeout, TimeUnit unit) 是通过定时释放已获得的锁,放弃本次操作。

public interfece Lock
{
    void lock();//显式加锁
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();//轮询锁通过释放已获得的锁,并退回重新尝试获取所有锁
    boolean tryLock(long timeout, TimeUnit unit) 
        throw InterruptedException;//定时锁获取
    void unlock();
    Condition newCondition();
}

  ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。ReentrantLock同样提供了可重入的加锁语义
  大多情况下,内置锁能很好的工作,但在功能上仍存在一些局限性。例如:无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限等待下去。内置锁在获取的过程中无法中断。内置锁必须在获取该锁的代码块中释放,无法实现非阻塞结构的加锁规则,很难实现带有时间限制的操作。(Lock接口中对应每个方法就是解决内置锁不足)
  简单的ReentrantLock用法:ReentrantLock使用时必须在finally块中释放锁,如果忘记会非常“危险”,当然可以使用静态分析工具FindBugs中的“未释放锁”检查器检查。

Lock lock = new ReentrantLock();
...
lock.lock();
try {
    // 更新对象状态
    // 捕获异常,并在必要时恢复不变性条件
} finally {
    lock.unlock();//一定要记得在finally块里释放
}

显式锁优点1:轮询与定时

  Lock接口中有可定时可轮询的锁获取方法tryLock,解决了内置锁死锁的难题。(在内置锁中,死锁恢复程序的唯一方法是重新启动程序,而放置死锁的唯一方法是构造程序时避免出现不一致的锁顺序)。重新改写10章中动态顺序死锁问题,使用tryLock来获取两个锁,如果不能同时获取,那么就回退并重新尝试。

public class DeadlockAvoidance {
    private static Random rnd = new Random();

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

     private static final int DELAY_FIXED = 1;
     private static final int DELAY_RANDOM = 2;

     static long getFixedDelayComponentNanos(long timeout, TimeUnit unit) {
            return DELAY_FIXED;
     }

     static long getRandomDelayModulusNanos(long timeout, TimeUnit unit) {
            return DELAY_RANDOM;
     }

     static class DollarAmount implements Comparable<DollarAmount> {
            public int compareTo(DollarAmount other) {
                return 0;
            }
            DollarAmount(int dollars) {
            }
        }
     class Account {
            public Lock lock;
            void debit(DollarAmount d) {
            }
            void credit(DollarAmount d) {
            }
            DollarAmount getBalance() {
                return null;
            }
     }
     class InsufficientFundsException extends Exception {
     }
}

  在实现具有时间限制的操作时,定时锁非常有用。 当在带有时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个时限。如果操作不能在指定时间内给出结果,那么程序就会提前结束。 当使用内置锁时,在开始请求锁后,这个操作将无法取消,因此内置锁很难实现带有时间限制的操作
  例如在第六章中旅游门户网站示例中,为询价的每个汽车租赁公司都创建了一个独立的任务,询价的操作中可能徐奥实现对紧缺资源的独占访问。定时的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();
    }   
}

显式锁优点2:锁获取操作可中断

  可中断的锁获取操作能在可取消的操作中使用加锁。 第七章中给出了几种不能响应中断的机制,例如请求内置锁。这些不可中断的阻塞机制将使的实现可取消的任务变得复杂。 lockInterruptibly方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无需创建其他类型的不可中断阻塞机制。

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

//   13-5   可中断的锁获取操作
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;
    }
}

显式锁优点3:非块结构加锁(可以不要像内置锁获取释放都基于代码块)

  在内置锁中,锁的获取和释放等操作都是基于代码块的——释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块,可以避免可能的编码错误,但有时候需要更加灵活的加锁规则。
  在第11章中,通过降低锁的粒度提高了代码的可伸缩性。锁分段技术在基于散列的容器中实现了不同的散列链,以便使用不同的锁。
  我们可以采用类似原则来降低链表中锁的粒度,为每个链表节点使用一个独立的锁,使不同的线程能独立地对链表的不同部分进行操作。每个节点的锁将保护连接指针以及在该节点中存储的数据,因此当遍历或修改链表时,我们必须持有该节点上的这个锁,直到获得了下一个节点的锁,这样我们才能释放上一个节点的锁。。

性能考虑因素

  竞争性能是可伸缩性的关键要素:如果有越多的资源被消耗在锁的管理和调度上,那么应用程序可以得到的资源就越少。锁的实现方式越好,系统调用和上下文切换消耗的资源越少,在共享的内存总线的内存同步通信量也越少。
  java5.0刚出显式锁时,ReentrantLock确实极大的与内置锁体现出吞吐率的差距,ReentrantLock能提供更高的吞吐量。但到了java6中,内置锁的性能得到极大改善,性能并不会由于竞争而急剧下降,并且与ReentrantLock可伸缩性基本相当。
              在这里插入图片描述

扫描二维码关注公众号,回复: 5292086 查看本文章

公平锁与非公平锁

  在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。 在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁(在Semaphore中同样可以选择采用公平或非公平的获取顺序)。
  在激烈竞争的情况下,非公平锁的性能高于公平锁,其中的一个原因时:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。 假设线程A持有一个锁,并且线程B请求这个锁。由于A持有这个锁,因此B挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。此时,如果C也请求这个锁,那么C很可能在B被完全唤醒之前获得,使用及释放这个锁。 这是一种“双赢”的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。
  当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。
              在这里插入图片描述

在Synchronized和ReentrantLock之间如何选用

  在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
  内置锁相比ReentrantLock优点在于:
  a、内置锁在线程转储中能给出哪些帧中获得了哪些锁,并能识别和检测发生死锁的线程。而ReentrantLock在java5.0时还不知道哪些线程持有ReentrantLock,但在java6.0中提供了一个接口,通过对接口注册可以访问ReentrantLock的加锁信息。
  b、内置锁自动加锁与释放锁,ReentrantLock需要在finally中手动释放锁。

读/写ReadWriteLock锁

  ReentrantLock和内置锁相同属于互斥锁,每次最多只能有一个线程持有ReentrantLock。互斥是一种保守的加锁策略,虽然可以避免“写/写”冲突和“写/读”冲突,但是也避免了“读/读”冲突,在许多情况下大多数的操作都是读操作,那么互斥这一保守的加锁策略会影响并发的读取性能。
  如果能够放宽加锁需求,允许多个执行读操作的线程同时访问数据结构,那么将提升程序的性能。只要每个线程都能确保读取到最新的数据,并且在读取数据时不会有其他线程修改数据,那么就不会发生问题。
  在这种情况下就可以使用读/写锁ReadWriteLock:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。ReadWriteLock是一个接口,其中暴露了两个Lock对象,其中一个用于读操作,而另一个用于写操作。要读取有ReadWriteLock保护的数据,必须先获得读取锁,当需要修改ReadWriteLock保护的数据,必须先获得写入锁。

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

读写锁ReadWriteLock在读取锁和写入锁之间的交互可以采用多种实现方式。其中的实现需要考虑以下的问题:
  a、释放优先 :当一个写入操作释放写入锁时,并且队列中同时存在读线程和写线程,那么应该优先选择读线程、写线程、还是最先发出请求的线程?
  b、读线程插队 :如果锁是由读线程持有,但有写线程正在等待,那么新达到的读线程能否立即获得访问权,还是应该在写线程后面等待?如果允许读线程插队到写线程之前,那么将提高并发性,但却可能造成写线程发生饥饿问题。
  c、重入性 :读取锁和写入锁释放可重入?
d、降级 :如果一个线程持有写入锁,那么它能否在不释放该锁的情况下获得读取锁?这可能会使得写入锁被降级为读取锁,同时不允许其他写线程修改被保护的资源,
e、升级 :读取锁能够优先于其他正在等待的读线程和写线程而升级为一个写入锁?

ReentrantReadWriteLock解决互斥加锁问题

  ReentrantReadWriteLock实现了ReadWriteLock 接口,并且ReentrantReadWriteLock为读/写锁都提供了可重入的加锁语义。ReentrantReadWriteLock在构造时也可以选择时一个非公平的锁(默认)还是一个公平的锁
  ReentrantReadWriteLock中的写入锁只能有唯一的拥有者,并且只能由获得该锁的线程来释放。

  13-7的ReadWriteMap 中使用了ReentrantReadWriteLock来包装Map,从而使它能在多个读线程直接被安全地共享,并且仍然避免“读—写”或“写—写”冲突。
  ConcurrentHashMap的性能已经很好了,如果只需要一个并发的基于散列的映射,就可以使用ConcurrentHashMap来代替这种方法,但如果需要对另一种Map实现(例如LinkedHashMap)提供并发性更高的访问,可以使用这种技术。

//       13-7  用读—写锁来包装Map
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 remove(Object key) {
        w.lock();
        try {
            return map.remove(key);
        } finally {
            w.unlock();
        }
    }
    public void putAll(Map<? extends K, ? extends V> m) {
        w.lock();
        try {
            map.putAll(m);
        } finally {
            w.unlock();
        }
    }
    public void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }
    public V get(Object key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    public int size() {
        r.lock();
        try {
            return map.size();
        } finally {
            r.unlock();
        }
    }
    public boolean isEmpty() {
        r.lock();
        try {
            return map.isEmpty();
        } finally {
            r.unlock();
        }
    }
    public boolean containsKey(Object key) {
        r.lock();
        try {
            return map.containsKey(key);
        } finally {
            r.unlock();
        }
    }
    public boolean containsValue(Object value) {
        r.lock();
        try {
            return map.containsValue(value);
        } finally {
            r.unlock();
        }
    }
}

  图13.3给出了ReentrantLock和ReadWriteLock封装ArrayList吞吐量比较。
              在这里插入图片描述
小结:与内置锁相比,显式的Lock在处理锁上更加灵活,但是ReentrantLock不能完全替代synchronized。当访问被保护对象以读取操作为主,那么读/写锁才能提高程序的可伸缩性

猜你喜欢

转载自blog.csdn.net/weixin_41262453/article/details/86971117