并发编程学习笔记之显示锁(十)

ReentrantLock(重进入锁)并不是作为内部锁(synchronized)机制的替代,而是当内部锁被证明受到局限时,提供可选择的高级特性.

1. Lock 和 ReentrantLock

Lock接口:

public interface Lock {

 
    void lock();

    
    void lockInterruptibly() throws InterruptedException;

   
    boolean tryLock();

    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    
    void unlock();

   
    Condition newCondition();
}

与内部加锁机制不同,Lock提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显示的.

Lock的实现必须提供具有与内部加锁相同的内存可见性的语义.但是加锁的语义、调度算法、顺序保证,性能特性这些可以不同.

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

获得ReentrantLock的锁与进入synchronized块有着相同的内存语义,释放ReentrantLock锁与退出synchronized块有相同的内存语义.

ReentrantLock提供了与synchronized一样的可重入加锁的语义.ReentrantLock支持Lock接口定义的所有获取锁的模式.

一句话synchronized能做的,ReentrantLock都能做,但是ReentrantLock为处理不可用的锁提供了更多灵活性(好吧,ReentrantLock写起来比较麻烦)

为什么要使用显示锁

内部锁在大部分情况下都能很好地工作,但是有一些功能上的局限--不能中断那些正在等待获取锁的线程,并且在请求锁失败的情况下,必须无限等待.

内部锁必须在获取他们的代码块中被释放:这很好地简化了代码,与异常处理机制能够良好的互动,但是在某些情况下,一个更灵活的加锁机制提供了更好的活跃度和性能.

public class LockTest {
        Lock lock  = new ReentrantLock();

        public void testLock(){
            lock.Lock();
            try {
            // 需要加锁的代码..

            }finally {
                lock.unlock();
            }
        }
}

这个模式在某种程度上比使用内部锁更加复杂:锁必须在finally块中释放.

另一方面,如果锁守护的代码在try块之外抛出了异常,它将永远都不会被释放了;

如果对象能够被置于不一致的状态,可能需要额外的try-catch,或try-finally块.

显示的lock的缺点

使用lock之后必须unlock释放锁,这也是ReentrantLock不能完全替代synchronized的原因.

它更加危险,因为当程序的控制权离开了守护的块时,不会自动清除锁.

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

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

使用内部锁,发生死锁时唯一的恢复方法是重启程序,唯一的预防方法是在构建程序时不要出错,所以不可能允许不一致的锁顺序.

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

使用方式:

public class LockSample {
    //创建一个锁的实例
    Lock lock = new ReentrantLock();

