5.4 Lock接口与ReentrantLock

2016-06-26 11:42:09 4,436 0


锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现以前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)来实现锁的功能,它提供了与synchronized关键字类似的同步功能。只是在使用的时候,需要显示地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是确拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

使用synchronized关键字将会隐式的获取锁,但是它将锁的获取和释放固化了,也即是先获取,再释放。当然这种方式简化了锁的管理,可是扩展性没有显示的获取锁和释放来的好。例如,针对一个场景,手把手进行锁的获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不是那么容易实现了,而使用Lock确容易许多。

Lock的使用也很简单。

Lock接口的特性

Lock接口提供的synchronized关键字所不具备的主要特性

特性描述
尝试非阻塞的获取锁当线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断的获取锁
与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁
在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

Lock接口的API

Lock是一个接口,它定义了锁获取和释放的基本操作,其API如下表所示:

方法名称描述
void lock()获取锁,调用该方法当前线程会获取锁,当锁获得后,该方法返回
void lockInterruptibly() throws InterruptedException可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock()尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException

超时获取锁,当前线程在以下三种情况下会被返回:

当前线程在超时时间内获取了锁

当前线程在超时时间内被中断

超时时间结束,返回false

void unlock()释放锁
Condition newCondition()获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁

Lock接口类图

QQ截图20160615213842.png

从图中,可以看到,Lock接口有三个实现类,分别是ReentrantLock(可重入锁)、ReadLock(读锁)、WriteLock(写锁)。Segment继承了ReentrantLock。

在本节中我们以ReentrantLock来讲解Lock接口的使用。

以下代码说明了ReentrantLock的使用方式

Lock接口使用的模板方法

        Lock lock = new ReentrantLock();
        ...
        lock.lock();//获取锁
        try {
            ...
            } finally {
            lock.unlock();//释放锁
        }

在finally中释放锁,目的是在保证获取到锁之后,一定能够被释放。

不要将锁的获取过程写在try块中。因为如果在获取锁(自定义锁的实现)时,发生了异常,异常抛出的同时,也会导致锁的无故释放。

案例代码:Lock接口的独占性演示

两个线程竞争一个锁:

public class ReentrantLockDemo1 {
    public static void main(String[] args) {
        Lock lock=new ReentrantLock();
        new Thread("Thread A"){
            @Override
            public void run() {
                lock.lock();//加锁
                try{
                    work();//work
                    }finally {
                    lock.unlock();//释放锁
                }

            }
        }.start();
        new Thread("Thread B"){
            @Override
            public void run() {
                lock.lock();//加锁
                try{
                    work();//work
                    }finally {
                    lock.unlock();//释放锁
                }

            }
        }.start();
    }

    public static void work(){
        try {
            System.out.println(Thread.currentThread().getName()+" started to work,currrentTime:"+System.currentTimeMillis());
            Thread.currentThread().sleep(1000);
            System.out.println(Thread.currentThread().getName()+" wnd work,currrentTime:"+System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

案例代码输出:

Thread A started to work,currrentTime:1466001530235

Thread A wnd work,currrentTime:1466001531235

Thread B started to work,currrentTime:1466001531235

Thread B wnd work,currrentTime:1466001532235

从输出结果中可以看到,A获取到锁,先执行,而必须等到A执行完成之后,B才能执行。因此Lock对象的实际上是一个独占锁。

案例代码:Lock接口的可重入性演示

public class ReentrantLockDemo2 {
    public static void main(String[] args) {
        ReentrantLock lock=new ReentrantLock();
        System.out.println(lock.getHoldCount());//没有调用lock之前,hold count为0
        lock.lock();//holdCount+1
        System.out.println(lock.getHoldCount());
        lock.lock();//holdCount+1
        System.out.println(lock.getHoldCount());
        lock.unlock();//holdCount-1
        System.out.println(lock.getHoldCount());
        lock.unlock();//holdCount-1
        System.out.println(lock.getHoldCount());
    }
}

注意这里直接定义了ReentrantLock,通过直接使用这个类而不是Lock接口,我们可以使用其独有的方法getHoldCount(),这个方法表示的是当前线程持有锁的次数。上面的输出为:

0

1

2

1

0

通过输出,我们可以看到,一个线程(本案例就是主线程)的确可以多次持有同一个锁。每当调用lock方法时,次数+1,每当调用unlock方法时,次数-1。我们看到出现了2,这事实上就体现了所谓的可重入。需要注意的是:当getHoldCount()不为0的时候,表示锁当前正在被某个线程使用,只有当其为0的时候,其他线程才有机会获取这个锁。


ReentrantLock内部组成

ReentrantLock支持公平锁(FairSync)和非公平锁(NonfairSync)。在上面的案例中,我们使用new ReentrantLock(),事实上使用的就是非公平锁。我们可以在源码中看到:

//默认的构造方法
public ReentrantLock() {
    sync = new NonfairSync();//非公平锁
}

所谓非公平,指的是多个线程同时尝试获取一个锁时,可能会多次被同一个线程获取。实际中公平锁吞吐量比非公平锁小很多,因此我们大多数情况下使用的都是非公平锁。

ReentrantLock内部维护了一个Sync成员对象,其是FairSync和NonfairSync的抽象父类。表面上看锁的功能是由ReentrantLock实现的,实际是由其内部的私有变量Sync来完成的,根据是否需要是公平锁,给Sync提供不同的具体实现。

Sync类图:

QQ截图20160615221543.png


我们可以看到FairSync和NonfairSync最终都继承了AbstractQueuedSynchronizer,事实上,自Java 5之后众多同步组件都继承了这个类。其是基于模板方法设计的,把很多公共的操作抽取出来,并实现,而某些细节不能确定如何实现时,则定义为抽象方法,由子类实现。例如我们这里,Sync类就直接之继承了AbstractQueuedSynchronizer,并实现了部分抽象方法,并把更具体的实现交给子类FairSync和NonfairSync实现。