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
的写状态部分是否发生变化,因为每次写锁都会留下痕迹。