重入锁&读写锁原理分析笔记
ReentrantLock可重入锁
支持一个线程多次(重入)获取锁,当前线程可不进行unlock(),再一次lock()
锁的公平非公平,如果再一个绝对时间上,可以按照先请求锁的线程先获取到,那么就是公平的。反之,是非公平
任何实现重入锁(排它锁)
ReentrantLock默认是非公平锁,以此解析
//这个是继承了Sync类的NonfairSync实现的方法
final void lock() {//先去判断当前同步状态,是0则表示获取成功并修改状态为1,否则就会去调用AQS的acquire独占式的尝试获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//AQS中
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//tryAcquire(arg)需要继承了AQS类的子类去重写,尝试获取不到则进入同步队列自旋获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//在ReentrantLock中的NonfairSync实现了该方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//执行不公平的tryLock
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//只有当前同步状态为0时,才会获取成功
if (compareAndSetState(0, acquires)) {//cas(0,1)比较并设置状态为1
setExclusiveOwnerThread(current);
return true;
}
}
//可重入锁关键
else if (current == getExclusiveOwnerThread()) {//如果同步状态不为0,但是此时线程和持有锁线程一致,也会获取成功
int nextc = c + acquires;//同步状态+1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置同步状态
return true;
}
return false;
}
可见,可重入锁是不仅判断同步状态还会去判断获取尝试同步状态的线程和持有锁线程是否一致。
如果一致,则同步状态+1,当然线程释放锁时,必须state–,释放一次减1
//释放锁代码,比如重进入了n次,那么前n-1次释放锁返回都是false
protected final boolean tryRelease(int releases) {//unlock默认参数为1
int c = getState() - releases;//同步减状态
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
公平与非公平
公平就是上面的代码,非公平获取锁和公平就差了一个方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&//这里,非公平直接cas获取,而公平锁为了保证FIFO,则增加了判断同步队列这当前节点有没有前驱节点的方法
compareAndSetState(0, acquires)) {//有则返回true,不执行cas等待前驱结点获取执行并释放后再次尝试获取
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;
}
如何选择非公平和公平?默认为什么是非公平?
并发体现的是性能,速度。非公平有着极少的线程切换,而公平锁为了保证FIFO,有着极大的线程切换耗能。
但非公平锁可能会造成“饥饿”,就是存在线程一直获取不到锁。
读写锁
JUC提供的读写锁是reentrantReadWriteLock 可重入读写锁
此类维护了一对锁,一个读锁,一个写锁。可以多个线程持有读锁,但当写锁被持有后所有线程都会被阻塞(获取读锁和写锁的线程都会被阻塞)。
在JDK1.5之前,需要使用等待通知机制。有了此类就读线程获取读锁,写线程获取写锁。当写锁被持有后,其它所有读写操作都会被阻塞,写锁被释放后就都开始执行。
ReentrantReadWriteLock特性:
1. 支持公平,非公平选择。默认公平
2. 可重入获取锁。读操作可以重新进入读锁,写操作可以重新进入写锁,同时也可以获取读锁。
3. 锁降级,只有写锁可以降为读锁并不可返回
读写锁的使用
package com.w.juc;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteDemo {
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
public static void write(){
try {
writeLock.lock();
System.out.println(Thread.currentThread()+"写"+System.currentTimeMillis());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
public static void read(){
try {
readLock.lock();
System.out.println(Thread.currentThread()+"读"+System.currentTimeMillis());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->read(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
new Thread(()->read(),"Thread5").start();
new Thread(()->read(),"Thread6").start();
}
}
-
读锁可以一起被获取,而写锁被一个线程持有后其余线程只能排队
-
当写锁被获取到,尝试获取读锁的线程也会被阻塞
read方法和write方法都会让线程睡眠3秒
看打印结果可知,获取读锁的线程是同时获取到了,而且3秒后都释放了。而写锁则获取其余等待3秒,两个写操作使得后面的读操作就等待了6秒
读写锁公平与非公平
读写锁默认是使用非公平锁
-
二者区别
和ReentrantLock一致,公平策略下必须FIFO。无法插队
非公平则可插队,插队策略:
- 对于写操作获取写锁可任意插队
- 对于读操作获取读锁:在同步队列中头节点不为获取写操作线程,则可插队
-
示例
package com.w.juc; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteDemo { private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false); private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); public static void write(){ System.out.println(Thread.currentThread()+"尝试获取写锁"); try { writeLock.lock(); System.out.println(Thread.currentThread()+"获取成功写锁"+System.currentTimeMillis()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock.unlock(); System.out.println(Thread.currentThread()+"释放写锁成功"); } } public static void read(){ System.out.println(Thread.currentThread()+"尝试获取读锁"); try { readLock.lock(); System.out.println(Thread.currentThread()+"获取成功读锁"+System.currentTimeMillis()); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); System.out.println(Thread.currentThread()+"释放读锁成功"); } } public static void main(String[] args) { new Thread(() -> write(), "Thread1【写】").start(); new Thread(() -> read(), "Thread2【读】").start(); new Thread(() -> read(), "Thread3【读】").start(); new Thread(() -> write(), "Thread4【写】").start(); new Thread(() -> read(), "Thread5【读】").start(); } }
这个代码多执行几次,你会有机会发现Thread3在Thread2之前获取锁成功
在Thread1获取成功写锁之后,其余线程都被放到等待队列。
所以才会有Thread3先于Thread2获取到锁的结果,如果是公平锁则必须按照先进入同步队列的先获取FIFO策略
读写锁实现分析
1.读写状态设计
对于读写锁,它的同步状态也是由AQS队列同步器管理。但是它有读锁和写锁俩种状态,而AQS只有一个状态变量,而且读锁可以被多个线程获取
这里就使用了“按位切割”这个状态变量是一个整型变量。读写锁将该变量切割为高16位表示读状态、低16位表示写状态。
通过位运算来获取读和写状态。
-
写状态获取 state & 0x0000FFFF(将高16位抹去),读状态获取 state >>>16 无符号右移16位
-
写状态更改 state + 1 读状态更改 state + (1<<<16)
2.写锁的释放和获取
获取:写锁和ReetrantLock差不多,支持重进入。但是多了一个读锁的判断,如果当前存在读锁,则获取写锁失败。要保证写锁对之后读锁都可见,而如果当前存在读锁,写锁依旧可以进入,则对于当前读锁此写锁的操作不可见。只有当其它线程都释放了读锁,写锁才能被当前线程获取。
//尝试获取同步状态,此方法return false 则会被AQS放入同步队列
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();//获取当前同步状态
int w = exclusiveCount(c);//返回写状态数
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0) 怎么知道读锁存在?c是共同的状态 当前c!=0,但是W==0则比如r!=0
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);//低16位 可直接+1
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))//cas设置状态
return false;
setExclusiveOwnerThread(current);
return true;
}
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }//c & 0x0000FFFF 16进制
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //1 << 16 - 1 == 0x0000FFFF
static final int SHARED_SHIFT = 16;
final boolean writerShouldBlock() { //写操作总是可以插队
return false; // writers can always barge
}
释放:和ReetrantLock一致,如果写状态不为0则每次释放都减少写状态,为0时表示释放成功
3.读锁的释放和获取
获取:读锁也是支持重进入,当没有其它写线程持有写锁时(写状态为0),读锁能够被同时多个读线程获取。读状态是所有读线程获取锁次数的总和,而每个线程各自获取锁的次数只能保存在TreadLocal里面
如果其它线程已经获取写锁,则当前获取读锁失败,进入等待状态。如果是当前线程获取了写锁或者写锁未被任何线程获取,那么当前线程增加读状态,CAS保证。可能多个线程同时获取读状态成功
释放:读锁释放也得是线程安全的,多个线程同时减少读状态
4.锁降级
锁降级是指:当前线程已经持有写锁,再不释放写锁的前提下获取读锁,之后释放写锁。