Java多线程-锁接口和类
Java原生的锁是基于对象的锁,一般是配合synchronized使用的。而在java.util.concurrent包下,还提供了几个关于锁的接口和类。
synchronized的不足
- 使用
synchronized,临界区的代码同一时间只有一个线程能执行。即便临界区是只读操作。 synchronized无法知道线程有没有成功获得锁。- 使用
synchronized如果临界区因为IO或者sleep等阻塞了,当前线程又没有释放锁,就会导致所有的线程都会等待。
锁的分类
锁可以根据不同的方式进行分类。
可重入锁和不可重入锁
可重入锁就是支持重新进入的锁,也就是支持一个线程对资源重复加锁。
synchronized就是使用的可重入锁。可重入性实际上是基于线程的分配,而不是基于方法调用的分配。
Lock也是一个可重入锁。
公平锁与非公平锁
这里的公平指的是先来先执行。如果一个锁对先请求获取的线程先满足,对后请求获取的线程后满足,这个锁就是一个公平锁。
一般,非公平锁能提升效率,但非公平锁可能会导致一些线程长时间获取不到锁。
ReentrantLock支持公平锁与非公平锁两种。
读写锁和排它锁
synchronized和ReetrantLock都是排它锁,这些锁在同一时间内只允许一个线程进行访问。
而读写锁同一时刻允许多个线程访问。Java提供了ReetrantReadAndWriterLock类作为读写锁的默认实现,其内部维护了两个锁:一个读锁、一个写锁。通过分离读锁和写锁,使得读多写少的场景下效率大大提高。使用读写锁时,在写线程启动时,读线程和其他的写线程均被阻塞。
JDK中关于锁的接口和类
抽象类AQS/AQLS/AOS
AQS,抽象队列同步器,是JDK提供的一个“队列同步器”的基本功能实现。
AQS中的资源使用一个int类型的数据表示的,如果我们的资源数量超出了int的范围,就可以使用AQLS。AQLS与AQS几乎一样,只是把资源的类型变成了long类型。
AQS和AQLS都继承了一个类AOS:AbstractOwnableSynchronizer,用于表示锁与持有者的关系(独占模式)。
1 | public abstract class AbstractOwnableSynchronizer implements Serializable { |
接口Condition/Lock/ReadWriteLock
java.util.concurrent包下有三个接口:Condition、Lock、ReaderWriteLock。其中,Lock和ReadWriteLock分别是锁和读写锁。
ReadWriteLock只有两个方法,分别返回读锁和写锁。
1 | public interface ReadWriteLock { |
Lock接口的方法如下:
1 | public interface Lock { |
Lock接口中有一个方法可以获得Condition。每个对象都可以利用继承自Object的wait/notify方法来实现等待/通知机制。Condition也提供了类似的方法,通过与Lock的配合来实现等待/通知机制。
| 对比项 | Object监视器 | Condition |
|---|---|---|
| 前置条件 | 获取对象的锁 | 调用Lock.lock获取锁,调用Lock.newCondition()获取Condition对象 |
| 调用方式 | 直接调用,如object.notify() |
直接调用,如condition.await() |
| 等待队列的个数 | 一个 | 多个 |
| 当前线程释放锁进入等待状态 | 支持 | 支持 |
| 当前线程释放锁进入等待状态,在等待状态中断 | 不支持 | 支持 |
| 当前线程释放锁并进入超市等待状态 | 支持 | 支持 |
| 当前线程释放锁进入等待状态直到将来的某个时间 | 不支持 | 支持 |
| 唤醒等待队列中的一个线程 | 支持 | 支持 |
| 唤醒等待队列中的全部线程 | 支持 | 支持 |
Condition与Object的wait/notify基本相似。Condition.await()对应Object.wait(),Condition.signal/signalAll对应Object.notify/notifyAll。
ReentrantLock
ReentrantLock是JDK提供的Lock接口的默认实现,实现了锁的基本功能。其是一个可重入锁,内部有一个抽象类abstract static class Sync extends AbstractQueuedSynchronizer继承了AQS,是自己实现的一个同步器,有两个非抽象类static final class FairSync extends ReentrantLock.Sync和static final class NonfairSync extends ReentrantLock.Sync,分别是公平同步器和非公平同步器,代表着ReentrantLock支持公平锁与非公平锁。
这两个同步器都调用了AOS的setExclusiveOwnerThread(current);方法,所以ReentrantLock的锁是独占的,也就是说是排他锁,不能共享。
1 | public ReentrantLock(boolean fair) { |
在ReentrantLock的构造方法中,可以传入一个布尔类型的参数fair来指定其是否是公平锁,默认是非公平锁。
ReentrantReadWriteLock
ReentrantReadWriteLock类是ReadWriteLock接口的默认实现。与ReentrantLock类似,都是可重入的,支持公平锁与非公平锁。不同的是其还支持读写锁。
1 | public class ReentrantReadWriteLock implements ReadWriteLock, Serializable { |
可以看到,ReentrantReadWriteLock内部维护了两个同步器和两个Lock的实现类ReadLock和WriteLock。
实现了读写锁,但在写操作时,其他线程不能读也不能写,存在“写饥饿”。
StampedLock
public class StampedLock implements Serializable可以看到,StampedLock并没有实现Lock接口和ReadWriteLock接口,但其实现了“读写锁”的功能,并且性能更好。StampedLock把读锁和写锁分为乐观读锁和悲观读锁两种。
StampedLock避免了写饥饿现象,它的核心思想在于,在读的时候如果发生了写,应该通过重试的方法来获取新的值,而不应该阻塞写操作。这种模式也是典型的无锁编程思想,与CAS自旋的思想一样。StampedLock适合在读多写少的场景下使用,同时避免了写饥饿的产生。官方使用实例:
1 | public class Point { |
乐观读锁的意思是假定在这个锁获取期间,共享变量不会被改变。在获取乐观读锁后进行了一些操作,然后调用validate方法,这个方法是验证是否有写操作执行过,如果有,则获取一个悲观读锁。
StampedLock获取锁时会返回一个long类型的变量,释放锁时再把这个变量传进去。
1 | //用于操作state后获取stamp的值 |
StampedLock用long类型变量的前7位(LG_READERS)来表示读锁,每获得一个悲观读锁,就加一(RUNIT),每释放一个悲观读锁,就减一。而悲观读锁最多只能存储128个(7位限制),所以用一个int类型的变量来存储溢出的悲观读锁。
写锁用state变量剩下的位来表示,每次获得一个写锁,就加 0000 1000 0000(WBIT)。每次释放一个写锁,并不是减WBIT,而是再加上WBIT,这样做的目的是让每次写锁都留下痕迹,解决CAS的ABA问题,也为乐观锁见检查变化validate方法提供基础。
乐观读锁并没有改变state的值,而是在获取锁的时候记录state的状态,在操作完成后检查state的写状态部分是否发生变化,因为每次写锁都会留下痕迹。