公平锁与非公平锁含义
公平锁:按照线程等待时间来排序,等待越久的先被执行。内部代码实现说白就是一个链表(等待队列),有对应的preview和next,每次锁释放后从链表头部开始取,然后修改head的位置。(First in First out)
非公平锁:非公平锁发生在对象A.unLock同时,对象B里面想获取锁,这时候对象A来没来得及通知等待队列里面的非洲孩子(thread),就被对象B抢先占用了锁,这就是非公平锁。而公平锁在这种情况下还是会让B进入等待队列去排队,等待取号。
源码分析前奏-volatile
Java内存模型
这部分在Jvm的第五部分,高效并发,具体详细可以自行看书。主要要了解,线程对所有值的操作都是基于自己的工作内存操作的,而工作内存中的值又是从主内存拷贝过来的。当线程对值操作结束后,工作内存会重新写入到主内存中,所以在多线程问题下处理变量才需要我们额外花点心思。
image.png
volatitle作用(JVM内存模式章节)
volatitle的轻量级的同步机制,作用是:一、保证变量对所有的线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说都是可以立即得知的(但并非立即可见)。这句话简单来说,就是工作内存每次使用前都会从主内存中获取一次该变量的值。
注意:以下两种场景满足时候,我们可以通过volatitle去实效锁的效果:一、运算结果并不依赖变量的当前值(i++这种属于依赖当前值了),或者能够确保只有单一的线程修改变量值。二、变量不需要与其他的状态变量共同参与不变约束。
源码分析
ReentrantLock锁
- ReentrantLock可以实现公平/非公平锁的效果。
- 构造函数中可以传入boolean,从而实现你需要实现哪个锁的效果。
//默认构造方法,非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
非公平锁
获取锁过程
- 非公平锁在lock方法里面可以看到,只会判断当前锁是否被对象持有,不会判断等待队列里面是否有线程。compareAndSetState(int expect, int update)是期待0,如果是0,将状态改成1。0是锁未被持有状态。如果期待值不是0,就会走到acquire方法,里面的第一个判断条件,又会走进具体的NonfairSync/FairSync的tryAcquire方法里面。
- nonfairTryAcquire方法里判断当前锁的状态,如果未被持有直接就让当前线程持有锁(与下面对比可以明显看出if条件少了个判断,剩下的都基本大同小异,这里就不详细讲了)。
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//添加到等待队列里
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//nonfairTryAcquire方法在Sync父类已经默认实现了(只能感叹什么都是相对公平而已,连个程序都告诉没有默认的公平,要公平得自己创造)
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;
}
公平锁
获取锁过程
- 公平锁在lock方法里面可以看到与上面的明显区别,公平锁根本没给你机会判断锁的使用情况,直接把你丢进acquire(1)里。acquire方法第一个判断条件,又会走进具体的NonfairSync/FairSync的tryAcquire方法里面。
- tryAcquire方法里判断当前锁的状态,如果未被持有,还要判断等待队列里是否有线程在排队了,如果都符合:1.锁未被持有;2.等待队列为空,就让当前线程持有锁。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
//直接添加到等待队列里面
acquire(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;
}
}
//这里可以看出ReentrantLock属于重入锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
等待队列
Note结点
- 等待队列就是自行定义的一个数据结构,里面很多变量都用到volatitle,这也是为什么开头我就要介绍volatitle字段的用法和含义。
static final class Node {
……
//线程等待状态
volatile int waitStatus;
//
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
……
}
尾部插入等待结点
- 公平锁/非公平锁的acquire方法里第二个判断条件就有个addWaiter方法,这个方法就是构建等待队列的关键。
private Node addWaiter(Node mode) {
//重新创建是为了在构造函数中把当前线程存储起来
Node node = new Node(mode);
for (;;) {
//获取链表的尾部结点
Node oldTail = tail;
if (oldTail != null) {
U.putObject(node, Node.PREV, oldTail);
//判断链表尾部是否为oldTail,是的会就更新成node,这个很多同步操作都可以看到用到这个底层API去实现的
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
//如果尾部的Node为空,证明还没有等待队列
//创建等待队列
initializeSyncQueue();
}
}
}
判断队列中线程状态
- 等待队列中的添加线程,首先要判断线程自身状态,因为线程只能自己中断自己,别人无法帮你做这个东西。所以入队列的时候需要检测自己是否已经被中断了。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//线程自己设置自己的中断状态
selfInterrupt();
}
//只看acquireQueued里面的shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* 从链表中删除掉已经被置成cancelled的线程
*
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 刚开始所以加入等待队列的线程都被置成SIGNAL状态(等待被唤醒)
*/
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
//检测
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//👇这个和线程的interrupt很像,但这只不过是获取线程是否中断状态
return Thread.interrupted();
}
等待队列中取出线程
- 因为我们在占用锁的时候,把当前线程附值给了exclusiveOwnerThread变量,所以当锁调用unlock方法时候,我们要这个变量置null
public void unlock() {
//调用抽象类AbstractQueuedSynchronizer的方法,然后回调到抽象类Syn里面
sync.release(1);
}
//AbstractQueuedSynchronizer的方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//Syn的方法
protected final boolean tryRelease(int releases) {
//参数默认值都是1,然后c最后等于0,就证明释放干净了
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;
}
- 将等待队列里面的下一个线程取出,里面你会发现head其实是指当前这个持有锁的线程,并非说是等待线程。这里就很简单,取队列前判断里面有没有线程被cancelled了,有就删除掉。然后把下个准备持有锁的线程解除阻塞状态(唤醒线程去持有锁)。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
//waitStatus大于0都是需要cancelled的线程
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread);//解除阻塞状态
}
总结
- 发现很多的同步机制都依赖compareAndSet****方法,这个可能后面有时间想去研究下里面的实现。
- 看了一次锁的同步机制,后面自己在写多线程的调用时能避免踩更多的坑。
网友评论