让你不再害怕JAVA的锁(一)中我们介绍了java的synchronized锁。
本篇文章将会介绍一下 Lock锁
Lock与synchronized的对比
类别 | synchronized | Lock |
---|---|---|
存在层面 | 语法层面;JVM实现 | API层面,是一个类,JDK实现 |
锁的获取 | A获得锁,B等待 | A获得锁,B可以由多种情况,具体后文介绍 |
锁的释放 | JVM自动释放,不存在死锁 | finally中必须释放,否则容易造成死锁 |
锁的状态 | 无法判断 | 可以判断 |
锁类型 | 可重入,不可中断,非公平 | 可重入,可中断,可公平 |
性能 | 少量线程同步 | 可大量同步 |
竞争资源不激烈时,两者的性能是差不多的;当有大量线程同时竞争,此时Lock的性能要远远优于synchronized
Lock需要用lock与unlock显示指明,并在finally中unlock以防死锁
synchronized是一种悲观锁,当有很多线程竞争锁的时候,会引起cpu频繁的切换上下文环境,导致效率很低。
Lock是一种乐观锁,使用了CAS机制(需要CPU的支持),如果查看Lock源码,会发现一个比较重要的获得锁的方法就是compareAndSetState
Lock与synchronized使用场景对比
一般大部分情况下,两者是都可以使用的。但是在一些非常复杂的同步应用中,建议使用ReentrantLock,尤其是以下两种case:
- 线程在等待一个锁的这段时间需要中断
- 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify到哪个线程,即ReentrantLock控制粒度比synchronized要细
- 需要用到公平锁
ReentrantLock源码分析
ReentrantLock是Lock的一种实现方式,相比与synchronized的非公平锁,Lock是可公平可非公平。
公平锁(Fair)
:加锁前检查是否有排队等待的线程,优先排队等待的线程,有个先来后到
非公平锁(Nonfair)
:加锁时压根就不会考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
ReentrantLock默认的lock方法采用的是非公平锁
下面我们逐步分析一下公平锁
的实现原理
1 abstract static class Sync extends AbstractQueuedSynchronizer
在ReentrantLock内部定义了一个static类型的AbstractQueuedSynchronizer(这个就是传说中的AQS
这个类提供了对操作系统层面线程的一些操作方法的封装调用)的子类Sync,我们看一下AQS的源码,看一下里面究竟有什么东西
private volatile int state;
CAS操作(compareAndSetState函数)其实就是在操作这个volatile类型的state,设置state+1返回true,就说明获取到了锁。
volatile
的作用说白了就是能让所有线程能够获取最新的volatile值,这是什么意思呢?请看下图的说明
我们知道对象的成员跟对象是在堆(即主存)中的,而方法运行是在栈中的,
如果state没有volatile的修饰,线程1和线程2同时执行CAS方法(即expect=0,update=1)来更改state的值为1,线程1更改成功后回写了堆,但是线程2并没有感知到这个变化,还认为expect=0,此时线程2update state也会成功,即与线程1一样,也获取到了锁,这显然是不对了。
如果加了volatile修饰,线程1更改state成功后,其它线程中的state副本就会失效,线程2就会重新从主存load state值,此时在用(expect=0和update=1)的条件去更新时,就会失败,因为此时的state expect是1,就不会获取到锁
2 看一下ReentrantLock带参数的构造方法,我们可以通过传入参数来构造公平的可重入锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
3 ReentrantLock中的lock调用的就是sync中的lock(),如果步骤2中的fair=true,则sync变量被初始化为 Sync的公平锁子类FairSync
4下面我们看一下FairSync中的lock实现
// 把公平锁和非公平锁放一块,我们看一下这俩实现到底有啥不一样
// 公平锁的lock实现
final void lock() {
acquire(1);
}
//非公平锁的lock实现
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
看见了吧,非公平锁上来就进行一次CAS,即上来就进行获取锁的操作,压根就不会考虑排队等待的问题,就没有先来后到这个意思。现在看一下这个acquire(1)是个什么沙雕函数吧 ,acquire是AQS中的一个模版方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
5让我们先看一下非公平锁 NonfairSync中tryAcquire的实现逻辑
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
nonfairTryAcquire(acquires)的源码
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;
}
如果当前状态state=0,就通过CAS(之后我会专门写一篇文章来介绍)操作将state的值update为1,如果成功(即获取锁成功)就执行当前线程。如果当前执行线程就是该线程,就把state++,这也是重入锁的表现
所以这里无需在进行CAS的操作,看见了没这里有个nextc<0的判断,说明锁的重入也是有一定次数的限制的。
我们再回到acquire函数源码,如下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可知,如果tryAcquire失败,就acquireQueued(addWaiter(Node.EXCLUSIVE), arg),如果acquireQueued成功,就acquireQueued
这块没看明白,待之后详细研究
通过上述分析,我们暂时知道了公平锁与非公平锁的实现原理
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)是个什么鬼?
通过Node.EXCLUSIVE参数我们知道这是独占模式,addWaiter源码如下,看看到底干了点啥
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 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;
}
其实就是一个双向链表,获取到链表的最后一个节点(tail)作为新加入节点Node的前驱节点,通过CAS的操作将当前线程的Node节点(其实就是当前线程的一个占位符)
放入链表最后,如果CAS失败,就调用enq不停的重复CAS操作直至Node放入队列成功。
在看一下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
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在分析这个函数的逻辑之前,我们应该知道链表的结构如下
Node链表的结构
链表结构
如果当前节点的前驱节点是head,并且可以通过casupdate state的状态,代表当前节点占有锁。此时就把当前节点设置为head,设置next为null(即从当前链表中剔除) 否则进入等待状态(即将当前线程从线程调度器上摘下)。
示例代码
说了这么多,我们来用实际代码看一下ReentrantLock的使用吧
// 待更新
首先我们需要知道:
synchronized一般与Object的wait 、notify、notifyAll绑定使用,而Lock可以使用Condtion的await,signal和signalAll,一般使用ReentrantLock类作为锁
网友评论