主要内容

1. 线程同步标准的处理方法:上锁

2. 锁的问题

3. 硬件同步原语CAS

4. 使用CAS实现计数器

5. Lock-free和 wait-free 算法

6. Atomic原子变量类

十五年前,多处理器系统是高度专业化的系统,通常耗资数十万美元(其中大多数具有两到四个处理器)。

如今,多处理器系统既便宜又丰富,几乎主流的微处理器都内置了对多处理器的支持,很多能够支持数十或数百个处理器。

为了充分利用多处理器系统的性能,通常使用多个线程来构建应用程序。

但是,任何一个写并发应用的人都会告诉你,仅仅把工作分散在多个线程中处理不足以充分利用硬件的性能,你必须保证你的线程大部分时间都在工作,而不是在等待工作,或者在等待共享数据上的锁。

问题:线程之间的协作

很少有应用可以不依赖线程协作而实现真正的并行化。

例如一个线程池,其中的任务通常是彼此独立的被执行,互不干扰。一般会使用一个工作队列来维护这些任务,那么从工作队列中删除任务或向其中添加任务的过程必须是线程安全的,这意味着需要协调队列头部、尾部、以及节点之间的链接指针。这种协调工作是麻烦的根源。

标准的处理方法:上锁

在Java中,协调多线程访问共享变量的传统方式是同步,

通过同步(synchronized关键字)可以保证只有持有锁的线程才可以访问共享变量,此外可以确保持有锁的线程对这些变量的访问具有独占访问权,且线程对共享变量的改变对于其他后来的线程是可见的。

同步的缺点是,当锁的竞争激烈时(多个线程频繁的尝试获取锁),吞吐量会受到影响,同步的代价会非常高。

基于锁的算法另一个问题是如果一个持有锁的线程被延迟(由于page fault、调度延迟、或其他异常),那么其他正在等待该锁的线程都将无法执行。

volatile变量也可以用于存储共享变量,其成本比synchronized要低。

但是它有局限性,虽然volatile变量的修改对其他线程是立即可见的,但是它无法呈现原子操作的read-modify-write操作序列,

这意味着,volatile变量无法实现可靠的互斥锁或计数器。

用锁实现计数器和互斥体

考虑开发一个线程安全的计数器类,该类公开get()、increment()和decrement()操作。清单1展示了使用同步锁实现此类。

请注意,所有方法,甚至get(),都是同步的,以保证不会丢失任何更新,并且所有线程都可以看到计数器的最新值。

Listing 1. A synchronized counter class

public class SynchronizedCounter {

private int value;

public synchronized int getValue() { return value; }

public synchronized int increment() { return ++value; }

public synchronized int decrement() { return --value; }

}

increment() 和 decrement()都是原子的read-modify-write操作,为了安全的递增计数器,你必须取出当前值,然后对它加1,最后再把新值写回。

所有这些操作都将作为一个单独的操作完成,中途不能被其他线程打断。

否则,如果两个线程同时进行increment操作,意外的操作交错会导致计数器只被递增了一次,而不是两次。(请注意,通过把变量设置为volatile,不能可靠的实现以上操作)

原子的read-modify-write组合操作出现在很多并发算法中。下面清单2中的代码实现了一个简单的互斥体(Mutex,Mutual exclusion的简写)。acquire()方法就是原子的read-modify-write操作。

要获取这个互斥体,你必须确保没有其他线程占用它(curOwner==null),成功获取后标识你已经持有该锁(curOwner = Thread.currentThread()),这样其他线程就不可能再进入并修改curOwner变量。

Listing 2. A synchronized mutex class

public class SynchronizedMutex {

private Thread curOwner = null;

public synchronized void acquire() throws InterruptedException {

if (Thread.interrupted()) throw new InterruptedException();

while (curOwner != null)

wait();

curOwner = Thread.currentThread();

}

public synchronized void release() {

if (curOwner == Thread.currentThread()) {

curOwner = null;

notify();

} else

throw new IllegalStateException("not owner of mutex");

}

}

清单1中的计数器类在没有竞争或竞争很少的情况下可以可靠的工作。

然而,在竞争激烈时性能将大幅下降,因为JVM将花费更多的时间处理线程调度以及管理竞争,而花费较少的时间进行实际工作,如增加计数器。

锁的问题

如果一个线程尝试获取一个正在被其他线程占用的锁,该线程会一直阻塞直到锁被其他线程释放。

这种方式有明显的缺点,当线程被阻塞时它不能做任何事情。

