ReentrantLock和ReentrantReadWriteLock类的使用

在Java中,可以使用synchronized关键字实现线程之间同步互斥,除此以外,JDK中的Lock对象也能实现同步的效果,而且在使用上更加方便灵活,扩展功能也更加强大。常用的两个Lock类为ReentrantLock和ReentrantReadWriteLock。

ReentrantLock类在功能上相比synchronized关键字更多。

下面先介绍一下ReentrantLock类的使用。

1.ReentrantLock类的使用

(1)调用ReentrantLock对象的lock( )方法获取锁,调用unlock( )方法释放锁。

在需要进行同步调用的代码前面添加lock( )方法加上锁,在同步调用的代码块的最后面加unlock( )方法释放锁,效果类似于用synchronized关键字在同步方法和代码块上加锁。共享同一个对象锁的线程,它们之间还是顺序执行的。

线程会在执行lock( )方法之后获取锁,执行unlock( )方法之后释放锁。即调用ReentrantLock.lock( )代码的线程就持有了“对象监视器”,其他线程只有等待锁被释放时再次争抢。

在有try/catch/finall语句块中的方法中,lock( )方法常放在try语句块中,unlock( )方法经常放在finally语句块中。

(2)使用Condition实现等待/通知

关键字synchronized与wait( )和notify( )/notityAll( )方法相结合可以实现等待/通知模式,ReentrantLock类也可以实现相同的功能,但需要借助Condition对象。使用Condition类可以实现更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock里面可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

在使用notify( )/notityAll( )方法进行通知时,通知的线程是由JVM的线程调度程序随机选择的。但使用ReentrantLock结合Condition类可以实现“选择性通知”,这个功能是非常重要的,在Condition类中是默认提供的。

synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上。线程执行notify( )方法时,只能随机唤醒一个等待的线程,不具有灵活性;线程开始notigyAll( )方法时,需要通知所有的WAITING线程,没有选择权,会出现相当大的效率问题。

需要注意的是,在调用Condition类实现线程等待时必须在Condition.await( )方法调用之前调用Lock.lock( )代码获得同步监视器,否则就会出现监视器错误。就像使用wait()和notify( )方法时这两个方法必须用在被synchronized修饰的同步方法或同步代码块中,实现获取对象监视器,然后再使用。

如果在当前执行任务的线程调用了Condition对象的await( )方法,那么该线程就会进入等待WAITING状态。

Object类中的wait( )方法相当于Condition类中的await( )方法。

Object类中的wait(long timeout)方法相当于Condition类中的await(long time,TimeUnit unit)方法。

Object类中的notify( )方法相当于Condition类中的signal( )方法。

Object类中的notifyAll( )方法相当于Condition类中的signalAll( )方法。

(3)使用多个Condition实现等待部分线程

前面使用一个Condition对象实现等待/通知模式,Condition对象也可以创建多个。多个Condition可以实现通知部分线程。

如果想单独唤醒部分线程,需要使用Condition对象。Condition对象可以唤醒部分指定的线程,有助于提升程序运行的效率。可以先对线程进行分组,然后唤醒指定组中的线程。

使用Condition对象可以唤醒指定种类的线程,这是控制部分线程行为的方便方式。

(4)用ReentrantLock和Condition实现生产者/消费者模式

关键字synchronized与wait( )和notify( )/notityAll( )方法相结合可以实现生产者/消费者模式,ReentrantLock类结合Condition对象也可以实现同样的效果。和wait( )和notify( )/notityAll( )方法方法一样,如果是一个生产者和一个消费者能够很好的运行,但是如果是使用ReentrantLock和Condition对象实现多生产者/多消费者,也有可能出现程序“假死”的情况,这时的解决方法也是把释放等待线程的signal( )方法变成释放所有对象的signalAll( )方法,调用signalAll( )方法唤醒的线程还可以根据Condition指定为特定类型的,就不会出现“假死”的情况了,还会更加灵活。

(5)公平锁与非公平锁

锁Lock分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先到先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是根据线程调度程序的调度随机获得锁。先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。

这里需要注意的是:公平锁只能保证线程执行的基本有序,不能保证线程执行的顺序完全按照书写的顺序执行。

非公平锁的执行基本上是乱序的,先start( )启动的线程不代表先获得锁。

在默认的情况下,ReentrantLock类使用的是非公平锁。

下面先介绍ReentrantReadWriteLock类的使用。

2.ReentrantReadWriteLock类的使用

ReentrantLock类具有完全互斥排他的效果,即同一时间只能有一个线程在执行ReentrantLock.lock( )方法后面的任务。这样做虽然保证了实例变量的线程安全性,但效率确实非常低下的。所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁ReentrantReadWriteLock类来提升该方法的代码运行速度。

读写锁表示有两个锁:一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。在这两个锁中规定:多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。在没有现成Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时间只允许一个Thread进行写入操作。

ReentrantLock类的读写使用规则简化来说就是:读读共享,写写互斥,读写互斥,写读互斥。

3.总结

在学习并发时,Lock是synchronized关键字的进阶,掌握Lock有助于学习并发包中源代码的实现原理,在并发包中大量的类使用Lock接口作为同步的处理方式,

参考《Java多线程编程核心技术》---高洪岩(Chapter4 4.1 4.2)

猜你喜欢

转载自blog.csdn.net/kongmin_123/article/details/81431173