无锁多线程编程初步(基础部分)

1.volatile
关于volatile可讲述的部分有很多,不过精简的说这个关键字的功能有两点。
     a.volatile修饰的变量对如果有修改,会对任意线程可见。
     b.volatile修饰的变量:
          如果是volatile写,那么它可以保证任何在它之前需要完成的读写都会完成,但是它之后的普通读写不能保证(会提前执行)【volatile读不会提前】。
          如果是volatile读,那么它可以保证后面的普通读写操作不会挪到它前面来执行,但它不能保证它之前的读写不会跑到它后面执行。
2.CAS
CAS操作是一种不可靠的原子操作,它的返回值有可能成功,有可能失败,这要取决于在执行这段代码的时候有没有其它线程竞争。
注意,一般来说,对一个值(整形,浮点,布尔值等)进行CAS操作的时候,这个变量应该是volatile修饰的。
不然的话,每个线程读的有可能都是本地内存的值,这样的话就有可能造成都会成功(因为没有反映到主内存中)
 
另,通过CAS操作可以实现简单的自旋锁。
3.CLH锁与MCS锁
CLH锁和MCS锁都是公平锁,他们的实现思路不同,但是目的都是一样的(能获取锁就获取锁,获取不到就自旋)。
 
CLH锁是通过不断轮训前驱来决定自己是不是获取锁(也即推出死循环)。
而MCS锁则是由前驱来改变自己来退出循环。
 
4.AQS
AQS是全称AbstractQueuedSynchronization
其中的这个queue就是设计者基于CLH进行了改造而实现的。
 
没变的是大体思想还是通过查询自己的前驱来决定自己要不要退出循环。
变得是没有通过轮训而是通过阻塞的形式(LookSupport)来防止不必要的空转。
 
下面依据上面两个来大致说一下原理:
1.查询前驱
既然要通过查询前驱的方式来退出循环,这就涉及到了队列的插入和移除。
以tryAcquire为契机,
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
从上可以看到,tryAcquire成功,则意味着获取到锁(也意味着程序可以顺利进行下去)
如果失败的话,那么就要尝试进入队列了。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这里有两个操作
一个是acquireQueued
另一个是addWaiter
 
当然,第一步是addWaiter也就是进入队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//line 1
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;//line 2
if (pred != null) {//line3
     node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
 
想一下,如果是单线程程序的话,进入队列只需要插入到尾节点就可以了,但是现在是考虑多线程环境,所以你需要综合各种情况。
首先,line1 其实是线程无关的,每个线程都建立了一个Node.
但是line2就不确定了,因为tail节点是线程共享的。
首先查看是否为null,因为如果是null节点,说明队列还没初始化,要进入enq(node)做一些准备工作。
如果不为null,那么就尝试一次(CAS),因为只需要保证CAS成功,那么它就确定是新的tail节点。
如果尝试失败,进入enq,这是一个for(;;)循环。
事实上此处如果把line3所代表的这个if语句删除,直接多个线程都进入enq循环也没有问题。因为这就相当于两个都竞争失败,而竞争失败是无风险的(损失了一点时间,多了不正确的临时属性)。
此处进行如此设计还是考虑,很有可能不会有冲突,这样就省去了进入for循环的麻烦。
enq里面就是竞争后形成顺序的队列。
 
2.阻塞与解锁
既然会被阻塞,如果要退出,该如何并且何时被唤醒是一个重要的问题。
CLH锁会轮训然后确定何时退出,不过此处是进入到了队列后,就休眠了。
休眠被唤醒后,会查看自己是否前驱是头结点,如果是的话,说明轮到自己解锁了。
所以如果退出阻塞状态,一方面看前驱,另一方面要被唤醒。
 
如果是排他锁的话,也就意味着只有别人释放锁它才有机会获得锁。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
 
release成功后,它会找到头结点然后释放头结点的子节点。
如果是排他性锁,释放应该不会有多线程冲突。但是如果是共享锁,那么也可能会出现冲突。
所以做了一些处理之后,通过LockSupport.unpark(s.thread) 唤醒下一个节点。
 
当节点被唤醒后,它从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);
}
}
 
如果它是头结点的下一个节点,那么进入到p==head,
首先设置自己为头结点,然后返回。这样既获取了锁,也退出了阻塞状态。
posted @ 2017-07-17 21:30  lightverse  阅读(625)  评论(0编辑  收藏  举报