上篇文章已经对多线程有个初步的认识了,这次我们来看看Java的Lock锁,主要有以下知识点:
AQS
ReentrantLock
ReentrantReadWriteLock
Lock和synchronized的选择
在学习Lock锁之前,我们先来看看什么是AQS?
AQS
- AQS其实就是一个可以给我们实现锁的框架,juc包中很多可阻塞的类比如ReentrantLock、 ReadWriteLock都是基于AQS构建的。
- 内部实现的关键是:先进先出的队列、state状态
- 在AQS中实现了对等待队列的默认实现,子类只要重写部分的代码即可实现(大量用到了模板代码)
- AQS同时提供了互斥模式(exclusive)和共享模式(shared)两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如
ReadWriteLock
。
注意:ReentrantLock不是AQS的子类,其内部类Sync才是AQS的子类。
State状态
AQS维护了一个volatile int
类型的state
变量,用来表示当前同步状态。
volatile虽然不能保证操作的原子性,但是保证了当前变量state的可见性。
compareAndSetState
compareAndSetState用来修改state状态,它是一个原子操作,底层其实是调用系统的CAS算法,有关CAS可移步:CAS
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
请求资源
acquire
acquire(int arg) 以独占方式获取资源,如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如果tryAcquire(int)
方法返回true,则acquire直接返回,否则当前线程需要进入队列进行排队。addWaiter()
将该线程加入等待队列的尾部,并标记为独占模式;
ReentrantLock
学习ReentrantLock之前先来看看它实现的Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
- lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。
- unLock()方法是用来释放锁的。
- newCondition()方法是创建一个条件对象,用来管理那些得到锁但是不能做有用工作的线程。
ReentrantLock
,意思是"可重入锁
",线程可以重复地获得已经持有的锁。 ReentrantLock是唯一实现了Lock接口的类。接下来我们来看看有关源码:
AQS子类
ReentrantLock实现了三个内部类,分别是Sync、NonfairSync和FairSync。
abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync
这些内部类都是AQS的子类,这就印证了我们之前所说的:AQS是ReentrantLock的基础,AQS是构建锁的框架.
构造器
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认实现的是非公平锁,传入true表示使用公平锁。
加锁
- ReentrantLock中加锁使用的是
lock
方法 - 默认使用
非公平锁
的lock方法
加锁流程
首先会通过CAS
方法,尝试将当前的AQS中的State
字段改成从0改成1,如果修改成功的话,说明原来的状态是0,并没有线程占用锁,而且成功的获取了锁,只需要调用setExclusiveOwnerThread
函将当前线程设置成持有锁的线程即可。否则,CAS
操作失败之后,和普通锁一样,调用父类AQS的acquire(1)
函数尝试获取锁。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))//尝试获取锁
setExclusiveOwnerThread(Thread.currentThread());
else //获取失败则调用AQS的acquire方法
acquire(1);
}
而在AQS的acquire(1)
函数中,会判断tryAcquire(1)
以及acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,如果尝试获取失败并且添加队列成功的话,那么就会调用selfInterrupt
函数中断线程执行,说明已经加入到了AQS的队列中。
注意:AQS的tryAcquire(1)
是由子类Sync(也就是ReentrantLockd的静态内部类)自己实现的,也就是用到了模板方法,接下来我们去看看子类的实现。
tryAcquire
是在NonfairSync
类中实现的,其中调用了nonfairTryAcquire
函数。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//获取当前线程状态
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//可重入锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
在nonfairTryAcquire
函数中,会尝试让当前线程去获取锁:
- 获取当前线程,以及AQS的状态
- 如果当前AQS的状态为0的话,那么说明当前的锁没有被任何线程获取,则尝试做一次
CAS
操作,将当前的状态设置成acquires
,如果设置成功了的话,那么则将当前线程设置成锁持有的线程,并且返回true,表示获取成功。 - 如果当前的状态不为
0
的话,说明已经有线程持有锁,则判断当前线程与持有锁的线程是否相同,如果相同的话,则将当前的状态加上acquires重新将状态设置,并且返回true,这也就是重入锁
的原因。 - 如果当前线程没有获取到锁的话,那么就会返回false,表示获取锁失败。
源码参考:ReentrantLock中的NonfairSync加锁流程
ReentrantReadWriteLock
概述
我们知道synchronized内置锁和ReentrantLock都是互斥锁
(一次只能有一个线程进入到临界区(被锁定的区域))
而ReentrantReadWriteLock是一个读写锁
:
- 在读取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)
- 在写数据的时候,无论是读线程还是写线程都是互斥的
一般来说:我们大多数都是读取数据得多,修改数据得少。所以这个读写锁在这种场景下就很有用了!
ReentrantReadWriteLock
实现了ReadWriteLock
接口.
接口只有两个方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
性质
- 读锁不支持条件对象,写锁支持条件对象
- 读锁不能升级为写锁,写锁可以降级为读锁
- 读写锁也有公平和非公平模式
- 读锁支持多个读线程进入临界区,写锁是互斥的
和ReentrantLock相比,ReentrantReadWriteLock多了ReadLock
和WriteLock
两个内部类。
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
读锁和写锁的状态表示
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;// 高16位为读锁,低16位为写锁
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
读写锁对于同步状态的实现是将变量切割成两部分,高16位表示读,低16位表示写。
看个实际例子
class czy{
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
........
public void read(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} finally {
rwl.readLock().unlock();
}
}
}
Lock和synchronized的选择
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized
是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
总结
有关Lock锁的知识点就到这里,如果想了解更多请参考下面链接。
参考
Java3y多线程
Java技术之AQS详解