第十三章、显式锁

显式锁

java5.0以前访问共享对象使用的机制只有synchronized和volatile。java5.0后提供了一种新的机制:ReentrantLock。ReentrantLock并不是代替内置加锁方法,而是当内置锁满足不了需求时,作为一种可高端的选择。

一、Lock与ReentrantLock

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

为什么要创建一种与内置锁如此相似的加锁机制?

大多情况下,内置锁能很好的工作,但在功能上仍存在一些局限性。例如:无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限等待下去(tryLock)。内置锁在获取的过程中无法中断。内置锁必须在获取该锁的代码块中释放,无法实现非阻塞结构的加锁规则,很难实现带有时间限制的操作。(Lock接口中对应每个方法就是解决内置锁不足)

下面给出了Lock接口的标准的使用方法。必须在finally中释放锁,否则,如果在被保护的代码中抛出了异常,那么这个锁将永远无法释放。

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

如果没有在finally中释放锁,那么相当于启动了一个定时炸弹,程序不会自动清除锁,千万不要忘记。

1、轮询锁和定时锁(避免死锁发生)

可定时的与可轮询的锁获取模式是由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 {
     }
}

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

例子:带有时间限制的锁操作例子。

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方法)

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

定时的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可伸缩性基本相当。

二、公平性(公平锁和非公平锁)

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

四、在Synchronized和ReentrantLock之间进行选择

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

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

五、读-写锁(ReentrantLock)

ReentrantLock和内置锁相同属于互斥锁,每次最多只能有一个线程持有ReentrantLock。互斥是一种保守的加锁策略,虽然可以避免“写/写”冲突和“写/读”冲突,但是也避免了“读/读”冲突,在许多情况下大多数的操作都是读操作,那么互斥这一保守的加锁策略会影响并发的读取性能。
  如果能够放宽加锁需求,允许多个执行读操作的线程同时访问数据结构,那么将提升程序的性能。只要每个线程都能确保读取到最新的数据,并且在读取数据时不会有其他线程修改数据,那么就不会发生问题。

public interface ReadWriteLock 
{
   Lock readLock();
   Lock writeLock();
}

读写锁ReadWriteLock在读取锁和写入锁之间的交互可以采用多种实现方式。其中的实现需要考虑以下的问题:

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

与ReentrantLock类似,ReentrantReadWriteLock在构造的时候可以选择是一个非公平的锁(默认)还是一个公平的锁。在公平的锁中,等待时间最长的线程将优先获得锁。如何这个锁被读线程持有,而另外一个线程请求写入锁,那么其他读线程都不能获得锁,知道写线程使用完并且释放了写入锁。
写线程降级为读线程是可以的,但是从读线程升级为写线程则是不可以的。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 多线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
 * 但是
 * 如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或者写。
 * 小总结
 *  读读能并存
 *  读写不能并存
 *  写写不能并存
 *
 *  写操作:原子+独占,整个过程必须是一个完整的同一体,中间不许被分割被打断。
 *
 */
class MyCache //资源类
{
    private volatile Map<String,Object> map = new HashMap<>();
    private ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();

    public void put(String key,Object value)
    {
        rwlock.writeLock().lock();
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t 正在写入:"+key);
            try
            {
                TimeUnit.MILLISECONDS.sleep(300);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+"\t 写入完成");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally {
            rwlock.writeLock().unlock();
        }


    }
    public void get(String key)
    {
        rwlock.readLock().lock();
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t 正在读取:");
            try
            {
                TimeUnit.MILLISECONDS.sleep(300);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName()+"\t 读取完成:"+result);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally {
            rwlock.readLock().unlock();
        }
    }
}
public class ReadWirteLockDemo
{
    public static void main(String[] args)
    {
        MyCache myCache = new MyCache();
        for (int i=1;i<=5;i++)
        {
            final int tempInt = i;
            new Thread(()->{
                myCache.put(tempInt+"",tempInt+"");

            },String.valueOf(i)).start();
        }
        for (int i=1;i<=5;i++)
        {
            final int tempInt = i;
            new Thread(()->{
                myCache.get(tempInt+"");

            },String.valueOf(i)).start();
        }

    }
}

小结:与内置锁相比,显式的Lock在处理锁上更加灵活,但是ReentrantLock不能完全替代synchronized。当访问被保护对象以读取操作为主,那么读/写锁才能提高程序的可伸缩性

发布了105 篇原创文章 · 获赞 18 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_38367817/article/details/103890694
今日推荐