java多线程基本概述(十七)——原子性和易变性

      在有关java线程的讨论中,一个常不正确的知识是:“原子操作不需要同步控制”。原子操作是不能被线程调度戒指中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前执行完毕。依赖于原子性是很棘手而且是很危险的。

      原子性可以应用于除了long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存,但是JVM将64位的变量的读取和写入分为两个分离的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同的任务可以看不到正确结果的可能性(有时候,这也成为字撕裂,因为你可能会看到部分被修改或的值)。当你定义long和double时,如果使用volatile关键字,就会获得(简单的赋值和返回操作)原子性(注意:在java5以前,volatile关键字一直未能正常工作)因此,原子操作可由线程机制来保证其不可中断。

      在多核处理器上,相对于单核处理器而已, 可视性问题比原子性问题多得多。一个任务做出的修改,即使在不中断的意义上来将时原子性的,但是对其他任务时不可视的(例如:修改只是暂时的存在本地副处理器的缓存中),因此不同的任务对应用的状态有不同的视图。另一个方面,同步机制强制在处理器系统中,一个任务做出的修改必须在应用中是可视的。如果没有同步机制,那么修改时可视性将无法确定,

  volatile关键字还确保了应用中的可视性,如果一个变量被volatile修饰,那么只要对这个域产生了写操作,那么所有的读操作都将可以看到这个修改。即便使用了本地缓存,情况也是如此。volatile域会立即被写入到主存中去,而读操作就发生在主存中。

    理解原子性和易变性使不用的概念这一点很重要。在非olatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问,同步也会导致向主存中刷新,因此如果一个域完全有synchronized方法或语句块来防护,那就不必将该变量设置位volatile的。

    一个任务所作的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那就不必将该变量设置位volatile的。

 当一个域的值依赖于它之前的值时(例如递增操作),volatile就无法工作了。如果某个域的值收到其他域的值的限制,那么volatile也无法工作,例如Range类的lower和uppeer边界就必须遵循 lower<=upper的限制。

  使用volatile而不是synchronized的唯一安全的情况就是类中只有一个可变的域。 

     如果盲目的应用原子性的概念,那么下面这个get()操作就会产生问题:

package tij;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by huaox on 2017/4/19.
 */

class TestA implements Runnable{
    private int i=0;
     int get(){return i;}  //此时不是同步方法
    synchronized void add(){i++;i++;}
    @Override
    public void run() {
        while (true){
            add();
            System.out.println(i);
        }
    }
}

public class AtomicityTest {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        TestA a = new TestA();
        service.execute(a);
        while (true) {
            int val = a.get();
            if(val%2 != 0){
                System.out.println(val);
                System.exit(0);
            }
        }
    }

}

输出结果:

280
282
284
286
288
290
292
294
296
298
300
302
304
306
308
310
312
314
316
318
320
322
324
326
328
330
332
334
336
338
340
342
344
346
348
350
352
354
356
358
360
362
364
366
368
370
372
374
376
378
380
382
384
386
388
390
392
394
396
398
400
402
404
406
408
410
412
414
416
418
420
422
424
426
428
430
432
434
436
438
440
442
444
446
448
450
452
454
456
458
460
462
464
466
468
470
472
474
476
478
480
482
484
486
488
490
492
494
496
498
500
502
504
506
508
1
510
512
514
516
518
520
522

Process finished with exit code 0
View Code

可见真的产生了奇数,尽管return i属于原子操作,但缺少同步语句使其数值处于不稳定的中间状态时被读取,由于还不是volatile修饰的,还存在可视性问题。那么改为volatile修饰的效果又如何呢?

package tij;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by huaox on 2017/4/19.
 */

class TestA implements Runnable{
    private volatile int i=0;
     int get(){return i;}  //此时不是同步方法
    synchronized void add(){i++;i++;}
    @Override
    public void run() {
        while (true){
            add();
            System.out.println(i);
        }
    }
}

public class AtomicityTest {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        TestA a = new TestA();
        service.execute(a);
        while (true) {
            int val = a.get();
            if(val%2 != 0){
                System.out.println(val);
                System.exit(0);
            }
        }
    }

}

输出结果:

