锁是用来作并发最简单的方式,固然其代价也是最高的。内核态的锁的时候须要操做系统进行一次上下文切换,加锁、释放锁会致使比较多的上下文切换和调度延时,等待锁的线程会被挂起直至锁释放。在上下文切换的时候,cpu以前缓存的指令和数据都将失效,对性能有很大的损失。操做系统对多线程的锁进行判断就像两姐妹在为一个玩具在争吵,而后操做系统就是能决定他们谁能拿到玩具的父母,这是很慢的。用户态的锁虽然避免了这些问题,可是其实它们只是在没有真实的竞争时才有效。html
Java在JDK1.5以前都是靠synchronized关键字保证同步的,这种经过使用一致的锁定协议来协调对共享状态的访问,能够确保不管哪一个线程持有守护变量的锁,都采用独占的方式来访问这些变量,若是出现多个线程同时访问锁,那第一些线线程将被挂起,当线程恢复执行时,必须等待其它线程执行完他们的时间片之后才能被调度执行,在挂起和恢复执行过程当中存在着很大的开销。锁还存在着其它一些缺点,当一个线程正在等待锁时,它不能作任何事。若是一个线程在持有锁的状况下被延迟执行,那么全部须要这个锁的线程都没法执行下去。若是被阻塞的线程优先级高,而持有锁的线程优先级低,将会致使优先级反转(Priority Inversion)。java
独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的状况,而且只有在确保其它线程不会形成干扰的状况下执行,会致使其它全部须要锁的线程挂起,等待持有锁的线程释放锁。而另外一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。node
与锁相比,volatile变量是一和更轻量级的同步机制,由于在使用这些变量时不会发生上下文切换和线程调度等操做,可是volatile变量也存在一些局限:不能用于构建原子的复合操做,所以当一个变量依赖旧值时就不能使用volatile变量。(参考:谈谈volatiile)nginx
volatile只能保证变量对各个线程的可见性,但不能保证原子性。为何?见个人另一篇文章:《为何volatile不能保证原子性而Atomic能够?》git
原子操做指的是在一步以内就完成并且不能被中断。原子操做在多线程环境中是线程安全的,无需考虑同步的问题。在java中,下列操做是原子操做:github
问题来了,为何long型赋值不是原子操做呢?例如:算法
1
|
long
foo = 65465498L;
|
实时上java会分两步写入这个long变量,先写32位,再写后32位。这样就线程不安全了。若是改为下面的就线程安全了:数据库
1
|
private
volatile
long
foo;
|
由于volatile内部已经作了synchronized.编程
要实现无锁(lock-free)的非阻塞算法有多种实现方法,其中CAS(比较与交换,Compare and swap)是一种有名的无锁算法。CAS, CPU指令,在大多数处理器架构,包括IA3二、Space中采用的都是CAS指令,CAS的语义是“我认为V的值应该为A,若是是,那么将V的值更新为B,不然不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。CAS无锁算法的C实现以下:api
1
2
3
4
5
6
7
8
9
|
int
compare_and_swap (
int
* reg,
int
oldval,
int
newval)
{
ATOMIC();
int
old_reg_val = *reg;
if
(old_reg_val == oldval)
*reg = newval;
END_ATOMIC();
return
old_reg_val;
}
|
CAS比较与交换的伪代码能够表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
(上图的解释:CPU去更新一个值,但若是想改的值再也不是原来的值,操做就失败,由于很明显,有其它操做先改变了这个值。)
就是指当二者进行比较时,若是相等,则证实共享数据没有被修改,替换成新值,而后继续往下运行;若是不相等,说明共享数据已经被修改,放弃已经所作的操做,而后从新执行刚才的操做。容易看出 CAS 操做是基于共享数据不会被修改的假设,采用了相似于数据库的 commit-retry 的模式。当同步冲突出现的机会不多时,这种假设能带来较大的性能提高。
前面说过了,CAS(比较并交换)是CPU指令级的操做,只有一步原子操做,因此很是快。并且CAS避免了请求操做系统来裁定锁的问题,不用麻烦操做系统,直接在CPU内部就搞定了。但CAS就没有开销了吗?不!有cache miss的状况。这个问题比较复杂,首先须要了解CPU的硬件体系结构:
上图能够看到一个8核CPU计算机系统,每一个CPU有cache(CPU内部的高速缓存,寄存器),管芯内还带有一个互联模块,使管芯内的两个核能够互相通讯。在图中央的系统互联模块可让四个管芯相互通讯,而且将管芯与主存链接起来。数据以“缓存线”为单位在系统中传输,“缓存线”对应于内存中一个 2 的幂大小的字节块,大小一般为 32 到 256 字节之间。当 CPU 从内存中读取一个变量到它的寄存器中时,必须首先将包含了该变量的缓存线读取到 CPU 高速缓存。一样地,CPU 将寄存器中的一个值存储到内存时,不只必须将包含了该值的缓存线读到 CPU 高速缓存,还必须确保没有其余 CPU 拥有该缓存线的拷贝。
好比,若是 CPU0 在对一个变量执行“比较并交换”(CAS)操做,而该变量所在的缓存线在 CPU7 的高速缓存中,就会发生如下通过简化的事件序列:
以上是刷新不一样CPU缓存的开销。最好状况下的 CAS 操做消耗大概 40 纳秒,超过 60 个时钟周期。这里的“最好状况”是指对某一个变量执行 CAS 操做的 CPU 正好是最后一个操做该变量的CPU,因此对应的缓存线已经在 CPU 的高速缓存中了,相似地,最好状况下的锁操做(一个“round trip 对”包括获取锁和随后的释放锁)消耗超过 60 纳秒,超过 100 个时钟周期。这里的“最好状况”意味着用于表示锁的数据结构已经在获取和释放锁的 CPU 所属的高速缓存中了。锁操做比 CAS 操做更加耗时,是因深刻理解并行编程
为锁操做的数据结构中须要两个原子操做。缓存未命中消耗大概 140 纳秒,超过 200 个时钟周期。须要在存储新值时查询变量的旧值的 CAS 操做,消耗大概 300 纳秒,超过 500 个时钟周期。想一想这个,在执行一次 CAS 操做的时间里,CPU 能够执行 500 条普通指令。这代表了细粒度锁的局限性。
如下是cache miss cas 和lock的性能对比:
在JDK1.5以前,若是不编写明确的代码就没法执行CAS操做,在JDK1.5中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS的操做,而且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令,若是处理器/CPU不支持CAS指令,那么JVM将使用自旋锁。所以,值得注意的是,CAS解决方案与平台/编译器紧密相关(好比x86架构下其对应的汇编指令是lock cmpxchg,若是想要64Bit的交换,则应使用lock cmpxchg8b。在.NET中咱们可使用Interlocked.CompareExchange函数)。
在原子类变量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操做,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。
Java 1.6中AtomicLong.incrementAndGet()的实现源码为:
因而可知,AtomicLong.incrementAndGet的实现用了乐观锁技术,调用了sun.misc.Unsafe类库里面的 CAS算法,用CPU指令来实现无锁自增。因此,AtomicLong.incrementAndGet的自增比用synchronized的锁效率倍增。
1
2
3
4
5
6
7
8
9
10
11
12
|
public
final
int
getAndIncrement() {
for
(;;) {
int
current = get();
int
next = current +
1
;
if
(compareAndSet(current, next))
return
current;
}
}
public
final
boolean
compareAndSet(
int
expect,
int
update) {
return
unsafe.compareAndSwapInt(
this
, valueOffset, expect, update);
}
|
下面是测试代码:能够看到用AtomicLong.incrementAndGet的性能比用synchronized高出几倍。
下面是比非阻塞自增稍微复杂一点的CAS的例子:非阻塞堆栈/ConcurrentStack
。ConcurrentStack
中的 push()
和 pop()
操做在结构上与NonblockingCounter
上类似,只是作的工做有些冒险,但愿在 “提交” 工做的时候,底层假设没有失效。push()
方法观察当前最顶的节点,构建一个新节点放在堆栈上,而后,若是最顶端的节点在初始观察以后没有变化,那么就安装新节点。若是 CAS 失败,意味着另外一个线程已经修改了堆栈,那么过程就会从新开始。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public
class
ConcurrentStack<E> {
AtomicReference<Node<E>> head =
new
AtomicReference<Node<E>>();
public
void
push(E item) {
Node<E> newHead =
new
Node<E>(item);
Node<E> oldHead;
do
{
oldHead = head.get();
newHead.next = oldHead;
}
while
(!head.compareAndSet(oldHead, newHead));
}
public
E pop() {
Node<E> oldHead;
Node<E> newHead;
do
{
oldHead = head.get();
if
(oldHead ==
null
)
return
null
;
newHead = oldHead.next;
}
while
(!head.compareAndSet(oldHead,newHead));
return
oldHead.item;
}
static
class
Node<E> {
final
E item;
Node<E> next;
public
Node(E item) {
this
.item = item; }
}
}
|
在轻度到中度的争用状况下,非阻塞算法的性能会超越阻塞算法,由于 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及线程挂起和上下文切换,只多了几个循环迭代。没有争用的 CAS 要比没有争用的锁便宜得多(这句话确定是真的,由于没有争用的锁涉及 CAS 加上额外的处理),而争用的 CAS 比争用的锁获取涉及更短的延迟。
在高度争用的状况下(即有多个线程不断争用一个内存位置的时候),基于锁的算法开始提供比非阻塞算法更好的吞吐率,由于当线程阻塞时,它就会中止争用,耐心地等候轮到本身,从而避免了进一步争用。可是,这么高的争用程度并不常见,由于多数时候,线程会把线程本地的计算与争用共享数据的操做分开,从而给其余线程使用共享数据的机会。
以上的示例(自增计数器和堆栈)都是很是简单的非阻塞算法,一旦掌握了在循环中使用 CAS,就能够容易地模仿它们。对于更复杂的数据结构,非阻塞算法要比这些简单示例复杂得多,由于修改链表、树或哈希表可能涉及对多个指针的更新。CAS 支持对单一指针的原子性条件更新,可是不支持两个以上的指针。因此,要构建一个非阻塞的链表、树或哈希表,须要找到一种方式,能够用 CAS 更新多个指针,同时不会让数据结构处于不一致的状态。
在链表的尾部插入元素,一般涉及对两个指针的更新:“尾” 指针老是指向列表中的最后一个元素,“下一个” 指针从过去的最后一个元素指向新插入的元素。由于须要更新两个指针,因此须要两个 CAS。在独立的 CAS 中更新两个指针带来了两个须要考虑的潜在问题:若是第一个 CAS 成功,而第二个 CAS 失败,会发生什么?若是其余线程在第一个和第二个 CAS 之间企图访问链表,会发生什么?
对于非复杂数据结构,构建非阻塞算法的 “技巧” 是确保数据结构总处于一致的状态(甚至包括在线程开始修改数据结构和它完成修改之间),还要确保其余线程不只可以判断出第一个线程已经完成了更新仍是处在更新的中途,还可以判断出若是第一个线程走向 AWOL,完成更新还须要什么操做。若是线程发现了处在更新中途的数据结构,它就能够 “帮助” 正在执行更新的线程完成更新,而后再进行本身的操做。当第一个线程回来试图完成本身的更新时,会发现再也不须要了,返回便可,由于 CAS 会检测到帮助线程的干预(在这种状况下,是建设性的干预)。
这种 “帮助邻居” 的要求,对于让数据结构免受单个线程失败的影响,是必需的。若是线程发现数据结构正处在被其余线程更新的中途,而后就等候其余线程完成更新,那么若是其余线程在操做中途失败,这个线程就可能永远等候下去。即便不出现故障,这种方式也会提供糟糕的性能,由于新到达的线程必须放弃处理器,致使上下文切换,或者等到本身的时间片过时(而这更糟)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public
class
LinkedQueue <E> {
private
static
class
Node <E> {
final
E item;
final
AtomicReference<Node<E>> next;
Node(E item, Node<E> next) {
this
.item = item;
this
.next =
new
AtomicReference<Node<E>>(next);
}
}
private
AtomicReference<Node<E>> head
=
new
AtomicReference<Node<E>>(
new
Node<E>(
null
,
null
));
private
AtomicReference<Node<E>> tail = head;
public
boolean
put(E item) {
Node<E> newNode =
new
Node<E>(item,
null
);
while
(
true
) {
Node<E> curTail = tail.get();
Node<E> residue = curTail.next.get();
if
(curTail == tail.get()) {
if
(residue ==
null
)
/* A */
{
if
(curTail.next.compareAndSet(
null
, newNode))
/* C */
{
tail.compareAndSet(curTail, newNode)
/* D */
;
return
true
;
}
}
else
{
tail.compareAndSet(curTail, residue)
/* B */
;
}
}
}
}
}
|
具体算法相见IBM Developerworks
Java5中的ConcurrentHashMap,线程安全,设计巧妙,用桶粒度的锁,避免了put和get中对整个map的锁定,尤为在get中,只对一个HashEntry作锁定操做,性能提高是显而易见的。
具体实现中使用了锁分离机制,在这个帖子中有很是详细的讨论。这里有关于Java内存模型结合ConcurrentHashMap的分析。如下是JDK6的ConcurrentHashMap的源码:
ConcurrentLinkedQueue也是一样使用了CAS指令,但其性能并不高由于太多CAS操做。其源码以下:
服务端编程的3大性能杀手:一、大量线程致使的线程切换开销。二、锁。三、非必要的内存拷贝。在高并发下,对于纯内存操做来讲,单线程是要比多线程快的, 能够比较一下多线程程序在压力测试下cpu的sy和ni百分比。高并发环境下要实现高吞吐量和线程安全,两个思路:一个是用优化的锁实现,一个是lock-free的无锁结构。但非阻塞算法要比基于锁的算法复杂得多。开发非阻塞算法是至关专业的训练,并且要证实算法的正确也极为困难,不只和具体的目标机器平台和编译器相关,并且须要复杂的技巧和严格的测试。虽然Lock-Free编程很是困难,可是它一般能够带来比基于锁编程更高的吞吐量。因此Lock-Free编程是大有前途的技术。它在线程停止、优先级倒置以及信号安全等方面都有着良好的表现。
另外,在设计思路上除了尽可能减小资源争用之外,还能够借鉴nginx/node.js等单线程大循环的机制,用单线程或CPU数相同的线程开辟大的队列,并发的时候任务压入队列,线程轮询而后一个个顺序执行。因为每一个都采用异步I/O,没有阻塞线程。这个大队列可使用RabbitMQueue,或是JDK的同步队列(性能稍差),或是使用Disruptor无锁队列(Java)。任务处理能够所有放在内存(多级缓存、读写分离、ConcurrentHashMap、甚至分布式缓存Redis)中进行增删改查。最后用Quarz维护定时把缓存数据同步到DB中。固然,这只是中小型系统的思路,若是是大型分布式系统会很是复杂,须要分而治理,用SOA的思路,参考这篇文章的图。(注:Redis是单线程的纯内存数据库,单线程无需锁,而Memcache是多线程的带CAS算法,二者都使用epoll,no-blocking io)
若是深刻 JVM 和操做系统,会发现非阻塞算法无处不在。垃圾收集器使用非阻塞算法加快并发和平行的垃圾搜集;调度器使用非阻塞算法有效地调度线程和进程,实现内在锁。在 Mustang(Java 6.0)中,基于锁的 SynchronousQueue
算法被新的非阻塞版本代替。不多有开发人员会直接使用 SynchronousQueue
,可是经过 Executors.newCachedThreadPool()
工厂构建的线程池用它做为工做队列。比较缓存线程池性能的对比测试显示,新的非阻塞同步队列实现提供了几乎是当前实现 3 倍的速度。在 Mustang 的后续版本(代码名称为 Dolphin)中,已经规划了进一步的改进。