什么是Lock?什么是ReentrantLock?ReentrantReadWriteLock又是啥?
ps:不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
目录
四. 使用Lock对象和Condition实现等待/通知实例
五.使用Lock对象和多个Condition实现等待/通知实例
七、使用ReentrantReadWriteLock实现并发
一.什么是Lock对象?
Lock其实是一个接口,在JDK1.5以后开始提供,其实现类常用的有ReentrantLock,这里所说的Lock对象即是只Lock接口的实现类,简称为Lock对象。
在前面的synchronized这一篇博客中,可以讲了它可以实现线程间的同步互斥,从JDK1.5开始新增的 ReentrantLock类能够达到同样的效果,并且在此基础上还扩展了很多实用的功能,比 使用synchronized更佳的灵活。
ReentrantLock的另一个称呼就是重入锁,下面就看看它怎么实现线程同步。
二.使用ReentrantLock实现线程同步
- 首先,我们先来看一个例子:
public class Run {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
//lambda写法
new Thread(() ‐ > runMethod(lock), "thread1").start();
new Thread(() ‐ > runMethod(lock), "thread2").start(); //常规写法
new Thread(new Runnable() {
@Override
public void run() {
runMethod(lock);
}
}, "thread3").start();
}
private static void runMethod(Lock lock) {
lock.lock();
for (int i = 1; i <= 3; i++) {
System.out.println("ThreadName:" + Thread.currentThread().getName()
+ (" i=" + i));
}
System.out.println();
lock.unlock();
}
}
运行结果:
ThreadName:thread1 i=1
ThreadName:thread1 i=2
ThreadName:thread1 i=3
ThreadName:thread2 i=1
ThreadName:thread2 i=2
ThreadName:thread2 i=3
ThreadName:thread3 i=1
ThreadName:thread3 i=2
ThreadName:thread3 i=3
从代码和运行结果中我们可以发现,这段代码中的三个线程都是分组执行的,只有当前线程执行完之后,其他线程才可以获得锁,然后才可以执行。这也说明了当前线程只有执行完了才会释放它所持有的锁,但线程之间打印的顺序是随机的。
下面再来看一个例子来更好地理解ReentrantLock是怎么实现线程同步的:
public class Run {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(() ‐ > runMethod(lock, 0), "thread1").start();
new Thread(() ‐ > runMethod(lock, 5000), "thread2").start();
new Thread(() ‐ > runMethod(lock, 1000), "thread3").start();
new Thread(() ‐ > runMethod(lock, 5000), "thread4").start();
new Thread(() ‐ > runMethod(lock, 1000), "thread5").start();
}
private static void runMethod(Lock lock, long sleepTime) {
lock.lock();
try {
Thread.sleep(sleepTime);
System.out.println("ThreadName:" +
Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果:
ThreadName:thread1
ThreadName:thread2
ThreadName:thread3
ThreadName:thread4
ThreadName:thread5
由此我们可以看出,在sleep指定的时间内,当调用了lock.lock()方法线程就持有了”对象监视器”,其他线程只能等待锁被释放后再次争抢,效果和使用synchronized关键字是一样的。
三.使用Lock对象实现线程间通信
上面了解了ReentrantLock是怎么实现线程间同步的,下面我们来看一下ReentrantLock是怎么实现线程间通信的。在线程间的通信这篇博客中我们可以看到synchronized与wait()方法和notify()方式结合实现线程间通信,也就是等待/通知模式。在ReentrantLock中,是借助Condition对象进行实现的。
public class LockConditionDemo {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public static void main( String[] args ) throws InterruptedException
{
/* 使用同一个LockConditionDemo对象,使得lock、condition一样 LockConditionDemo demo = new LockConditionDemo(); */
new Thread( () ‐ > demo.await(), "thread1" ).start(); Thread.sleep( 3000 );
new Thread( () ‐ > demo.signal(), "thread2" ).start();
}
private void await()
{
try {
lock.lock();
System.out.println( "开始等待await! ThreadName:" + Thread.currentThread().getName() );
condition.await();
System.out.println( "等待await结束! ThreadName:" + Thread.currentThread().getName() );
} catch ( InterruptedException e ) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void signal()
{
lock.lock();
System.out.println( "发送通知signal! ThreadName:" + Thread.currentThread().getName() );
condition.signal();
lock.unlock();
}
}
Condition的创建方式如下:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
这里可以理解 Condition其实就是在创建条件,可以通过上面的创建的方式实现一个对象创建多个Condition条件,然后根据这些不同的条件实现不同的等待和通知。在使用关键字synchronized与wait()方法和 notify()方式结合实现线程间通信的时候,notify/notifyAll的通知等待的线程时是随机 的,显然使用Condition相对灵活很多,可以实现”选择性通知”。
这是因为,synchronized关键字相当于整个Lock对象只有一个单一的Condition对象, 所有的线程都注册到这个对象上。线程开始notifAll的时候,需要通知所有等待的线程,让他们开始竞争获得锁对象,没有选择权,这种方式相对于Condition条件的方式 在效率上肯定Condition较高一些。
四. 使用Lock对象和Condition实现等待/通知实例
主要方法对比如下:
(1)Object的wait()/wait(long timeout)方法相当于Condition类中的await()/await(long timeout)方法;
(2)Object的notify()方法相当于Condition类中的signal()方法;
(3)Object的notifyAll()方法相当于Condition类中的signalAll()方法;
ReentrantLock结合Condition类可以实现选择性通知。synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上。线程开始notifyAll()时,需要通知所有的wait状态的线程,没有选择权,这样子比较消耗资源。
下面看一个例子,跟之前一样,要先获取锁:
public class LockConditionDemo {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public static void main( String[] args ) throws InterruptedException
{
/* 使用同一个LockConditionDemo对象,使得lock、condition一样
LockConditionDemo demo = new LockConditionDemo(); */
new Thread( () ‐ > demo.await(), "thread1" ).start();
Thread.sleep( 3000 );
new Thread( () ‐ > demo.signal(), "thread2" ).start();
}
private void await()
{
try {
lock.lock();
System.out.println( "开始等待await! ThreadName:" + Thread.currentThread().getName() );
condition.await();
System.out.println( "等待await结束! ThreadName:" + Thread.currentThread().getName() );
} catch ( InterruptedException e ) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void signal()
{
lock.lock();
System.out.println( "发送通知signal! ThreadName:" + Thread.currentThread().getName() );
condition.signal();
lock.unlock();
}
}
运行结果:
开始等待await! ThreadName:thread1
发送通知signal! ThreadName:thread2
等待await结束! ThreadName:thread1
首先,thread1先获得lock锁,然后调用Condition的wait方法,进入等待状态,然后执行thread2,调用Condition的signal方法,唤醒lock锁,然后继续执行thread1。
五.使用Lock对象和多个Condition实现等待/通知实例
实现多个Condition实现等待/通知的代码如下:
public class LockConditionDemo {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
LockConditionDemo demo = new LockConditionDemo();
new Thread(() ‐> demo.await(demo.conditionA),
"thread1_conditionA").start();
new Thread(() ‐> demo.await(demo.conditionB),
"thread2_conditionB").start();
new Thread(() ‐> demo.signal(demo.conditionA),
"thread3_conditionA").start();
System.out.println("稍等5秒再通知其他的线程!");
Thread.sleep(5000);
new Thread(() ‐> demo.signal(demo.conditionB),
"thread4_conditionB").start();
}
private void await(Condition condition) {
try {
lock.lock();
System.out.println("开始等待await! ThreadName:" + Thread.currentThread().getName());
condition.await();
System.out.println("等待await结束! ThreadName:" + Thread.currentThread().getName());
}
catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
private void signal(Condition condition) {
lock.lock();
System.out.println("发送通知signal! ThreadName:" + Thread.currentThread().getName());
condition.signal();
lock.unlock();
}
}
执行结果:
开始等待await! ThreadName:thread1_conditionA
开始等待await! ThreadName:thread2_conditionB
发送通知signal! ThreadName:thread3_conditionA
等待await结束! ThreadName:thread1_conditionA
稍等5秒再通知其他的线程!
发送通知signal! ThreadName:thread4_conditionB
等待await结束! ThreadName:thread2_conditionB
可以看出实现了分别通知。因此,我们可以使用Condition进行分组,可以单独的通知某一个分组,主要保证同一组的线程传的是同一组condition就可以,另外还可以使用signalAll()方法实现通知某一个分组的所有等待的线程。
六、公平锁和非公平锁
顾名思义,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配,即先进先出;非公平是一种抢占机制,是随机获得锁,并不是先来的一定能先得到锁。
ReentrantLock提供了一个构造方法,可以很简单的实现公平锁或非公平锁,源代码构 造函数如下:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
fair为true表示是公平锁,反之为非公平锁。
默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多
七、使用ReentrantReadWriteLock实现并发
上述的类ReentrantLock具有完全互斥排他的效果,即同一时间只能有一个线程在执行 ReentrantLock.lock()之后的任务。
类似于我们集合中有同步类容器 和 并发类容器,HashTable(HashTable几乎可以等 价于HashMap,并且是线程安全的)也是完全排他的,即使是读也只能同步执行,而 ConcurrentHashMap就可以实现同一时刻多个线程之间并发。为了提高效率, ReentrantLock的升级版ReentrantReadWriteLock就可以实现效率的提升。
ReentrantReadWriteLock有两个锁:一个是读操作相关的锁,也称为“共享锁”;另一个是写操作相关的锁,称为“排它锁”。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。即一个是共享的,一个是排斥的,只有有排斥的锁,即为互斥。
在没有线程进行写入操作时,进行读操作的多个线程都可以获取到读锁,而进行写入操作的线 程只有获取写锁后才能进行写入操作。即:多个线程可以同时进行读操作,但是同一 时刻只允许一个线程进行写操作,因为与写相关的锁是排它的。
ReentrantReadWriteLock锁的特性:
(1)读读共享; (2)写写互斥; (3)读写互斥; (4)写读互斥;
八.ReentrantReadWriteLock实例代码
(1)读读共享
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() ‐> demo.read(), "ThreadA").start();
new Thread(() ‐> demo.read(), "ThreadB").start();
}
private void read() {
try {
try {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName()
+ " 时间:" + System.currentTimeMillis());
//模拟读操作时间为5秒
Thread.sleep(5000);
}
finally {
lock.readLock().unlock();
}
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
获得读锁ThreadA 时间:1507720692022
获得读锁ThreadB 时间:1507720692022
可以看出两个线程之间,获取锁的时间几乎同时,说明lock.readLock().lock()允许多 个线程同时执行lock()方法后面的代码。
(2)写写互斥
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() ‐> demo.write(), "ThreadA").start();
new Thread(() ‐> demo.write(), "ThreadB").start();
}
private void write() {
try {
try {
lock.writeLock().lock();
System.out.println("获得写锁" + Thread.currentThread().getName()
+ " 时间:" + System.currentTimeMillis());
//模拟写操作时间为5秒
Thread.sleep(5000);
}
finally {
lock.writeLock().unlock();
}
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
获得写锁ThreadA 时间:1507720931662
获得写锁ThreadB 时间:1507720936662
可以看出执行结果大致差了5秒的时间,可以说明多个写线程是互斥的。
(3)读写互斥或写读互斥
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() ‐> demo.read(), "ThreadA").start();
Thread.sleep(1000);
new Thread(() ‐> demo.write(), "ThreadB").start();
}
private void read() {
try {
try {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName()
+ " 时间:" + System.currentTimeMillis());
Thread.sleep(3000);
}
finally {
lock.readLock().unlock();
}
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
private void write() {
try {
try {
lock.writeLock().lock();
System.out.println("获得写锁" + Thread.currentThread().getName()
+ " 时间:" + System.currentTimeMillis());
Thread.sleep(3000);
}
finally {
lock.writeLock().unlock();
}
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
获得读锁ThreadA 时间:1507721135908
获得写锁ThreadB 时间:1507721138908
可以看出执行结果大致差了3秒的时间,可以说明读写线程是互斥的。
PS:注意lock()与lockInterruptibly()的区别
lockInterruptibly()允许在等待时由其他线程的Thread.interrupt()方法来中断等待线程而直接返回,这时是不用获取锁的,而会抛出一个InterruptException。而ReentrantLock.lock()方法则不允许Thread.interrupt()中断,即使检测到了Thread.interruptted一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功之后在把当前线程置为interrupted状态。
参照:《Java多线程编程的核心技术》