1
2
4
6
8
10
12
14
16
18
20
22
24
26
28
30
32
34
36
38
40
42
44
46

Process finished with exit code 0

最终还是产生奇数了。所以我们应该用同步语句来修饰。

package tij;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by huaox on 2017/4/19.
 */

class TestA implements Runnable{
    private  int i=0;
    synchronized int get(){return i;}  //此时不是同步方法
    synchronized void add(){i++;i++;}
    @Override
    public void run() {
        while (true){
            add();
            System.out.println(i);
        }
    }
}

public class AtomicityTest {

    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Aborting");
                System.exit(0);
            }
        }, 5000);//在5秒后结束

        ExecutorService service = Executors.newCachedThreadPool();
        TestA a = new TestA();
        service.execute(a);
        while (true) {
            int val = a.get();
            if(val%2 != 0){
                System.out.println(val);
                System.exit(0);
            }
        }


    }

}

现在不会存在脏读问题了。那么我们也可以使用原子类来操作。

api包描述:

软件包 java.util.concurrent.atomic 的描述
类的小工具包,支持在单个变量上解除锁的线程安全编程。事实上,此包中的类可将 volatile 值、字段和数组元素的概念扩展到那些也提供原子条件更新操作的类,其形式如下:
boolean compareAndSet(expectedValue, updateValue);
如果此方法(在不同的类间参数类型也不同)当前保持 expectedValue,则以原子方式将变量设置为 updateValue,并在成功时报告 true。此包中的类还包含获取并无条件设置值的方法,以及以下描述的较弱条件的原子更新操作 weakCompareAndSet。
这些方法的规范使实现能够使用当代处理器上提供的高效机器级别原子指令。但是在某些平台上,该支持可能需要某种形式的内部锁。因而,该方法不能严格保证不被阻塞 - 执行操作之前可能暂时阻塞线程。
类 AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的实例各自提供对相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。
例如,类 AtomicLong 和 AtomicInteger 提供了原子增量方法。一个应用程序将按以下方式生成序列号:
class Sequencer { private final AtomicLong sequenceNumber = new AtomicLong(0); public long next() { return sequenceNumber.getAndIncrement(); } } 原子访问和更新的内存效果一般遵循以下可变规则,正如 The Java Language Specification, Third Edition (17.4 Memory Model) 中的声明: get 具有读取 volatile 变量的内存效果。 set 具有写入(分配)volatile 变量的内存效果。 除了允许使用后续(但不是以前的)内存操作,其自身不施加带有普通的非 volatile 写入的重新排序约束,lazySet 具有写入(分配)volatile 变量的内存效果。
在其他使用上下文中,当为 null 时(为了垃圾回收),lazySet 可以应用不会再次访问的引用。
weakCompareAndSet 以原子方式读取和有条件地写入变量但不 创建任何 happen
-before 排序,因此不提供与除 weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。 compareAndSet 和所有其他的读取和更新操作(如 getAndIncrement)都有读取和写入 volatile 变量的内存效果。
除了包含表示单个值的类之外,此包还包含 Updater 类,该类可用于获取任意选定类的任意选定
volatile 字段上的 compareAndSet 操作。
AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater 和 AtomicLongFieldUpdater 是基于反射的实用工具,可以提供对关联字段类型的访问。
它们主要用于原子数据结构中,该结构中同一节点(例如,树节点的链接)的几个 volatile 字段都独立受原子更新控制。这些类在如何以及何时使用原子更新方面具有更大的灵活性,
但相应的弊端是基于映射的设置较为拙笨、使用不太方便,而且在保证方面也较差。 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操作,对这些类型的数组提供了支持。
这些类在为其数组元素提供
volatile 访问语义方面也引人注目,这对于普通数组来说是不受支持的。 原子类也支持 weakCompareAndSet 方法,该方法具有受限制的适用性。在某些平台上,弱版本在正常情况下可能比 compareAndSet 更有效,
但不同的是 weakCompareAndSet 方法的任何给定调用可能意外 返回
false(即没有明确的原因)。返回 false 仅意味着可以在需要时重新尝试操作,
具体取决于重复执行调用的保证,当该变量保持 expectedValue 并且没有其他线程也在尝试设置该变量时,最终将获得成功。
(例如,这样的虚假失败可能是由于内存争用的结果,该争用与期望值和当前值是否相等无关)。 此外,weakCompareAndSet 不提供通常需要同步控制的排序保证。
但是,在这样的更新与程序的其他 happen-before 排序不相关时,该方法可用于更新计数器和统计数据。当一个线程看到对 weakCompareAndSet 导致的原子变量的更新时,
它不一定能看到在 weakCompareAndSet 之前发生的对任何其他 变量的更新。例如,在更新性能统计数据时,这也许可以接受,但其他情况几乎不可以。 AtomicMarkableReference 类将单个布尔值与引用关联起来。例如,可以在数据结构内部使用此位,这意味着引用的对象在逻辑上已被删除。
AtomicStampedReference 类将整数值与引用关联起来。例如,这可用于表示与更新系列对应的版本号。 设计原子类主要用作各种构造块,用于实现非阻塞数据结构和相关的基础结构类。compareAndSet 方法不是锁的常规替换方法。仅当对象的重要更新限定于单个 变量时才应用它。 原子类不是 java.lang.Integer 和相关类的通用替换方法。它们不 定义诸如 hashCode 和 compareTo 之类的方法。(因为原子变量是可变的,所以对于哈希表键来说,它们不是好的选择。)另外,仅为那些通常在预期应用程序中使用的类型提供类。例如,没有表示
byte 的原子类。这种情况不常见,如果要这样做,可以使用 AtomicInteger 来保持 byte 值,并进行适当的强制转换。也可以使用 Float.floatToIntBits 和 Float.intBitstoFloat 转换来保持 float 值,使用 Double.doubleToLongBits 和 Double.longBitsToDouble 转换来保持 double 值。

