一、基于锁的编程的缺点
在传统的多线程编程中,多线程之间一般用各种锁的机制来保证正确的对共享资源进行访问和操作。只要需要共享某些数据,就应当将对它的访问串行化。比如像++count(count是整型变量)这样的简单操作也得加锁,因为即便是增量操作这样的操作,实际上也是分三步进行的:读、改、写(回)。
在使用锁机制的过程中,即便在锁的粒度、负载、竞争、死锁等需要重点控制的方面解决的很好,也无法彻底避免这种机制的如下一些缺点:
1. 锁机制会引起线程的阻塞,对于没有能占用到锁的线程或者进程,将一直等待到锁的占有者释放锁资源后才能继续执行,而等待时间理论上是不可设置和预估的。
2. 申请和释放锁的操作,增加了很多访问共享资源的消耗,尤其是在锁竞争很严重的时候。
3. 现有实现的各种锁机制,都不能很好的避免编程开发者设计实现的程序出现死锁或者活锁的可能。
4. 难以调试。
二、无锁编程的定义
无锁编程按字面最直观的理解是不使用锁的情况下实现多线程之间对变量同步和访问的一种程序设计实现方案。一个锁无关的程序能够确保它所有线程中至少有一个能够继续往下执行,而有些线程可能会被的延迟。然而在整体上,在某个时刻至少有一个线程能够执行下去。作为整体进程总是在前进的,尽管有些线程的进度可能没有其它线程进行的快。
三、无锁编程的原理
无锁编程具体使用技术方法包括:原子操作(atomic operations), 内存栅栏(memory barriers), 内存顺序冲突(memory order), 指令序列一致性(sequential consistency)和顺ABA现象等等。其中最重要的是原子操作,其中最常用的原子操作又是CAS(COMPARE AND SWAP)算法。
CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
在java语言中在java.util.concurrent包中大量实现都是建立在基于CAS实现Lock-Free算法上。java.util.concurrent.atomic提供了基于CAS实现的若干类:
- AtomicBoolean – 原子布尔
- AtomicInteger – 原子整型
- AtomicIntegerArray – 原子整型数组
- AtomicLong – 原子长整型
- AtomicLongArray – 原子长整型数组
- AtomicReference – 原子引用
- AtomicReferenceArray – 原子引用数组
- AtomicMarkableReference – 原子标记引用
- AtomicStampedReference – 原子时间戳记引用(解决ABA现象)
- AtomicIntegerFieldUpdater – 用来包裹对整形 volatile 域的原子操作
- AtomicLongFieldUpdater – 用来包裹对长整型 volatile 域的原子操作
- AtomicReferenceFieldUpdater – 用来包裹对对象 volatile 域的原子操作
引入这些类的主要目的就是为了实现Lock-Free算法和相关数据结构,以incrementAndGet这个方法为例:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在这里使用CAS原子操作,每次读取数据数值后将此数值和+1后的结果进行CAS操作,如果成功就返回结果,否则重试到成功为止。其中compareAndSet是java中实现的CAS函数,在java语言中的实现,是借助JNI机制来调用汇编实现的。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
unsafe.compareAndSwapInt是个本地方法调用,对应的x86处理器的jni源码。
四、无锁编程(同步)栈和队列的实现
1、无锁栈
package concurrenttest;
import java.util.concurrent.atomic.AtomicReference;
public class ConcurrentStack<E> {
AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();
public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
while (true) {
oldHead = top.get();
newHead.next = oldHead;
if (top.compareAndSet(oldHead, newHead)) {
return;
}
}
}
public E pop() {
while (true) {
Node<E> oldHead = top.get();
if (oldHead == null) {
return null;
}
Node<E> newHead = oldHead.next;
if (top.compareAndSet(oldHead, newHead)) {
return oldHead.item;
}
}
}
private static class Node<E> {
public final E item;
public Node<E> next;
public Node(E item) {
this.item = item;
}
}
}
2、无锁队列(ConcurrentLinkedQueue)
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
五、无锁编程的注意事项
循环CAS操作对时会大量占用cpu,对系统时间的开销也是很大。这也是基于循环CAS实现的各种自旋锁不适合做操作和等待时间太长的并发操作的原因。