关于Java中的锁 在面试和工作中经常用到的一些知识
一、Java中锁的种类
1.synchronized
synchronized 的底层 利用的是计算机底层实现的,通过阅读.class的反编译内容可以发现 在synchronized定义的同步代码块的前后会加入 moniterenter 和 moniterexit
2.实现接口Lock
通常用的就是ReentrantLock 点开可以发现它实现了Lock接口。使用的方法很简单,创建ReentrantLock的对象rLock,然后在需要保证原子性的代码前后分别调用rLock的lock方法和unlock方法。
2.1 Lock原理
实现Lock的接口,然后自己写个CAS+volatile或者其他的方式来实现锁;
2.2 ReetrantLock原理
这里有两种区别,一种是非公平锁,一种是公平锁。他两者的区别可以看ReetrantLock的两个内部类 NonfairSync 和 FariSync ,两个内部类都继承了Sync。两者的主要区别其实是lock方法和tryAcquire方法。
先从简单的NonfairSync开始说起。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
上面这段是NonfairSync里的lock方法代码。
其中compareAndSetState 就是所说的cas操作,第一个参数0代表原值,第二个参数1代表要赋予的值,解释成人类语言就是——看下state的值是不是0,如果是0就把0修改成1,并返回修改成功还是修改失败。这个操作底层调用的是unsafe方法,可以保证其判断和修改是原子性的操作。当第一个线程进来的时候 该方法肯定返回的是true 。然后setExclusiveOwnerThread 设置一下当前正在占着锁的线程,之后就可以执行真正的业务代码了。
若compareAndSetState返回false,则表明当前锁已经有其他线程占用着了,于是执行acquire(1)方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)的方法里做了两件事,1.再次尝试进行cas操作,若成功,则直接返回true。2.若不成功,则要看看目前拥有锁的线程和当前线程是不是同一个线程,若是同一个,则将state的值加1,再set进去。同样也是cas操作,并返回true,否则返回false。这里代码表示了ReetrantLock是可重入锁,既 同一个线程可以多次获取到他已经获取到的锁。代码如下:
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;
}
回过头看acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这个方法,addWaiter相当于创建一个属于当前线程的Node对象,Node.EXCLUSIVE 就是null。
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;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上面就是创建一个当前线程的Node节点,用CAS插入到Node节点的尾部。
由此可以看出,所有在竞争当前锁的非当前线程都会创建一个node,并按顺序拍成了一个Node里链表。
之后就是将创建好的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);
}
}
一上来就看见一个循环,循环里做的事情 就是:1.判断下当前的头节点是不是传入进来的节点的前一个点。同时 利用CAS是否能竞争到锁,成功怎返回false。2.是否应该停止当前线程,先查看当前节点的前置节点是否是signal状态,若是则将当前线程挂起,使用LockSupport.park的方法。若不是,则向前遍历节点,直到找到一个节点状态<0的节点,将找到的节点的next指向当前节点。
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) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
以上就是NonfairSync原理。
再来讲一下FairSync原理。
其实看了NonfairSync的原理后FairSync就很简单了,两者的区别就在tryAcquire上
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;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
可以看到区别就在Cas的操作前,先进行了 hasQueuedPredecessors的判断一下当前node链表的头节点的下一个节点所属的线程是否是当前线程,是则返回true。代码如下:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这就保证了,只有正在运行节点的后一个节点可以通过CAS来竞争锁。从而实现了公平锁。