如果被阻塞的线程是较高优先级的任务,那么后果是灾难性的(这种危险被称为优先级倒置,priority inversion)。

使用锁还有其他一些风险,例如死锁(当以不一致的顺序获取多个锁时可能会发生死锁)。

即使没有这样的危险,锁也只是相对粗粒度的协调机制。

因此,对于管理简单的操作(例如计数器或互斥体)来说,锁是相当“重”的。

如果有一个更细粒度的机制能够可靠地管理对变量的并发更新,那将是极好的。

幸运的是,大多数现代处理器都有这种轻量级的机制。

硬件同步原语

如前所述,大多数现代处理都支持多处理器,这种支持除了基本的多个处理器共享外设和主存储器的能力,它通常还包括对指令集的增强,以支持多处理的特殊要求。特别是,几乎每个现代处理器都具有用于更新共享变量的指令,该指令可以检测或阻止来自其他处理器的并发访问。

Compare and swap (CAS)

第一批支持并发的处理器提供了原子的test-and-set操作,这些操作通常在一个bit上进行(非0即1)。但是当前主流的处理器(包括Intel和Sparc处理器)最常用的方法是实现一个被称为compare-and-swap(CAS)的原语(32-bit的字段)。(在Intel处理器上,CAS是由cmpxchg指令系列实现的。PowerPC处理器有一对"load and reserve" 和 "store conditional"的指令达到同样的效果)

CAS包括三个操作对象-内存位置(V),预期的旧值(A)和新的值(B)。

如果该位置的值V与预期的旧值A匹配,则处理器将原子地将该位置更新为新值B,否则它将不执行任何操作。

无论哪种情况,它都会返回CAS指令之前该位置的值V。 (CAS的某些版本会简单地返回CAS是否成功,而不获取当前值。)

CAS表示:“我认为位置V应该有值A;如果有,则将B放入其中,否则,不要改变它,但要告诉我现在有什么值。”

CAS通常的使用方法是,从地址V读取值A,然后对A执行多次计算得到新值B,最后使用CAS指令将位置V的值从A变为B。

如果该位置V同时没有被其他处理器更新,那么CAS就会成功。

像CAS这样的指令允许程序执行read-modify-write序列,而不必担心同时有另一个线程修改变量,因为如果另一个线程确实修改了变量,则CAS会检测到该变量(并失败),并且程序可以重试该操作。

清单3,通过synchronized模拟了CAS的内部逻辑。(不包括性能模拟,也没办法模拟,因为CAS的价值就在于它是在硬件中实现的,非常轻量级。)

Listing 3. the behavior (but not performance) of compare-and-swap

public class SimulatedCAS {

private int value;

public synchronized int getValue() { return value; }

public synchronized int compareAndSwap(int expectedValue, int newValue) {

int oldValue = value;

if (value == expectedValue)

value = newValue;

return oldValue;

}

}

使用CAS实现计数器

基于CAS的并发算法称为lock-free,因为线程不必等待锁(有时称为互斥体或临界区,术语因实现平台而异)。

无论CAS操作成功还是失败,它都可以在预期的时间内完成。如果CAS失败,则调用者可以重试CAS操作或采取其他合适措施。

清单4中使用CAS重写了计数器类:

Listing 4. Implementing a counter with compare-and-swap

public class CasCounter {

private SimulatedCAS value;

public int getValue() {

return value.getValue();

}

public int increment() {

int oldValue = value.getValue();

while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue){ //不断重试

oldValue = value.getValue();

}

return oldValue + 1;

}

}

Lock-free和 wait-free 算法

wait-free算法保证每个线程都在执行(make progress)。相反的,lock-free算法要求至少有一个线程能取得进展(make progress)。

可见,wait-free比lock-free的要求更苛刻。

在过去的15年中,人们对wait-free和lock-free算法(也称为非阻塞算法)进行了大量研究,并且发现了许多常见数据结构的非阻塞算法实现。

非阻塞算法在操作系统和JVM级别广泛用于诸如线程和进程调度之类的任务。

尽管实现起来较为复杂,但与基于锁的替代方法相比,它们具有许多优点,

避免了诸如优先级反转和死锁之类的危险,竞争成本更低,并且协调发生在更细粒度级别,从而实现了更高程度的并行性。

原子变量类

在JDK5之前,要实现wait-free、lock-free的算法必须通过native方法。但是,在JDK5中增加了java.util.concurrent.atomic原子包后,情况发生了变化。

atomic包提供了多种原子变量类(AtomicInteger; AtomicLong; AtomicReference; AtomicBoolean等)。

