Java多线程学习之 Lock 的使用

显式锁

Java 为同步提供了两种锁,一种是语言特性提供的内置锁,即 synchronized 关键字,详见 Java多线程学习之对象及变量的并发访问 ;还有一种是 JDK 提供的显式锁。本文我们来介绍显式锁:

使用 ReentrantLock 类

java.util.concurrent.locks(J.U.C)包中提供了可重入的显式锁(ReentrantLock),需要显式进行 lock 以及 unlock 操作。ReentrantLock 和 synchronized 一样都能实现同步,且都是可重入的。但 ReentrantLock 在扩展功能上更为强大,使用上更加灵活。

用 ReentrantLock 保护代码块的基本结构如下 :

lock.lock() ; // lock是一个ReentrantLock对象,lock方法上锁
try
{
  //被同步的临界区
}
finally
{
lock.unlock(); //unlock方法解锁,放在finally子句中,即使抛出异常也要解锁
}

这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过 lock 语句。当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放锁对象。

注意:
1.把解锁操作置于 finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
2.如果使用锁,就不能使用带资源的 try 语句。首先,解锁方法名不是 close。不过,即使将它重命名,带资源的 try 语句也无法正常工作。它的首部希望声明一个新变量。但是如果使用一个锁,你可能想使用多个线程共享的那个变量 (而不是新变量)。

使用 ReentrantLock 实现同步:测试1

MyService.java


Java多线程学习之 Lock 的使用_第1张图片

MyThread.java
Java多线程学习之 Lock 的使用_第2张图片

Run.java
Java多线程学习之 Lock 的使用_第3张图片

运行结果
Java多线程学习之 Lock 的使用_第4张图片

使用 ReentrantLock 实现同步:测试2

ConditionTestMoreMethod.java

Java多线程学习之 Lock 的使用_第5张图片

第一组线程类

Java多线程学习之 Lock 的使用_第6张图片

第二组线程类

Java多线程学习之 Lock 的使用_第7张图片

Run.java

Java多线程学习之 Lock 的使用_第8张图片

Java多线程学习之 Lock 的使用_第9张图片

运行结果

Java多线程学习之 Lock 的使用_第10张图片

分析
实验说明,调用lock.lock()的代码相当于持有了“对象监视器”,其他线程只有等待锁被释放时再次争抢,效果和使用 synchronized 关键字一样。

条件对象

Java多线程学习之 Lock 的使用_第11张图片

下面是 Condition 对象常用的API
Java多线程学习之 Lock 的使用_第12张图片

Condition 对象的await(),await(long time, TimeUnit unit), signal(),signalAll()方法的功能分别对应Object类的wait(),wait(long timeout), notify(),notifyAll()方法。

和Object类的wait(),wait(long timeout), notify(),notifyAll()方法必须在同步方法或同步代码块中使用类似,Condition 对象的await(),await(long time, TimeUnit unit), signal(),signalAll()方法也必须在lock.lock()和lock.unlock()构成的临界区内使用,否则会抛出IllegalMonitorStateException!

await() 会使当前线程释放锁,如果一个线程在 await 时调用 interrupt() 会抛出 InterruptedException。

使用 Condition 实现等待 / 通知:错误用法与解决

MyService.java


Java多线程学习之 Lock 的使用_第13张图片

ThreadA.java
Java多线程学习之 Lock 的使用_第14张图片

Run.java
Java多线程学习之 Lock 的使用_第15张图片

运行结果
Java多线程学习之 Lock 的使用_第16张图片

分析
因为无监视器对象,所以程序抛出了异常。解决方法是在 condition.await() 方法调用前调用 lock.lock() 获得同步监视器。
修改MyService.java
Java多线程学习之 Lock 的使用_第17张图片

MyThreadA.java 和 Run.java
Java多线程学习之 Lock 的使用_第18张图片

运行结果

正确使用 Condition 实现等待 / 通知

MyService.java

Java多线程学习之 Lock 的使用_第19张图片

ThreadA.java
Java多线程学习之 Lock 的使用_第20张图片

Run.java
Java多线程学习之 Lock 的使用_第21张图片

运行结果

使用多个 Condition 实现通知部分线程:错误用法

MyService.java

Java多线程学习之 Lock 的使用_第22张图片

Java多线程学习之 Lock 的使用_第23张图片

ThreadA.java 和 ThreadB.java

Java多线程学习之 Lock 的使用_第24张图片

Run.java

Java多线程学习之 Lock 的使用_第25张图片

运行结果

Java多线程学习之 Lock 的使用_第26张图片

我们发现A、B线程都被唤醒了,如何实现唤醒指定的一个线程呢?这时候就需要使用多个 Condition 对象了,可以先对线程进行分组,再唤醒指定组内的线程。

使用多个 Condition 实现通知部分线程:正确用法

MyService.java

Java多线程学习之 Lock 的使用_第27张图片

