Java并发编程之——原子变量(解决一些线程共享数据问题)
在JDK5新增并发编程包中有一个子包,java.util.concurrent.atomic,此包中定义了对单一变量执行原子操作的类。所有类都有get和set方法,工作方法和对volatile变量的读取和写入一样。
java.util.concurrent.atomic 包中添加原子变量类,所有原子变量类都公开“比较交设置”原语(与比较并交换类似),这些原语都是使用平台上可用的最快的本机结构(比较并交换,加载链接、条件存储,最坏情况下是旋转锁)来实现的。此包提供了原子变量的9种风格(AtomicInteger,AtomicLong,AtomicReference、AtomicBoolean,原子整型,长型,引用、及原子标记引用和戳记引用类的数组形式,其原子地更新一对值)原子变量类共有12个,分成4组:计量器,域更新器,数组以及复合变量。最常用的原子变量是计量器:AtomicInteger,AtomicLong,AtomicBoolean及AtomicReference.他们都支持CAS。
原子变量的引入(实现无锁操作):
锁同步法之缺点: 在 Java 语言中,协调对共享字段访问的传统方法是使用同步,确保完成对共享字段的所有访问,同时具有适当的锁定。通过同步,可以确定(假设类编写正确)具有保护一组访问变量的所有线程都将拥有对这些变量的独占访问权,并且以后其他线程获得该锁定时,将可以看到对这些变量进行的更改。弊端是如果锁定竞争太厉害(线程常常在其他线程具有锁定时要求获得该锁定),会损害吞吐量,因为竞争的同步非常昂贵。对于现代 JVM 而言,无竞争的同步现在非常便宜。基于锁的算法的另一个问题是:如果延迟具有锁的线程(因为页面错误、计划延迟或其他意料之外的延迟),则没有要求获的锁的线程可以继续运行。
使用锁,如果一个线程试图获取其他线程已经具有的锁,那么该线程将被阻塞,直到该锁可用。此方法具有一些明显的缺点,其中包括当线程被阻塞来等待锁时,它无法进行其他任何操作。如果阻塞的线程是高优先级的任务,那么该方案可能造成非常不好的结果(称为优先级倒置的危险)。
使用锁还有一些其他危险,如死锁(当以不一致的顺序获得多个锁时会发生死锁)。甚至没有这种危险,锁也仅是相对的粗粒度协调机制,同样非常适合管理简单操作,如增加计数器或更新互斥拥有者。如果有更细粒度的机制来可靠管理对单独变量的并发更新,则会更好一些;在大多数现代处理器都有这种机制。
无锁算法之比较与交换:
大多数现代处理器都包含对多处理的支持。当然这种支持包括多处理器可以共享外部设备和主内存,同时它通常还包括对指令系统的增加来支持多处理的特殊要求。特别是,几乎每个现代处理器都有通过可以检测或阻止其他处理器的并发访问的方式来更新共享变量的指令。现在的处理器(包括 Intel 和 Sparc 处理器)使用的最通用的方法是实现名为“比较并交换(Compare And Swap)”或 CAS 的原语。(在 Intel 处理器中,比较并交换通过cmpxchg 系列指令实现。CAS 操作包含三个操作数—— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法可以对该操作重新计算。但是 CAS 的价值是它可以在硬件中实现,并且是极轻量级的(在大多数处理器中)。后面我们分析Java的源代码可以知道,JDK在实现的时候使用了本地代码。
基于 CAS 的并发算法称为“无锁定算法”,因为线程不必再等待锁定(有时称为互斥或关键部分,这取决于线程平台的术语)。无论 CAS 操作成功还是失败,在任何一种情况中,它都在可预知的时间内完成。如果 CAS 失败,调用者可以重试 CAS 操作或采取其他适合的操作
“无锁定算法”要求某个线程总是执行操作。(无等待的另一种定义是保证每个线程在其有限的步骤 中正确计算自己的操作,而不管其它线程的操作,计时,交叉或速度。这一限制可以是系统中线程数的函数:如,如果有10个线程,每个线程都执行一次Counter.increment()操作,最坏情况下,每个线程都必须重试最多9次,才能完成增加。
一个线程的失败或挂起不应该影响其他线程的失败或挂起,这样的算法被称为非阻塞(non-blocking)算法。如果算法的每一步骤中都有一些线程能够继续执行,那么这样的算法称为无锁(lock-free)算法。非阻塞算法对死锁和优先级倒置有“免疫性”。
原子变量:
并发算法的研究主要焦点是无锁算法(nonblocking algorithms),这些无锁算法使用低层原子化的机器指令,例如使用compare-and-swap(CAS)代替锁保证并发情况下数据的完整性。现在无锁算法广泛应用于操作系统与JVM中,比如线程和进程的调度、垃圾收集、实现锁和其他并发数据结构。在 JDK5.0 之前,如果不使用本机代码,就不能用 Java 语言编写无等待、无锁定的算法。在 java.util.concurrent 中添加原子变量类之后,这种情况发生了变化。我们可以利用这些新类开发高度可伸缩的无阻塞算法。
1 /** 2 * 3 * 模拟各有万个线程对原子整形类和普通整型类加1和减1,看到最后的结果原子整形类为0,普通的却不一定是0 4 * 5 */ 6 public class AtomicTest { 7 8 public static AtomicInteger atomicNum = new AtomicInteger(); 9 public static int intNum =0; 10 11 public static void main(String[] args) { 12 //ExecutorService ex = Executors.newFixedThreadPool(10); 13 AtomicTest atomic = new AtomicTest(); 14 for(int i=0;i<10000;i++){ 15 new Thread(new AddThread(atomic)).start(); 16 new Thread(new SubThread(atomic)).start(); 17 } 18 19 } 20 21 22 public void add(){ 23 intNum ++; 24 atomicNum.getAndIncrement(); 25 26 } 27 public void sub(){ 28 intNum --; 29 atomicNum.getAndDecrement(); 30 } 31 32 } 33 34 class AddThread implements Runnable{ 35 private AtomicTest atomicTest; 36 37 public AddThread(AtomicTest atoMic){ 38 this.atomicTest = atoMic; 39 } 40 @Override 41 public void run() { 42 atomicTest.add(); 43 44 System.out.println(Thread.currentThread().getName()+":atomicNum:"+atomicTest.atomicNum+",intNum:"+atomicTest.intNum); 45 } 46 47 } 48 49 class SubThread implements Runnable{ 50 private AtomicTest atomicTest; 51 public SubThread(AtomicTest atomic){ 52 this.atomicTest = atomic; 53 } 54 @Override 55 public void run() { 56 atomicTest.sub(); 57 System.out.println(Thread.currentThread().getName()+":atomicNum:"+atomicTest.atomicNum+",intNum:"+atomicTest.intNum); 58 } 59 60 }