显式锁
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
MyThread.java
Run.java
运行结果
使用 ReentrantLock 实现同步:测试2
ConditionTestMoreMethod.java
第一组线程类
第二组线程类
Run.java
运行结果
分析
实验说明,调用lock.lock()的代码相当于持有了“对象监视器”,其他线程只有等待锁被释放时再次争抢,效果和使用 synchronized 关键字一样。
条件对象
下面是 Condition 对象常用的API
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
ThreadA.java
Run.java
运行结果
分析
因为无监视器对象,所以程序抛出了异常。解决方法是在 condition.await() 方法调用前调用 lock.lock() 获得同步监视器。
修改MyService.java
MyThreadA.java 和 Run.java
运行结果
正确使用 Condition 实现等待 / 通知
MyService.java
ThreadA.java
Run.java
运行结果
使用多个 Condition 实现通知部分线程:错误用法
MyService.java
ThreadA.java 和 ThreadB.java
Run.java
运行结果
我们发现A、B线程都被唤醒了,如何实现唤醒指定的一个线程呢?这时候就需要使用多个 Condition 对象了,可以先对线程进行分组,再唤醒指定组内的线程。
使用多个 Condition 实现通知部分线程:正确用法
MyService.java
ThreadA.java 和 ThreadB.java
Run.java
运行结果
只有A被唤醒了。我们可以发现,同一个 Condition 对象只能唤醒它 await() 的线程,可以把同一个 Condition 对象 await() 的线程分为一组,该 Condition 对象的 signal() 是在该组等待线程中随机选择一个唤醒,而 signalAll() 是唤醒该组中所有的等待线程。
实现生产者 / 消费者模式:一对一交替打印
MyService.java
ThreadA.java 和 ThreadB.java
Run.java
实现生产者 / 消费者模式:多对多交替打印
为了防止假死,把 signal() 改为 signalAll() 即可。
公平锁与非公平锁
Service.java
RunFair.java
运行结果
打印结果基本呈有序状态,这就是公平锁的特点。
再来看看非公平锁,只要修改传入的 true 参数为 false 即可。
运行结果
非公平锁的运行结果基本上是乱序的,说明先启动的线程不一定先获得锁。
方法 getHoldCount() 、getQueueLength()、getWaitQueueLength() 的测试
1)int getHoldCount() 方法的作用是查询当前线程保持此锁定的个数,也就是调用 lock() 方法的次数。
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计加1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个ReentrantLock锁住的代码块时,计数器会递减,直到计数器为0才释放该锁。getHoldCount() 方法返回的就是这个计数器的值。
Service.java
Run.java
运行结果
2)方法 int getQueueLength() 的作用是返回正等待获取此锁定线程的估计数
比如有 5 个线程,1 个线程首先执行 await() 方法,那么在调用 getQueueLength() 方法后返回值是 4,说明有 4 个线程同时在等待 lock 的释放。
Service.java
Run.java
运行结果
3)方法 int getWaitQueueLength(Condition condition) 的作用是返回同一个 Condition 对象的等待线程数。
如果同时开启了5个线程,都调用了await()方法,并且它们是用的同一个Condition 对象,那么在调用该方法返回值为5。
Service.java
Run.java
方法 hasQueuedThread()、hasQueuedThreads() 和 hasWaiters() 的测试
1)方法 boolean hasQueuedThread(Thread thread) 的作用是查询指定的线程是否正在等待获取此锁定。方法 boolean hasQueuedThreads() 的作用时查询是否有线程正在等待获取此锁定。
Service.java
Run.java
运行结果
2)方法 boolean hasWaiters(Condition condition) 的作用是查询是否有线程正在等待与此锁定有关的 condition 条件。
Service.java
Run.java
运行结果
方法 isFair()、isHeldByCurrentThread() 和 isLocked() 的测试
1)方法 isFair() 的作用是判断是不是公平锁
lock.isFair() 可以判断 lock 是否是公平锁,默认情况下,ReentrantLock 类使用的是非公平锁。
2)方法 boolean isHeldByCurrentThread() 的作用是查询当前线程是否保持此锁定。
Service.java
Run.java
运行结果
3)方法 boolean isLocked() 的作用是查询此锁定是否由任意线程保持。
Service.java
Run.java
方法 lockInterruptibly()、tryLock() 和 tryLock(long timeout, TimeUnit unit) 的测试
1)方法 void lockInterruptibly() 的作用是:如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。
MyService.java
Run.java
运行结果
没有出现异常,说明即使线程被 interrupt() 中断了,执行 lock() 也不出现异常。
把 MyService.java 中的 lock.lock() 修改为 lock.lockInterruptibly()
运行结果
线程被中断后调用 lockInterruptibly() 会抛出 InterruptedException。
2)方法 boolean tryLock() 的作用是,仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。
MyService.java
Run.java
运行结果
3)方法 boolean tryLock(long timeout, TimeUnit unit) 的作用是,如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。
MyService.java
Run.java
运行时间
awaitUninterruptibly() 的使用
awaitUninterruptibly() 和 await() 方法用法基本相同,只是它并不响应中断请求,即在调用 awaitUninterruptibly() 时调用 interrupt() 不会抛出 InterruptedException。
MyService.java
MyThread.java
Run.java
运行结果
修改 Service.java 中的 await() 为 awaitUninterruptibly(), 运行结果如下
awaitUntil() 的使用
awaitUntil() 设定一个超时间隔,如果在规定时间内没有被通知或中断,线程将被唤醒。
ThreadA.java 和 ThreadB.java
Service.java
Run1.java
运行结果
线程等待 10 秒后自动唤醒自己。
Run2.java
使用 Condition 实现顺序执行
使用 Condition 对象可以对线程执行的业务进行排序规划。指定唤醒特定的线程,比 Object 的 notify() 更具精准可控性。
Run.java
运行结果
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 类
类 ReentrantReadWriteLock 的使用:读读共享
Service.java
Run.java
ThreadA.java 和 ThreadB.java
运行结果
我们发现两个线程同时获得读锁,说明读操作是共享的。
类 ReentrantReadWriteLock 的使用:写写互斥
Service.java
运行结果
我们发现第二个线程获得写锁的时间比第一个线程晚了 10 秒,说明写操作是互斥的。
类 ReentrantReadWriteLock 的使用:读写互斥
Service.java
Run.java
运行结果
实验说明读写操作是互斥的。
类 ReentrantReadWriteLock 的使用:写读互斥
同理写读操作也是互斥的。
Run.java
运行结果
总结:“读写”、“写读”、“写写”都是同步的、互斥的;"读读"是异步的、共享的。