    public void methodA(){
        lock.lock();
        try {
            System.out.println("执行了方法A");
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void methodB(){
        lock.lock();
        try {
            System.out.println("执行了方法B");
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }


    public static void main(String [] args){

        LockSample lockSample = new LockSample();
        lockSample.methodA();
        //methodB()方法必须在锁可用的时候才会执行
        lockSample.methodB();
    }  
}

使用tryLock能解决第九篇博客死锁,提到过的动态的顺序死锁问题.

public class LockTest {

    public static void main(String [] args){
                LockTest lockTest = new LockTest();
        Account fromAccount = new Account();
        Account toAccount = new Account();
        Account account = new Account();
        //开启一个新线程,获取两个用户的锁,这个方法是假设,对象的锁已经被获得用的.
        new Thread(){
            @Override
            public void run(){
                //这两个方法的内部实现就是Thread.sleep()将代码阻塞住.
                fromAccount.credit(account);
                toAccount.dedit(account);
            }
        }.start();


        lockTest.transferMoney(fromAccount,toAccount,account);
    }

        public void transferMoney(Account fromAccount,Account toAccount,Account account){


            while(true){
                // lock.tryLock()返回一个布尔值,告诉你当前的锁是否可用,如果可用往下走
            if(fromAccount.lock.tryLock()){
                try {
                if (toAccount.lock.tryLock()){
                    try {
                        //走到这里,证明两个锁都可用,可以进行转账操作.
                        fromAccount.credit(account);
                        toAccount.dedit(account);
                    }finally {
                        toAccount.lock.unlock();
                    }
                }
                }finally {
                    fromAccount.lock.unlock();
                }
            }
        }

        }

}

Account的内部实现:

public class Account {
    public Lock lock = new ReentrantLock();

    public void credit(Account account) {
        lock.lock();
        try {
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }

    }

    public void dedit(Account account) {
        lock.lock();
        try {
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }

    }

定时锁可以在时间预算内设定相应的超时,如果活动子啊期待的时间内没能获得结果,这个机制是程序能够提前返回.

而使用内部锁一旦开始请求,锁就不能停止了,所以内部锁为实现具有时限的活动带来了风险.

.tryLock方法还有一个重载版本,可以设定等待的时间:

lock.tryLock(4, TimeUnit.SECONDS)

1.2 可中断的锁获取操作

lock.lockInterruptibly上的锁,是可以响应中断的:

public class LockSample {
    //创建一个锁的实例
    Lock lock = new ReentrantLock();

    public void testInterruptibly(){
        try {
            lock.lockInterruptibly();

            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }


    public void test(){
        System.out.println("lock.tryLock() = " + lock.tryLock());
        try {
            System.out.println("lock.tryLock(4,TimeUnit.SECONDS) = " + lock.tryLock(4, TimeUnit.SECONDS));;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void methodA(){
        lock.lock();
        try {
            System.out.println("执行了方法A");
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void methodB(){
        lock.lock();
        try {
            System.out.println("执行了方法B");
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }


    public static void main(String [] args){
        Long startTime = System.nanoTime();
        LockSample lockSample = new LockSample();
        Thread thread = Thread.currentThread();
        new Thread(){
            @Override
            public void run(){
                try {
                    //休眠两秒,执行中断
                    Thread.sleep(2000);
                    thread.interrupt();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
        //这里本来是休眠5秒的,因为上面直接中断了,可以看下面的endtime是两秒,证明了可以被中断
        lockSample.testInterruptibly();
        Long endTime = startTime - System.nanoTime();
        System.out.println("endTime = " + endTime);
    }
}

2. 对性能的考量

ReentrantLock提供的竞争上的性能要远远优于内部锁.

对于同步原语而言,竞态时的性能是可伸缩性的关键:若果有越多的资源花费在锁的管理和调度上,那程序执行的时间就越少.

在Java5.0中,ReentrantLock相比于synchronized能给吞吐量带来相当不错的提升,但是在Java6中,这两者非常接近.

也就是说之前选择显示锁,还有性能方面的考量,但是现在显示锁和synchronized已经差不多了.

3. 公平性

ReentrantLock构造函数提供了两种公平性的选择:

  • 创建非公平锁(默认)
  • 公平锁

公平锁:如果锁已经被其他线程占有,新的请求线程会加入到等待队列,或者已经有一些线程在等待锁了;

非公平锁: 非公平锁允许闯入,当请求这样的锁时,如果锁的状态变为可用,线程的请求可以在等待线程的队列中向前跳跃,获得该锁.(Semaphore同样提供了公平和非公平的获取顺序).在非公平的锁中,线程只有当锁正在被占用时才会等待.

为什么要使用不公平锁

当发生加锁的时候,公平会因为挂起和重新开始线程的代价带来巨大的性能开销.

在多数情况下,非公平锁的优势超过了公平的排队.

在竞争激烈的情况下,闯入锁比公平锁性能好的原因之一是:挂起的线程重新开始,与它真正开始运行,两者之间会产生严重的延迟.

比较公平锁和非公平锁,使用的例子:

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

在这样的情况下,各方面都获得了成功:B并没有比其他任何线程晚得到锁,C更早的得到了锁,吞吐量得到了改进.

如果持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么使用公平锁是比较好的.

4. 在synchronized和ReentrantLock之间进行选择

在内部锁不能够满足使用时,ReentrantLock才被作为更高级的工具,当你需要以下高级特性时,才应该使用:

可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁,否则,请使用synchronized.

5. 读-写锁

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

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReadWriteLock暴露了两个Lock对象,一个用来读,另一个用来写.读取ReadWriteLock锁守护的数据,你必须首先获得读取的锁,当需要修改ReadWriteLock守护的数据时,你必须首先获得写入的锁.

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

读-写锁的设计是用来进行性能改进的,使得特定情况下能够有更好的并发性.

多处理器系统中,频繁的访问主要为读取数据结构的时候,读-写锁能够改进性能;

在其他情况下运行的情况比独占的锁要稍差一些,这归因于它更大的复杂性.

ReentrantReadWriteLock也能被构造为非公平(默认)或公平的.

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

非公平: 线程允许访问的顺序是不定的.由写者降级为读者是允许的;从读者升级为写者是不允许的(尝试这样的行为会导致死锁).

使用读写锁的情况

当锁被持有的时间相对较长,并且大部分操作都不会改变锁守护的资源,那么读-写锁能够改进并发性.

使用读-写锁包装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();
        }
    }
    //remove(),putAll(),clear()使用w.lock


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

    //其他的只读map使用r.lock

}

总结

显示的Lock与内部锁相比提供了一些扩展的特性,包括处理不可用的锁时更好的灵活性,以及对队列行为更好的控制,但是ReentrantLock不能完全替代synchronized;只有当你需要synchronized没能提供的特性时才应该使用.

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

posted @ 2018-11-02 08:05  lbr617  阅读(506)  评论(0编辑  收藏  举报