Java多线程学习之 Lock 的使用_第28张图片

Java多线程学习之 Lock 的使用_第29张图片

ThreadA.java 和 ThreadB.java

Java多线程学习之 Lock 的使用_第30张图片

Run.java

Java多线程学习之 Lock 的使用_第31张图片

运行结果

Java多线程学习之 Lock 的使用_第32张图片

只有A被唤醒了。我们可以发现,同一个 Condition 对象只能唤醒它 await() 的线程,可以把同一个 Condition 对象 await() 的线程分为一组,该 Condition 对象的 signal() 是在该组等待线程中随机选择一个唤醒,而 signalAll() 是唤醒该组中所有的等待线程。

实现生产者 / 消费者模式:一对一交替打印

MyService.java

Java多线程学习之 Lock 的使用_第33张图片

Java多线程学习之 Lock 的使用_第34张图片

ThreadA.java 和 ThreadB.java
Java多线程学习之 Lock 的使用_第35张图片

Run.java
Java多线程学习之 Lock 的使用_第36张图片

实现生产者 / 消费者模式:多对多交替打印

为了防止假死,把 signal() 改为 signalAll() 即可。

公平锁与非公平锁


Service.java
Java多线程学习之 Lock 的使用_第37张图片

Java多线程学习之 Lock 的使用_第38张图片

RunFair.java

Java多线程学习之 Lock 的使用_第39张图片

Java多线程学习之 Lock 的使用_第40张图片

运行结果

Java多线程学习之 Lock 的使用_第41张图片

打印结果基本呈有序状态,这就是公平锁的特点。

再来看看非公平锁,只要修改传入的 true 参数为 false 即可。

运行结果

Java多线程学习之 Lock 的使用_第42张图片

非公平锁的运行结果基本上是乱序的,说明先启动的线程不一定先获得锁。

方法 getHoldCount() 、getQueueLength()、getWaitQueueLength() 的测试

1)int getHoldCount() 方法的作用是查询当前线程保持此锁定的个数,也就是调用 lock() 方法的次数。

每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计加1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个ReentrantLock锁住的代码块时,计数器会递减,直到计数器为0才释放该锁。getHoldCount() 方法返回的就是这个计数器的值。
Service.java

Java多线程学习之 Lock 的使用_第43张图片

Run.java
Java多线程学习之 Lock 的使用_第44张图片

运行结果
Java多线程学习之 Lock 的使用_第45张图片

2)方法 int getQueueLength() 的作用是返回正等待获取此锁定线程的估计数

比如有 5 个线程,1 个线程首先执行 await() 方法,那么在调用 getQueueLength() 方法后返回值是 4,说明有 4 个线程同时在等待 lock 的释放。
Service.java

Java多线程学习之 Lock 的使用_第46张图片

Run.java
Java多线程学习之 Lock 的使用_第47张图片

运行结果
Java多线程学习之 Lock 的使用_第48张图片

3)方法 int getWaitQueueLength(Condition condition) 的作用是返回同一个 Condition 对象的等待线程数。

如果同时开启了5个线程,都调用了await()方法,并且它们是用的同一个Condition 对象,那么在调用该方法返回值为5。
Service.java

Java多线程学习之 Lock 的使用_第49张图片

Run.java
Java多线程学习之 Lock 的使用_第50张图片

方法 hasQueuedThread()、hasQueuedThreads() 和 hasWaiters() 的测试

1)方法 boolean hasQueuedThread(Thread thread) 的作用是查询指定的线程是否正在等待获取此锁定。方法 boolean hasQueuedThreads() 的作用时查询是否有线程正在等待获取此锁定。

Service.java

Java多线程学习之 Lock 的使用_第51张图片

Run.java
Java多线程学习之 Lock 的使用_第52张图片

运行结果
Java多线程学习之 Lock 的使用_第53张图片

2)方法 boolean hasWaiters(Condition condition) 的作用是查询是否有线程正在等待与此锁定有关的 condition 条件。

Service.java

Java多线程学习之 Lock 的使用_第54张图片

Run.java
Java多线程学习之 Lock 的使用_第55张图片

Java多线程学习之 Lock 的使用_第56张图片

运行结果

方法 isFair()、isHeldByCurrentThread() 和 isLocked() 的测试

1)方法 isFair() 的作用是判断是不是公平锁

lock.isFair() 可以判断 lock 是否是公平锁,默认情况下,ReentrantLock 类使用的是非公平锁。

2)方法 boolean isHeldByCurrentThread() 的作用是查询当前线程是否保持此锁定。

Service.java

Java多线程学习之 Lock 的使用_第57张图片

Run.java
Java多线程学习之 Lock 的使用_第58张图片


运行结果
Java多线程学习之 Lock 的使用_第59张图片

3)方法 boolean isLocked() 的作用是查询此锁定是否由任意线程保持。

Service.java

Java多线程学习之 Lock 的使用_第60张图片