原子变量类都暴露了一个compare-and-set原语(类似compare-and-swap),它使用了平台上可用的最快的原生结构,具体实现方案因平台而异(可能是compare-and-swap, load linked/store conditional, 或者最坏的情况使用 spin locks)。

可以将原子变量类视为volatile变量的泛化,它扩展了volatile变量的概念以支持原子的compare-and-set更新。

原子变量的读写与volatile变量的读写具有相同的内存语义。

尽管原子变量类或许看起来像清单1中的示例,但是他们的相似只是表面上的。

在幕后,对原子变量的操作变成了平台提供的硬件原语,例如compare-and-swap。

细粒度意味着更轻量

优化并发应用的一个常用技术是减少锁对象的粒度,可以让更多的锁获取从竞争的变成非竞争的。

把锁变成原子变量也达到了同样的效果,通过切换到更小粒度的协调机制,减少有竞争的操作,以提升系统吞吐量。

java.util.concurrent包中的原子变量

juc包中几乎所有的类都直接或间接的使用了原子变量,而不是synchronized。

例如ConcurrentLinkedQueue类直接使用原子变量类实现了wait-free算法,

再比如ConcurrentHashMap类在需要的地方使用ReentrantLock上锁,而ReentrantLock使用原子变量类维护等待锁的线程队列。

如果没有JDK5的改进,这些类就无法实现,JDK5暴露了一个接口让类库可以使用硬件级的同步原语。而原子变量类以及juc中的其他类又把这些特性暴露给了用户类。

使用原子变量实现更高的吞吐量

清单5中分别使用同步和CAS实现了伪随机数生成器(PRNG)。要注意的是CAS必须在循环中执行,因为它在成功之前可能会失败一次或多次,这几乎是CAS的使用范式。

Listing 5. Implementing a thread-safe PRNG with synchronization and atomic variables

public class PseudoRandomUsingSynch implements PseudoRandom {

private int seed;

public PseudoRandomUsingSynch(int s) { seed = s; }

public synchronized int nextInt(int n) {

int s = seed;

seed = Util.calculateNext(seed);

return s % n;

}

}

public class PseudoRandomUsingAtomic implements PseudoRandom {

private final AtomicInteger seed;

public PseudoRandomUsingAtomic(int s) {

seed = new AtomicInteger(s);

}

public int nextInt(int n) {

for (;;) {

int s = seed.get();

int nexts = Util.calculateNext(s);

if (seed.compareAndSet(s, nexts))

return s % n;

}

}

}

下面的两张图分别显示了在8路Ultrasparc3和单核的Pentium 4上的线程数与随机数生成器的吞吐量关系。

你会看到,原子变量(ATOMIC曲线)相对于ReentrantLock(LOCK曲线)有了进一步改进,后者相比同步(SYNC曲线)已经取得了很大改进。

由于每个工作单元的工作量很少,因此下面的图形可能低估了原子变量与ReentrantLock相比在伸缩性方便的优势。

大多数用户不大可能使用原子变量自己实现非阻塞算法,他们更应该使用java.util.concurrent中提供的版本,例如ConcurrentLinkedQueue。

如果你想知道与之前的JDK中的类相比juc中的类的性能提升来自何处?那就是使用了原子变量类开放的更细粒度、硬件级并发原语。

另外,开发人员可以直接将原子变量用作共享计数器、序列号生成器以及其他独立共享变量的高性能替代品,否则必须通过同步来保护它们。

总结

JDK 5.0在高性能并发的开发上迈出了一大步。它在内部暴露新的低层协调原语,并提供了一组公共的原子变量类。现在,你可以使用Java语言开发第一个wait-free,lock-free的算法了。

不过,java.util.concurrent中的类都是基于这些原子变量工具构建的,与之前类似功能的类相比,在性能上有了质的飞跃,你可以直接使用他们。

尽管你可能永远不会直接使用原子变量,但是他们仍然值得我们为其欢呼。

译者注

这篇文章是Brian Goetz发表于2004年,即JDK5刚刚发布之后,作为Java布道者第一时间对JDK5的新特性做了很透彻的说明。

Brian Goetz是Java语言的架构师,是Lambda项目的主导者,也是《Java Concurrency in Practice》作者。

参考:

