原子性与易变性

原子性与易变性

原子性可以应用于除long和double之外的所有基本类型之上的简单操作。对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分的操作来操作内存。但是JVM可以将64位的读取和写入当作两个分离的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同的任务可以看到不正确结果的可能性(这有时被称为字撕裂,因为你可能会看到部分被修改过的数值)。但是,当你定义为long和double变量时,如果使用volatile关键字,就会获得原子性。不同的JVM可以任意地提供更强的保证,但是

你不应该依赖于平台相同的特性。
原子操作可由线程机制来保证其不可中断,可以利用这一点来编写无锁的代码,这些代码不需要被同步。
在多处理器系统上,相对于但处理器系统而言,可视性问题远比原子性问题多得多。一个任务做出的修改,即使在不中断的意义上讲是原子性的,对其它任务也可能是不可视的,因此不同的任务对应用的状态有不同的视图。另一方面,同步机制强制在处理器系统中,一个任务做出的修改必须在应用中是课室的。如果没有同步机制,那么修改时可视将无法确定。
volatile关键字还确保了应用中的可视性。如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就可以看到这个修改。即便使用本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中。
在非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问,同步也会导致想主存中刷新,因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的。
一个任务所作的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那么你就不需要将其设置为volatile的。
当一个域的值依赖于它之前的值时,volatile就无法工作了。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,例如Range类的lower和upper边界就必须遵循lower<=upper的限制。
使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。再次提醒,你的第一选择应该是使用syhchronized关键字,这是最安全的方式,而尝试其他任何方式都是风险的。

每条指令都会产生一个get和put,他们之间还有一些其他指令。因此在获取和放置之间,另一个任务可能会修改这个域,所以,这些操作不是原子性的:
如果盲目地应用原子性概念,那么就会看到在下面程序中的getValue符合上面的描述:

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

public class AtomicityTest implements Runnable{

    private int i = 0;
    public int getValue() {
        return  i;
    }

    private synchronized void evenIncrements() {
        i++;
        i++;
    }

    @Override
    public void run() {
        while (true) {
            evenIncrements();
        }
    }

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

该程序将找到奇数值并终止,尽管return i 确实是原子性操作,但是缺少同步使得其数值可以在处于不稳定的中间状态时被读取。除此之外,由于i也不是volatile的,因此还存在可视性问题。getValue和evenIncrement必须是synchronized的。
第二个示例,一个产生序列数字的类。每当nextSerialNumber被调用时,它必须向调用者返回唯一的值;

public class SaerialNumberGenerator {
    private static volatile int serialNumber = 0;
    public  static int nextSerialNumber() {
        return serialNumber++;
    }
}

Java递增操作不是原子性的,并且涉及一个读操作和一个写操作,所以即便是在这么简单的操作中,也为产生线程问题留下了空间。正如你所看到的,易变性在这里实际上不是什么问题,真正的问题在于nextSerialNumber在没有同步的情况下对共享可变值进行了访问。
如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那些你就应该将这个域设置为volatile的。如果你将一个域定义为volatile,那么它就会告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护对这个域的精确同步。实际上,读取和写入都是直接针对内存的,而且没有被缓存。但是volatile并不能对递增不是原子性操作这一事实产生影响。
为了测试SerialNumberGenerator,我们需要不会耗尽的集(Set),以防需要花费很长时间来探测问题。这里所示的GircularSet重用了存储int数值的内存,并假设生成序列化数时,产生数值覆盖冲突的可能性极小。add和contains方法都是synchronized,以防止线程冲突。

    public class CircularSet {

    private int[] array;
    
    private int len;
    
    private int index = 0;
    
    public CircularSet(int size) {
        array = new int[size];
        
        len = size;

        for (int i = 0; i < size; i++) {
            array[i] = -1;
        }
        
    }
    
    public synchronized void add(int i) {
        array[index] = i;
        
        index = ++index % len;
    }
    
    public synchronized boolean contains(int val) {
        for (int i = 0; i < len; i++) {
            if (array[i] == val)
                return true;
        }
        return false;
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SerialNumberChecker {

    private static final int SIZE = 10;

    private static CircularSet serials = new CircularSet(1000);

    private static ExecutorService exec = Executors.newCachedThreadPool();

    static class SerialChecker implements Runnable {

        @Override
        public void run() {
            while (true) {
                int serial = SaerialNumberGenerator.nextSerialNumber();

                if (serials.contains(serial)) {
                    System.out.println("Duplicate: " + serial);
                    System.exit(0);
                }

                serials.add(serial);
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < SIZE; i++) {
            exec.execute(new SerialChecker());
        }

        if (args.length > 0) {
            System.out.println("No duplicates detected");
            System.exit(0);
        }
    }
}

SerialNumberChecker包含一个静态的CircularSet,它持有锁产生的所有序列数,另外还包含一个内嵌的SerialChecker类,它可以确保序列数时唯一的。通过创建多个任务来竞争序列数,你将发现这些任务最终会得到重复的序列数,如果你运行的时间足够长的话。为了解决这个问题,在nextSerialNumber前面加了synchronized关键字。
对基本类型的读取和赋值操作被认为是安全的原子性操作。但是,正如你在AtomicityTest.java中看到的,当对象处于不稳定状态时,仍旧很有可能使用原子性操作来访问它们。对这个问题做出假设是棘手而危险的,最明智的做法就是遵循Brian的同步规则。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值