Run.java
Java多线程学习之 Lock 的使用_第61张图片

方法 lockInterruptibly()、tryLock() 和 tryLock(long timeout, TimeUnit unit) 的测试

1)方法 void lockInterruptibly() 的作用是:如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。

MyService.java

Java多线程学习之 Lock 的使用_第62张图片

Run.java
Java多线程学习之 Lock 的使用_第63张图片

运行结果
Java多线程学习之 Lock 的使用_第64张图片

没有出现异常,说明即使线程被 interrupt() 中断了,执行 lock() 也不出现异常。

把 MyService.java 中的 lock.lock() 修改为 lock.lockInterruptibly()

运行结果

Java多线程学习之 Lock 的使用_第65张图片

线程被中断后调用 lockInterruptibly() 会抛出 InterruptedException。

2)方法 boolean tryLock() 的作用是,仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。

MyService.java

Java多线程学习之 Lock 的使用_第66张图片

Run.java

Java多线程学习之 Lock 的使用_第67张图片

Java多线程学习之 Lock 的使用_第68张图片

运行结果

3)方法 boolean tryLock(long timeout, TimeUnit unit) 的作用是,如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。

MyService.java

Java多线程学习之 Lock 的使用_第69张图片

Run.java
Java多线程学习之 Lock 的使用_第70张图片

运行时间
Java多线程学习之 Lock 的使用_第71张图片

awaitUninterruptibly() 的使用

awaitUninterruptibly() 和 await() 方法用法基本相同,只是它并不响应中断请求,即在调用 awaitUninterruptibly() 时调用 interrupt() 不会抛出 InterruptedException。
MyService.java

Java多线程学习之 Lock 的使用_第72张图片


MyThread.java
Java多线程学习之 Lock 的使用_第73张图片

Run.java
Java多线程学习之 Lock 的使用_第74张图片

运行结果
Java多线程学习之 Lock 的使用_第75张图片

修改 Service.java 中的 await() 为 awaitUninterruptibly(), 运行结果如下
Java多线程学习之 Lock 的使用_第76张图片

awaitUntil() 的使用

awaitUntil() 设定一个超时间隔,如果在规定时间内没有被通知或中断,线程将被唤醒。
ThreadA.java 和 ThreadB.java

Java多线程学习之 Lock 的使用_第77张图片

Service.java
Java多线程学习之 Lock 的使用_第78张图片

Java多线程学习之 Lock 的使用_第79张图片

Run1.java
Java多线程学习之 Lock 的使用_第80张图片

运行结果
Java多线程学习之 Lock 的使用_第81张图片

线程等待 10 秒后自动唤醒自己。

Run2.java

Java多线程学习之 Lock 的使用_第82张图片

Java多线程学习之 Lock 的使用_第83张图片

使用 Condition 实现顺序执行

使用 Condition 对象可以对线程执行的业务进行排序规划。指定唤醒特定的线程,比 Object 的 notify() 更具精准可控性。
Run.java

Java多线程学习之 Lock 的使用_第84张图片

Java多线程学习之 Lock 的使用_第85张图片

Java多线程学习之 Lock 的使用_第86张图片

运行结果
Java多线程学习之 Lock 的使用_第87张图片

ReentrantLock与synchronized比较

1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。

4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。从而可以灵活地对线程进行分组阻塞和唤醒。

使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。而如果使用 ReentrantLock 必须把释放锁的代码放在finally子句中,使线程异常终止时也能释放锁。

使用 ReentrantReadWriteLock 类

Java多线程学习之 Lock 的使用_第88张图片

类 ReentrantReadWriteLock 的使用:读读共享

Service.java

Java多线程学习之 Lock 的使用_第89张图片

Run.java

Java多线程学习之 Lock 的使用_第90张图片

ThreadA.java 和 ThreadB.java
Java多线程学习之 Lock 的使用_第91张图片

运行结果
Java多线程学习之 Lock 的使用_第92张图片

我们发现两个线程同时获得读锁,说明读操作是共享的。

类 ReentrantReadWriteLock 的使用:写写互斥

Service.java


Java多线程学习之 Lock 的使用_第93张图片

运行结果

我们发现第二个线程获得写锁的时间比第一个线程晚了 10 秒,说明写操作是互斥的。

类 ReentrantReadWriteLock 的使用:读写互斥

Service.java

Java多线程学习之 Lock 的使用_第94张图片

Java多线程学习之 Lock 的使用_第95张图片

Run.java
Java多线程学习之 Lock 的使用_第96张图片

运行结果

实验说明读写操作是互斥的。

类 ReentrantReadWriteLock 的使用:写读互斥

同理写读操作也是互斥的。
Run.java

Java多线程学习之 Lock 的使用_第97张图片

运行结果

总结:“读写”、“写读”、“写写”都是同步的、互斥的;"读读"是异步的、共享的。

你可能感兴趣的:(Java多线程学习之 Lock 的使用)