读书笔记:Java并发实战 第13章 显式锁

来源:互联网 发布:linux查看ssh连接数 编辑:程序博客网 时间:2024/06/06 05:22

1、一般用法


<pre name="code" class="java">Lock lock = new ReentrantLock();public void method(){lock.lock();try{//...}finally{lock.unlock();}}


特点:


    1、加锁和解锁的方法都是显式的

    2、在调用lock()方法时,和进入同步代码块有相同的内存语义

    3、在调用unlock()方法时,和退出同步代码块有相同的内存语义

    4、独占和可重入


2、轮询锁


    先看由于动态的参数顺序造成死锁的例子:


    资源类:MyLock


public class MyLock {public Lock lock = new ReentrantLock();public String name;public MyLock(String name){this.name = name;}}

    任务类:Executor


public class Executor {public void work(MyLock a,MyLock b){String threadName = Thread.currentThread().getName();a.lock.lock();System.out.println(threadName+"获得"+a.name+"的锁");try{TimeUnit.SECONDS.sleep(1);System.out.println(threadName+"请求"+b.name+"的锁");b.lock.lock();}catch(Exception e){//...}finally{b.lock.unlock();a.lock.unlock();}}}

测试类:

MyLock x = new MyLock("锁X");MyLock y = new MyLock("锁y");Executor exec = new Executor();ThreadA a = new ThreadA("线程A",x,y,exec);ThreadB b = new ThreadB("线程B",x,y,exec);a.start();b.start();

    这段程序运行的结果就会造成死锁,下面,将Executor的work()方法改用轮询锁来解决死锁问题


    轮询锁是通过tryLock()方法来实现的,这个方法尝试获取一个锁,如果获取到,返回true,否则返回false,这种立即返回的方式,避免了无效的等待。当嵌套调用锁时,如果里层的tryLock()方法返回false,就释放外层的锁,以便让其他线程获得所有的锁


    使用轮询锁需要注意的一个重点是:各线程不要使用相同时间间隔来轮询锁,否则很容易发生活锁,如下面的例子


while(true){String threadName = Thread.currentThread().getName();if(a.lock.tryLock()){System.out.println(threadName+"获得"+a.name+"的锁");try{TimeUnit.SECONDS.sleep(1);System.out.println(threadName+"请求"+b.name+"的锁");if(b.lock.tryLock()){try{System.out.println(threadName+"获得"+b.name+"的锁");return;}catch(Exception e){//...}finally{b.lock.unlock();}}}catch(Exception e){//...}finally{a.lock.unlock();}}}

    发生活锁的原因是:


    线程A得到锁X请求锁Y,线程B得到锁Y请求锁X,此时两个线程都不得到内层锁而放弃外层锁,然后又立即做相同的步骤,这样,活锁就发生了


    我们只需要在每次释放外层锁之后,轮询之前等待一个随机时间,就能很大程度避免发生活锁


3、定时锁


    在使用tryLock()方法时,可以加入参数,指定超时时间,在这个时间内如果等待不到锁,则会返回false


4、可中断锁


    在等待获取内置锁的情况下是不能响应中断的,这让实现可取消的任务变得复杂。


    Lock实现了可中断锁,能够响应中断:lock.lockInterruptibli();——获取一个可响应中断的锁


5、公平锁和非公平锁


    ReentrantLock的构造函数中提供了公平锁(默认)和非公平锁的选择


    公平锁:所有请求锁的线程在队列中等待锁,线程获得锁的顺序按先来先得的原则


    非公平锁:当线程请求的锁不能得到时,就在队列中等待,如果线程请求锁时,该锁刚好被释放,那么该线程可以得到这个锁


    一般情况下,非公平锁的性能要高于公平锁。因为公平性会造成较多的线程挂起和恢复操作,在恢复一个被挂起的线程与该线程真正运行之间存在严重的延迟,比如:在高并发的情况下,当线程A是否一个锁时,因等待锁而被挂起的线程B会被唤醒,在B被完全唤醒之前,其他线程可能已经完成了"获得-释放"该锁的动作,而并没有影响B获得锁的时刻,如果采用公平锁,其他线程无法请求这个锁,吞吐率就降低了

    因此:如果线程持有锁的时间很短,应该尽量使用非公平锁


6、读写锁


    互斥锁是最保守的加锁策略,在很多情况下并不需要如此严格的加锁方式,并发的读操作如果使用互斥锁是不必要的,而且会降低读的性能,我们只要保证不会读取到脏数据即可,即:读取数据时不允许写,但允许读。读写锁可以实现这个功能,看下面的例子:


private ReadWriteLock lock = new ReentrantReadWriteLock();private Lock readLock = lock.readLock();private Lock writeLock = lock.writeLock();public void read(){readLock.lock();try {//...} catch (Exception e) {}finally{readLock.unlock();}}public void write(){writeLock.lock();try {//...} catch (Exception e) {}finally{writeLock.unlock();}}

线程A持有读锁,线程B请求读锁,线程B可以立即获得读锁

线程A持有读锁,线程B请求写锁:线程B需要在线程A释放读锁后才能获得写锁

线程A持有写锁,线程B请求读锁:线程B需要在线程A释放写锁后才能获得读锁

线程A持有写锁,线程B请求写锁:线程B需要在线程A释放写锁后才能获得写锁


读、写锁之间可以定义不同的交互方式,包括:


1、当释放写锁时,读线程和写线程哪个优先得到锁

2、当释放读锁时,并且队列中有写线程正在等待,此时是否允许读线程插队

3、读、写锁是否允许重入

4、锁降级:线程将持有的写锁在不释放锁的情况下转变为读锁

5、锁升级:线程将持有的读锁在不释放锁的情况下转变为写锁


    ReentrantReadWriteLock的公平锁情况下:如果一个线程持有读锁,当等待线程中有请求写锁时,优先于等待的读锁;锁降级是允许的,但锁升级不允许,这样会导致死锁,因为写锁是互斥的,获得写锁要等待所有其他线程释放读写锁,如果两个线程同时升级锁,那么会同时在等待对方释放锁


读写锁的性能


    在读操作较多的情况下,读写锁能提高性能,其他情况下要比独占锁的性能差,因为它的实现比较复杂


    总结:


    Lock提供一种灵活的加锁机制,在很多场合,是不适合使用同步代码块的,同步代码块的缺点和优点集于一身:锁的获取和释放是在同一个代码块中的,好处是我们不必考虑代码块中遇到异常退出时需要如何释放锁,缺点是无法实现连锁式加锁


    连锁式加锁是指:在锁分段的应用场景下,比如ConcurrentHashMap,它把散列桶分成不同的段,每个段使用一个独立的锁,当我们需要遍历时,需要持有某个段的锁,直到获得下一个段的锁才释放原有的锁,这种情况下,内置锁是无法完成的


    关于synchronized和ReentrantLock的选择


    在Java 5.0时代,ReentrantLock的性能比内置锁有更好的竞争性能,而Java 6之后改进了内置锁,二者性能相差无几


    由于ReentrantLock存在更高的活跃性危险,因此一般情况下优先使用synchronized,只有需要使用ReentrantLock的特殊功能是才考虑ReentrantLock


    并且,在后面的Java版本中可能会提升synchronized而不是ReentrantLock的性能,因为synchronized是JVM的内置属性,可以执行一些优化

0 0