java+cas实现类_像宝石一样的Java原子类-基于CAS实现相关推荐

  1. java rhino js类_让Rhino JS看Java类

    我正在玩 Rhino,我已经成功使用了stdlib中的Java类,但没有使用我编译的Java代码. 例如,这工作正常: print(new java.util.Date()); 但是使用NanoHTT ...

  2. java自动生成类_自动生成优化的Java类专业知识

    java自动生成类 如果您今年访问过JavaOne,您可能已经参加了我的演讲"如何从数据库生成定制的Java 8代码". 在那次演讲中,我展示了如何使用Speedment Open ...

  3. java引用公共类_使用键引用从Java公共类获取值 - java

    我们有一个Java公共类, public class Test { public class ob1 { public static final String test = "T1T1&qu ...

  4. 原子自增_多线程系列-(六)原子类与CAS(了解即可)

    原子类: ActomicInteger:1.常用方法:getAndSetgetAndAddCompareAndSet(预期值.更新值)incrementAndGet2.用处:提供原子操作来进行Inte ...

  5. 怎么查看java的引用类_一段代码看 Java 引用类型

    Java 中的操作数(不知道叫什么,相对于 bytecode 而言,类似 CPU 的操作码和操作数)分为值类型和引用类型: 值类型就是直接存储最终数值的,如 char, int, float, dou ...

  6. Java基础-原子类、CAS

    1.什么是原子类 原子类的作用和锁类似,是为了保证并发情况下的线程安全.不过原子类相比锁,有一定优势 粒度更细:他锁的范围更小 效率更高:相比于锁,效率更高,除了高度竞争的情况 2.6类原子类 Ato ...

  7. Java原子类中CAS的底层实现,java高级面试笔试题

    我总结出了很多互联网公司的面试题及答案,并整理成了文档,以及各种学习的进阶学习资料,免费分享给大家. 扫描二维码或搜索下图红色VX号,加VX好友,拉你进[程序员面试学习交流群]免费领取.也欢迎各位一起 ...

  8. java如何创造一个整数的类_【技术干货】Java 面试宝典:Java 基础部分(1)

    原标题:[技术干货]Java 面试宝典:Java 基础部分(1) Java基础部分: 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的 ...

  9. java 文本工具类_干货:排名前16的Java工具类

    原标题:干货:排名前16的Java工具类 作者丨Java技术栈 https://www.jianshu.com/p/9e937d178203 在Java中,工具类定义了一组公共方法,这篇文章将介绍Ja ...

最新文章

  1. oracle sql 导入mysql数据库备份_Oracle 备份、导入数据库命令
  2. Python之woe:woe库的简介、安装、使用方法之详细攻略
  3. 用ajax技术实现无闪烁定时刷新页面
  4. c++ char 转 string_4.2String类
  5. 编程学习笔记(第一篇)面向对象技术高级课程:绪论-软件开发方法的演化与最新趋势(1)...
  6. 《转》程序员必须知道的10大基础实用算法及其讲解
  7. java百度地图添加标注_调取百度地图接口,实现取自己的实时位置,然后可以在百度地图上添加信息标注...
  8. SNI: 实现多域名虚拟主机的SSL/TLS认证
  9. java将异常输出到日志_【ThinkingInJava】25、将异常输出记录到日志
  10. .NET 技术社区之我见(中文篇)
  11. 大数据技术的特点有哪些
  12. Codeforces Round #772 (Div. 2) C. Differential Sorting(思维+构造)
  13. Egret入门学习日记 --- 第十七篇(书中 7.4~8.2节 内容)
  14. 二阶无源低通滤波器幅频特性曲线_二阶无源滤波器
  15. elasticsearch,使用normalizer优化keyword字段的查询
  16. 美军回应网传UFO:视频为真 现有人类技术无法达到
  17. eclipse启动失败,提示“发生了错误,请参阅日志文件.log
  18. 地理地貌3D打印案例
  19. 进化算法EA——多对象优化遗传(MOO),差分进化法(DE),遗传编程(GP)
  20. 阿里薪资谈判技巧_如何像专业人士一样处理技术职业中的薪资谈判

热门文章

  1. aloha_与云共享Aloha精神
  2. 使用POI批量导出Excel文件(SSM)
  3. 你知道CAD绘图软件中的工具选项板是做什么的吗?
  4. python中字符串(十六进制和常规)和字节流互转处理
  5. java分号_java枚举类型中分号的用法
  6. winform自定义控件(gdi+)(7)——Matrix类的用法
  7. 电影制作的“文艺复兴”-你准备好了吗?
  8. 测试工具(四)Jenkins环境搭建与使用
  9. webadb通过usb调试功能操作手机
  10. php图片背景平铺,css如何设置背景图片的平铺方式?css设置背景图片平铺的方法(图文详解)...