用原子类改写上面的代码:

package tij;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by huaox on 2017/4/19.
 */

class TestA implements Runnable{
    private AtomicInteger i= new AtomicInteger(0);
     int get(){return i.get();}
     void add(){i.addAndGet(2);}
    @Override
    public void run() {
        while (true){
            add();
            System.out.println(i.get());
        }
    }
}

public class AtomicityTest {

    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Aborting");
                System.exit(0);
            }
        }, 5000);//在5秒后结束

        ExecutorService service = Executors.newCachedThreadPool();
        TestA a = new TestA();
        service.execute(a);
        while (true) {
            int val = a.get();
            if(val%2 != 0){
                System.out.println(val);
                System.exit(0);
            }
        }


    }

}

这个程序并不会失败

原子类源码:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

1、最外层是一个死循环

2、先获取旧值,将其复制到一个局部变量上

3、将局部变量值+1

4、比较旧值是否变化,如果没变化,说明没有其它线程对旧值修改,直接将新值覆盖到旧值,并返回新值,退出循环

5、如果旧值被修改了,开始下一轮循环,重复刚才这一系列操作,直到退出循环。

所以,第4步的compareAndSet其实是关键,继续看源码:

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

最终看到的是一个native方法(说明依赖不同OS的原生实现)

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

直接调用的是UnSafe这个类的compareAndSwapInt方法
全称是sun.misc.Unsafe. 这个类是Oracle(Sun)提供的实现. 可以在别的公司的JDK里就不是这个类了

compareAndSwapInt的实现

  /**
     * Atomically update Java variable to <tt>x</tt> if it is currently
     * holding <tt>expected</tt>.
     * @return <tt>true</tt> if successful
     */
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);

可以看到, 不是用Java实现的, 而是通过JNI调用操作系统的原生程序.

 compareAndSwapInt的native实现
如果你下载了OpenJDK的源代码的话在hotspot\src\share\vm\prims\目录下可以找到unsafe.cpp

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

可以看到实际上调用Atomic类的cmpxchg方法.

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

在这里可以看到是用嵌入的汇编实现的, 关键CPU指令是 cmpxchg
到这里没法再往下找代码了. 也就是说CAS的原子性实际上是CPU实现的. 其实在这一点上还是有排他锁的. 只是比起用synchronized, 这里的排他时间要短的多. 所以在多线程情况下性能会比较好.

posted @ 2017-04-19 23:33  soar_hu  阅读(1561)  评论(1编辑  收藏  举报