多线程之ReentrantLock

多线程之ReentrantLock

ReentrantLock 与 AQS

img
img
img
//加锁
public void lock() {
sync.lock();
}

//释放锁
public void unlock() {
sync.release(1);
}

重入锁实现同步过程:

  1. 线程1调用lock()加锁,判断state=0,所以直接获取到锁,设置state=1 exclusiveOwnerThread=线程1。
  2. 线程2调用lock()加锁,判断state=1 exclusiveOwnerThread=线程1,锁已经被线程1持有,线程2被封装成节点Node加入同步队列中排队等锁。此时线程1执行同步代码,线程2阻塞等锁。
  3. 线程1调用unlock()解锁,判断exclusiveOwnerThread=线程1,可以解锁。设置state减1,exclusiveOwnerThread=null。state变为0时,唤醒AQS同步队列中head的后继节点,这里是线程2。
  4. 线程2被唤醒,再次去抢锁,成功之后执行同步代码。

线程最终获取到锁的标志就是AQS.state>0AQS.exclusiveOwnerThread==当前线程

SYNC

img

  • NoFairSync
  • FairSync

LOCK Interface lock 借助sync

public void lock()                { sync.acquire(1); }
public boolean tryLock() { return sync.tryAcquire(1); }

NOFairSync (默认为非公平锁)

非公平锁,如果CAS state 0->1, 则占有锁

public boolean tryAcquire(int acquires) {
assert acquires == 1;

if (this.compareAndSetState(0, 1)) {
this.setExclusiveOwnerThread(Thread.currentThread());
return true;
} else {
return false;
}
}

FairSync

公平锁, 如果CAS state 0->1, 并且没有等待该线程的锁,才占有锁

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

区别

当判断到锁状态字段state == 0 时,不会立马将当前线程设置为该锁的占用线程,而是去判断是在此线程之前是否有其他线程在等待这个锁(执行hasQueuedPredecessors()方法),如果是的话,则该线程会加入到等待队列中,进行排队(FIFO,先进先出的排队形式)

总结

通常非公平锁性能高于公平锁,原因是: 恢复一个被挂起线程与线程执行存在延迟,在竞争激烈的情况下,比如a持有锁,b被挂起,a释放锁时,b将被唤醒并尝试获取锁。于此同时C再b唤醒前已经获得,使用并释放了该锁。。

如果每个线程持有锁的时间都比较长,可以使用公平锁。非公平锁抢占可能导致线程饥饿。

线程排队

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

获锁失败时候,AddWaiter(Node.EXCLUSIVE)就是请求排队的

如何排队?

双向链表实现的队列

关键信息:

  • 当前线程
  • 线程状态
  • 前驱和后继

img

线程的2种等待模式:

  • SHARED:表示线程以共享的模式等待锁(如ReadLock)
  • EXCLUSIVE:表示线程以互斥的模式等待锁(如ReentrantLock),互斥就是一把锁只能由一个线程持有,不能同时存在多个线程使用同一个锁

线程在队列中的状态枚举:

  • CANCELLED:值为1,表示线程的获锁请求已经“取消”
  • SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
  • CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
  • PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用上

例子

线程A获锁成功,无排队线程

线程B申请锁,排队

Screen Shot 2020-08-28 at 3.46.21 PM

如果感知锁空闲并获取锁

acquireQueued方法就是把放入队列中的这个线程不断进行循环“获锁”,直到它“成功获锁”或者“不再需要锁(如被中断)

解锁的时候:

unparkSuccessor(h)方法就是“唤醒操作

public void unlock() {
sync.release(1);
}

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

ReentrantReadWriteLock

ReentrantLock 是互斥锁,每次最多只有一个线程获得。

ReentrantReadWriteLock支持以下功能:

  1. 支持公平与非公平的获取锁方式。
  2. 支持可重入,读线程获取读锁后还可以获取读锁,但是不能获取写锁;写线程获取写锁后既可以再次获取写锁还可以获取读锁。
  3. 允许从写锁降级为读锁,其实现方式是:先获取写锁,然后获取读锁,最后释放写锁。但是,从读锁升级到写锁是不可以的;
  4. 读取锁和写入锁都支持锁获取期间的中断;
  5. Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,readLock().newCondition() 会抛出 UnsupportedOperationException。

读写锁: 一个资源可以允许多个读操作,或者一个写操作,读写不能同时进行。

 */
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();

/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}

两个锁,当线程对锁的持有时间长并且大部分操作不会修改共享资源,使用读写错

example

class CachedData {
Object data;
volatile boolean cacheValid;
// 读写锁实例
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

void processCachedData() {
// 获取读锁
rwl.readLock().lock();
if (!cacheValid) { // 如果缓存过期了,或者为 null
// 释放掉读锁,然后获取写锁 (后面会看到,没释放掉读锁就获取写锁,会发生死锁情况)
rwl.readLock().unlock();
rwl.writeLock().lock();

try {
if (!cacheValid) { // 重新判断,因为在等待写锁的过程中,可能前面有其他写线程执行过了
data = ...
cacheValid = true;
}
// 获取读锁 (持有写锁的情况下,是允许获取读锁的,称为 “锁降级”,反之不行。)
rwl.readLock().lock();
} finally {
// 释放写锁,此时还剩一个读锁
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}

try {
use(data);
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
}

ReadLock 和 WriteLock 中的方法都是通过 Sync 这个类来实现的。Sync 是 AQS 的子类,然后再派生了公平模式和不公平模式。

锁降级

将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。

但是,锁升级是不可以的。线程持有读锁的话,在没释放的情况下不能去获取写锁,因为会发生死锁

锁降级的本质是释放掉独占锁,使其他线程可以获取到读锁,提高并发,而当前线程持有读锁来保证数据的可见性。


ReentrantLock Vs Synchronized Vs Volatile

https://mp.weixin.qq.com/s/-Q-K9zhb8k0c3n_5zhxuTg

volatile 保证可见性,不保证原子性 (例如 i++ 为符合操作)

Volatile实现内存可见性是通过store和load指令完成的;对volatile变量执行写操作时,会在写操作后加入一条store指令,强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。

synchronized关键字能够实现原子性和可见性;synchronized底层是通过使用对象的监视器锁(monitor)来确保同一时刻只有一个线程执行被修饰的方法或者代码块。

Java5以后出现的juc包(java.util.concurent)中有很多Lock的实现类

1、读写锁(ReadLock、WriteLock、ReadWriteLock)

2、可重入锁(ReentrantLock)

3、可中断锁

4、公平锁和非公平锁 (在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。)

etc..


https://zhuanlan.zhihu.com/p/54297968

https://mp.weixin.qq.com/s/QzbrsRAC0WxyBjruqrmZhw

https://mp.weixin.qq.com/s/hump_BZTt7FX8w_Q9LmSnw

https://mp.weixin.qq.com/s?__biz=MzAxMjEwMzQ5MA==&mid=2448889644&idx=1&sn=98f814d8ff98a41658ba704a68c942b2&scene=21#wechat_redirect

https://mp.weixin.qq.com/s/BdmSJHdVFI5yGeBbb8Lr-A

https://mp.weixin.qq.com/s/bjyE40yHK2r0V3qi0pAYwA