先来段背景故事,1.6之前synchronize直接调用底层操作系统的函数进行加锁。这样做是很重的,CPU在内核态与用户态之间不断的切换十分消耗性能。此时我们的大神Doug Lea就站了出来,这个老头吊的很我们的JUC包就是这位大佬写的。在多线程领域有着举足轻重的地位。他创造了Reentrant Lock来优化我们加锁的性能。sun公司一看这我他妈能忍么,岂不是让外人看我们笑话。从1.7开始对
synchronize关键字不断的进行了优化。到1.8时synchronize已经得到了极大的优化与ReentrantLock已
经相差无几,甚至在并发量达到一定程度时synchronize的性能还要优于-ReentrantLock。可以明显看到在1.8后ConcurrentHashMap已经弃用ReentrantLock改用synchronize实现。毕竟synchronize才是亲儿子,这可能也是未来的走向。不扯了。。。。一起去学习下ReentrantLock
lock():
lock又分为公平锁与非公平锁分别对应FairSync和NonfairSync这两个内部类他们都继承于Sync,而Sync又继承了AQS。
ReentrantLock默认为非公平锁,默认构造函数使用NonfairSync
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:
final void lock() {
acquire(1);
}
AbstractQueuedSynchronizer
public final void acquire(int arg) {
//tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队,如果加锁成功则不会调用
//加入队列之后线程会立马park,等到解锁之后会被unpark,醒来之后判断自己是否被打断了(这句话其实漏掉了很多细节忽略了它自旋的过程,后面会有提到)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
FairSync
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
int c = getState();// private volatile int state;
//获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
if (c == 0) { //判断锁是否为自由状态
//hasQueuedPredecessors,判断自己是否需要排队
//如果不需要排队则进行cas尝试加锁,如果加锁成功则把当前线程设置为拥有锁的线程
//继而返回true
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//设置当前线程为拥有锁的线程,方便后面判断是不是重入(只需把这个线程拿出来判断是否当前线程即可判断重入)
setExclusiveOwnerThread(current);
return true;
}
}
//如果C不等于0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
//如果C不等于0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
//那么这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
AbstractQueuedSynchronizer
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && // 判断头和尾是否相等
// 当队列节点大于一个时h != t 成立会继续后面判断s = h.next) == null,第二个节点是否为null
因为是短路或所以不管返回什么后面依然需要判断s.thread != Thread.currentThread()判断第二个节中的thread是否为当前来获取锁的线程
((s = h.next) == null || s.thread != Thread.currentThread());
}
AQS中基本概念
private transient volatile Node head; //队首
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0
private transient Thread exclusiveOwnerThread;// 父类AbstractOwnableSynchronizer中定义,代表持有锁的线程
static final class Node {
static final int PROPAGATE = -3;
volatile int waitStatus;// 当前node状态
volatile Node prev; // 上一个节点
volatile Node next;// 下一个节点
volatile Thread thread; // 当前线程
Node nextWaiter;
static final int SIGNAL = -1;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
1.假设当前线程T1第一个来来获取锁,此时state为初始值0。 if (c == 0) 成立进入当前代码块,然后判断hasQueuedPredecessors()是否需要排队。此时由于是第一次进入head,tail都还是null,判断 h != t &&空不等于空所以结果为false后面是短路与就不再往后面判断直接返回false。
2.!hasQueuedPredecessors() ,false取反为true所以会继续后面的判断 compareAndSetState(0, acquires)。CAS就会将state值变为acquires=1。所以return true;而 if (!tryAcquire(arg) && 取反为false短路与后面的判断就不会继续, selfInterrupt();也不会执行
3.acquire(int arg)-> sync.lock();->lock()正常返回没有阻塞,继续执行下面代码。此时我们相当于已经获取到锁。
这里就体现了我们ReentrantLock设计的优点,当我们没有并发或者是交替执行的情况下并不会调用到我们操作系统的函数。而1.6之前的synchronize只要你加上这个关键字就会无脑去调用底层操作系统函数加锁这也是之前为什么synchronize锁很重的原因。1.6后逐步进行了优化。
4.假设T1还在执行没有释放掉锁时,T2来获取锁。T2又会调用tryAcquire方法来获取锁,此时因为T1已经占有了锁, int c = getState();此时获取到state的值就为1(因为T1已经用CAS改变了state的值)。此时c!= 0 就会进入下面 else if
(current == getExclusiveOwnerThread())判断当前持有锁的线程是否为当前线程,这里就是体现我们可重入的地方,如果当前线程为持有锁的线程则直接int nextc = c + acquires;setState(nextc);state就会变为2代表重入了一次。但是我们此时假想情况进入的是T2所以不是重入直接返回false。此时 if (!tryAcquire(arg) &&acquireQueued (addWaiter(Node.EXCLUSIVE), arg)),!tryAcquire(arg)取反就是true,所以会继续执行短路与后面的判断。
addWaiter----入队
接着就需要判断自己是否需要进入队列中等待获取锁
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);// 首先实例化一个node
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
5.addWaiter(Node.EXCLUSIVE), Node pred = tail;将尾部赋值给临时变量(这里因为队列仍然没有初始化所以tail依然
为null),此时pred=null, if (pred != null)此时就会跳过if代码块继续执行下面的 enq(node);
enq(node);
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))// 设置AQS头部
tail = head;
} else {
node.prev = t;// 入队
if (compareAndSetTail(t, node)) {//
t.next = node;
return t;
}
}
}
}
6.此时将T2创建的node(头尾为null ,thread为t2)传入到enq方法中, for (;;) 进入死循环,首先将队尾赋值给了t,此时tail依
然为null。 if (t == null)成立,if (compareAndSetHead(new Node()))。此时head跟tail都为null,thread也为null。现在我
们AQS队列中就有了一个node。这里是AQS设计的一个精髓,队列中的队头thread属性永远为空。因为这里是一个死循环所以又会回到开头 Node t = tail; 此时tail已不为空。就会接着进入 else, node.prev = t;node使我们t2产生的,这样就将我们t2节点与队列中头结点连接起来了,此时就是入队。 if (compareAndSetTail(t, node)),这里采用CAS将之前头结点的尾部改为T2的node。 t.next = node;将头结点的next指向T2节点。 return t;返回头结点。
acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC 将头节点的next为空彻底断开与下一个节点的连接,失去引用后方便垃圾回收
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
7.此时T2已经入队,但是在进入阻塞前需要去判断下此时T1是否已经将锁释放掉,这里又是自旋的再次体现。如果此时T1已经释放掉锁而我们没有去再次获取而是直接进入阻塞这样会产生不必要的性能浪费。这个判断只会产生于第二个node因为第一个node其实为空。在公平情况下只有可能node2先获取到锁。只有第二个node才有自旋的资格。
predecessor()
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
8. final Node p = node.predecessor();拿到node的上一个节点,即T2的上一个node就是头结点。 if (p == head && tryAcquire(arg)) 判断P是不是头部,其实就是判断自己是不是第一个排队的。此时T2是第一个排队,接下来就会调用
tryAcquire(arg)再次去尝试获取锁。这里就是所谓的自旋。tryAcquire(arg)如果拿到锁则继续往下执行,如果没拿到锁
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())则继续下面判断进入阻塞。
shouldParkAfterFailedAcquire
判断在自旋一次加锁失败后是否需要阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;// 获取头结点状态,此时ws仍为初始值0
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);// 为什么不直接返回
}
return false;
}
9.传入上一个节点即头结点,和T2节点。 if (ws == Node.SIGNAL)判断ws是否为-1,此时ws为0.所以继续往下执行
if (ws > 0) 也不符合,直接会进入else , compareAndSetWaitStatus(pred, ws, Node.SIGNAL);这里就会通过CAS将我们的ws改为-1(注意这里改变的是T2的上一个节点的值),并返回Flase。 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()),所以短路与后面 parkAndCheckInterrupt()并不会执行。而是再次自旋回到我们for循环,假设此时还是拿不到锁。就会继续执行
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()),上一次自旋我们已经将ws改为-1,此时
if (ws == Node.SIGNAL)成立,返回true整个方法结束。就会执行短路与后面的parkAndCheckInterrupt()
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程
return Thread.interrupted();
}
到这里T2正式进入阻塞状态。假设此时T3也来获取锁T3是否会自旋呢?上面我们说了只有第二个node才有自旋资格。
所以T3不会自旋。因为在公平锁环境下T3前面还有T2在等待获取锁,怎么滴都先轮不到她啊着急也没用的。T3入队后同样会把T2的waitstate改为-1。AQS这里都是下一个节点将上一个节点waitstate进行修改,为什么不自己修改呢?四个字可以理解为:当局者迷。你自己都休眠了你拿什么去改,如果你在休眠前就改了值有如何确保你改了值是否真的进入休眠了呢。
解锁:
unlock
public void unlock() {
sync.release(1);
}
release
public final boolean release(int arg) {
if (tryRelease(arg)) { //tryRelease返回true继续执行
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
Sync#tryRelease
protected final boolean tryRelease(int releases) {// 1
int c = getState() - releases; // state - 1
if (Thread.currentThread() != getExclusiveOwnerThread())// 判断持有锁线程与解锁线程是否同一线程
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // state = 1时
free = true;
setExclusiveOwnerThread(null);// 设置当前持有锁的线程为空
}
setState(c); 设置state为0
return free; // true
}
AbstractQueuedSynchronizer#getState
protected final int getState() {
return state;
}
AbstractQueuedSynchronizer#unparkSuccessor
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next; // t2
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
setHead
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
unparkSuccessor(h);唤醒头节点, ws 已经被T2改为-1, if (ws < 0)成立。 compareAndSetWaitStatus(node, ws, 0);
如果node=-1,则通过CAS改为0。s = node.next;拿到t2节点.因为此时队列中第二个节点不为null所以直接调用
LockSupport.unpark(s.thread);唤醒节点2中的t2线程。t2线程被唤醒后就会继续去竞争锁再次进入acquireQueued()方法,此时 parkAndCheckInterrupt()没有被打断返回false。条件不成立则继续从头循环。
final Node p = node.predecessor();拿出T2的上一个节点 if (p == head && tryAcquire(arg))因为此时T1已经将锁释放掉,所以可以成功获取到锁进入if代码块。这个时候就会 setHead(node);把之前的T2节点设置为head并将thread,prev 全部置为空 . p.next = null; // help GC 将之前头节点的next置为空。此时之前的头节与T2节点之间的关系就完全断掉了,没有引用后就会被垃圾回收。而T2节点也从被重置为null成为新的头节点,这也符合了我们前文说说说到的所有持有锁的节点不在队列中。所以当我们第一次获取锁时都会虚拟出一个空节点
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
非公平锁:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
ReentrantLock打断原理
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
普通lock是无法打断的
ublic class ReentrantLockDemo {
static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->
test()
);
Thread t2 = new Thread(()->
test()
);
t1.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("main");
t2.start();
t2.interrupt();
}
public static void test(){
reentrantLock.lock();
String threadName = Thread.currentThread().getName();
System.out.println(threadName);
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
reentrantLock.unlock();
}
}
acquireInterruptibly
public class ReentrantLockDemo {
static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->
test()
);
Thread t2 = new Thread(()->
test()
);
t1.start();
TimeUnit.SECONDS.sleep(3);
System.out.println("main");
t2.start();
t2.interrupt();
}
public static void test(){
// reentrantLock.lock();
try {
reentrantLock.lockInterruptibly();
String threadName = Thread.currentThread().getName();
System.out.println(threadName);
TimeUnit.SECONDS.sleep(1000);
reentrantLock.unlock();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被打断");
e.printStackTrace();
}
}
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) // 判断当前线程是否在没有加锁前被打断
throw new InterruptedException();
if (!tryAcquire(arg)) // 这里与普通锁是一样的,没获取到锁则返回false取反为true继续下面执行
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
对比与普通lock区别
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
============================================================================================
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
===========================================================================================
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted(); //当阻塞线程被唤醒后会从这里继续执行
}
==============================================================================================
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
假设t2此时阻塞在队列中,在被打断后就会继续执行 return Thread.interrupted();此时返回true。
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()),if中条件达成,就会继续执行下面的 interrupted = true;将interrupted 赋值为true.然后继续循环直到T2拿到锁后acquireQueued()方法返回true。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这样就会进入selfInterrupt方法
static void selfInterrupt() {
Thread.currentThread().interrupt(); // 再次打断当前线程,这里并不是真正打断而是标识作用
}
这里正是精髓所在在两次interrupted()之后返回值变为了false,当在普通lock中调用interrupt之后就会进入到selfInterrupt再次进行打断,这次打断过后就将之前的false还原为了false。(注意别把Interrupt与isInterrupted混淆),且普通lock并没有抛出interrupt无法捕获。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}