本质: 锁是用来解决线程安全
问题的
Java中Lock的其他实现,WiteLock写锁、ReadLock读锁,本文主要以ReentrantLock重入锁展开
ReentrantLock 重入锁
重入锁、互斥锁,用来解决死锁问题的
1.ReentrantLock 的使用
static Lock lock = new ReentrantLock();
static int sum = 0;
public static void incr(){
lock.lock(); //抢占锁 没有抢占的会阻塞
try {
Thread.sleep(1);
sum ++ ;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock(); //释放锁
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(()->{
LockExample.incr();
}).start();
}
Thread.sleep(3000);
System.out.println(sum); //输出1000
}
J.U.C Lock和Synchronized使用的区别就在于Lock的加锁和释放锁需要手动操作
2.ReentrantLock原理实现
满足线程的互斥特性,意味着同一个时刻只允许一个线程进入到加锁的代码中
一把锁应该具备的基础条件:
- 有锁无锁的标识
- 没有抢占锁的线程处理
- 等待(直接先阻塞,释放CPU资源)
- wait/notify 存在无法唤醒指定线程
- LockSupport.park/unpark (阻塞指定线程,唤醒指定线程)
- Condition
- 排队(运行N个线程阻塞,此时线程处于等待状态)
- 通过一个数据结构,把N个排队的线程存储起来
- 等待(直接先阻塞,释放CPU资源)
- 锁的释放过程
- LockSupport.unpark(thread) ->唤醒处于队列中的指定线程
- 锁的公平性(是否允许插队)
以上条件还不是很清晰的话,继续看下面锁流程分析图
3.源码分析
1. Lock.lock()加锁
public void lock() {
//这个地方可以看到 有两个方法 公平(FairSync)和非公平(NonfairSync)
sync.lock();
}
//非公平锁代码
final void lock() {
//非公平锁和公平锁就是这里不同
//非公平锁在这里先尝试更新state状态 如果成功 直接获取锁
//公平锁就是直接去取抢占锁
if (compareAndSetState(0, 1))
//如果这里修改状态成功 直接设置锁的拥有者为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//否则就去抢占锁 接下来看这里###
acquire(1);
}
//AbstractQueuedSynchronizer
public final void acquire(int arg) {
//尝试获取锁失败的话 就加到AQS队列 先看 tryAcquire
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//公平锁抢占的代码 非公共锁的抢占nonfairTryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//与非公平锁的区别在hasQueuedPredecessors 下面说
if (!hasQueuedPredecessors() &&
//cas操作state
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");
//状态 为0 或者>0
setState(nextc);
return true;
}
return false;
}
- 接下来分析公平锁hasQueuedPredecessors 的情况 ,返回true说明当前线程要去排队,非公平锁是没有这个逻辑的
public final boolean hasQueuedPredecessors() {
//尾节点
Node t = tail;
//头节点
Node h = head;
//h后面的节点
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
//h!=t 返回true代表队列中至少有两个不同Node节点存在。
//(s = h.next) == null 返回false代表是有后继节点的。
//s.thread != Thread.currentThread() 返回true代表后继节点的线程不是当前线程,那当前线程自然得老老实实的去排队。
- 上面可以看到,满足上述情况并且CAS操作state成功的话,线程就拿到锁了,接下来继续分析没有拿到锁的线程怎么处理?
//回到AbstractQueuedSynchronizer#acquire
//acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Node.EXCLUSIVE独占状态
//先看addWaiter
private Node addWaiter(Node mode) {
//把当前线程封装成Node
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//当前AQS队列不为空 则使用cas操作将node添加到tail节点
if (pred != null) {
//尾插法
node.prev = pred;
//设置新节点为尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//cas失败通过enq加到AQS队列
enq(node);
return node;
}
//通过不断的自旋以及CAS操作加到AQS队列
private Node enq(final Node node) {
for (;;) {
//从尾节点往前操作到头节点 线程不安全的情况下 头节点的next节点一直在变
Node t = tail;
//当前尾节点是空的 创建一个头尾节点
if (t == null) { // Must initialize
//因为此时是没拿到锁操作 多线程下不是安全的 所以必须用CAS操作
if (compareAndSetHead(new Node()))
tail = head;
} else {
//当前线程节点加到尾节点后面
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 接下来看下加到AQS队列的线程怎么处理
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋
for (;;) {
//取出当前node的pre节点(前一个节点)
final Node p = node.predecessor();
//如果前置节点是head 说明当前节点排在等待队列的第一位 直接再去获取锁
if (p == head && tryAcquire(arg)) {
//成功获取锁
//说明头节点已执行完毕并且是释放了锁 然后唤醒的当前节点
//设置为头节点 并返回 然后执行加锁的代码
setHead(node);
//将前节点移除队列,这样就没有指向了,帮助GC快速回收
p.next = null; // help GC
failed = false;
return interrupted; //将中断状态返回,false表示没有中断
}
//shouldParkAfterFailedAcquire有三种情况
//1 如果已经是SIGNAL(等待被叫醒)状态 返回true
//2 ws>0 关闭状态(线程被中断) 移除关闭线程 返回false
//3 更新状态为SIGNAL状态 返回false
//返回false就下次继续自旋去实现操作 最终会返回true并执行阻塞代码
if (shouldParkAfterFailedAcquire(p, node) &&
// LockSupport.park(this); 阻塞当前线程 唤醒后从这里开始执行
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
加锁的代码到这里就分析完了,接下来继续分析释放锁的代码。
2.Lock.unlock()释放锁
public void unlock() {
//还是已安全锁为例
sync.release(1);
}
//arg 代表锁的次数 重入锁需要释放到0才可被其他线程获取
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//1.修改state 为初始状态0
//2.Node s = node.next; LockSupport.unpark(s.thread); 唤醒下一个线程
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//重入锁需要释放到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;
}
//再看下释放锁并唤醒下一个等待线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//修改state为初始状态0
compareAndSetWaitStatus(node, ws, 0);
//获取下个等待唤醒的线程
Node s = node.next;
//下一个节点无效或者节点状态为关闭状态的
if (s == null || s.waitStatus > 0) {
s = null;
//通过从尾部节点开始扫描,找到距离head最近的一个
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒下一个有效的线程 下一个线程在acquireQueued自旋
LockSupport.unpark(s.thread);
}
只有我认为自旋用得很妙吗?以上就是本章的全部内容了。
一定要自己打开源码跟着看一遍,切莫纸上谈兵。
上一篇:线程安全性之有序性和内存屏障
下一篇:线程通信synchronized中的wait/notify、J.U.C Condition的使用和源码分析
书山有路勤为径,学海无涯苦作舟