java 多线程 原子性和易变性的理解(还有可视性)

来源:互联网 发布:黄焖鸡怎么做好吃 知乎 编辑:程序博客网 时间:2024/06/05 15:11
    volatile关键字还确保了应用中的可视性。如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以
看到这个修改。即便使用了本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中。
    理解原子性和易变性是不同的概念这一点很重要。在非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这
个新值。如果多个任务在同事访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致主存中刷新,
因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的。
    一个任务所作的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那么你就不需要将其设置为volatile的。
    当一个域的值依赖于它之前的值时(例如递增一个计数器),volatile就无法工作了。如果某个域的值受到其他域的值的限制,那么volatile
也无法工作,例如Range类的lower和upper边界就必须遵循lower<=upper的限制。
    使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。再次提醒,你的第一个选择应该是使用synchronized关键字,
这是最安全的方式,而尝试其他任何方式都是有风险的。
    什么才属于原子操作呢?对域中的值做赋值和返回操作通常都是原子性的,但是,在C++中,i++;i+=2都可能是原子性的。但是在C++中,这要
取决于编译器和处理器。你无法编写依赖于原子性的C++跨平台代码,因为C++没有像Java(在Java SE5中)那样一致的内存模型。

    在Java中,上面的操作可能不是原子性的,正如下面的方法所阐释的JVM指令中可以看到的那样:

    //concurrency / Atomicity.java    //{Exec: javap -c Atomicity}    public class Atomicity{int i;void f1(){i++;}void f2(){i+=3;}    }/* Output:(Sample)....void f1():Code:0:aload_01:dup2:getfield#2; //Field i:I5:iconst_16:iadd7:putfield#2; //Field i:I10:returnvoid f2():Code:0:aload_01:dup2:getfield#2;  //Field  i:I5:iconst_36:iadd7:putfield#2:  //Field  i:I10:return*/

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/**每条指令都会产生一个get和put,它们之间还有一些其他的指令。因此在获取和放置之间, * 另一个任务可能会修改这个域,所以,这些操作不是原子性的: * 如果你盲目地应用原子性概念,那么就会看到在下面程序中的getValue()符合上面的描述: * 该程序降到奇数值并终止。 *  * 解决办法getValue()和evenIncrement()必须是synchronized的。 * 在诸如此类情况下,只有并发专家才有能力进行优化,而你还是应该运用Brian的同步规则。 *  * @create @author Henry @date 2016-11-28 * */public class AtomicityTest implements Runnable {/** * 由于i也不是volatile的,因此还存在可视性问题。 */private int i=0;/** * 尽管return i 确实是原子性操作,但是缺少同步使得其数值可以子在处于不稳定的中间 * 状态时被读取。 * @return */public int getValue(){return i;}private synchronized void evenIncrement(){i++;i++;}@Overridepublic void run() {while(true)evenIncrement();}public static void main(String[] args) {ExecutorService exec=Executors.newCachedThreadPool();AtomicityTest at=new AtomicityTest();exec.execute(at);while(true){int val=at.getValue();if(val%2!=0){System.out.println(val);System.exit(0);}}}}

/** *   SerialNumberGenerator与你想象的一样简单,如果你有C++或其他低层语音的背景,那么可能会期望 * 递增是原子性操作,因为C++递增通常可以作为一条微处理器指令来实现(尽管不是以任何可靠的、跨平台的形式实现)。 * 然而正如前面注意到的,Java递增操作不是原子性的,并且涉及一个读操作和一个写操作,所以即使是在这么简单的操作中, * 也为产生线程问题留下了空间。正如你所看到的,易变性在这里实际上不是什么问题,真正的问题在于nextSerialNumber() * 在没有同步的情况下对共享可变值进行了访问。 *   基本上,如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么你就应该将这个域设置为 * volatile的。如果你将一个域定义为volatile,那么它就会告诉编译器不要执行任何移除读取和写入操作的优化, * 这些操作的目的是用线程中的局部变量维护对这个域的精确同步。实际上,读取和写入都是直接针对内存,而却没有被缓存。 * 但是,volatile并不能对递增不是原子性操这一事实产生影响。 *  * @create @author Henry @date 2016-11-29 * */public class SerialNumberGenerator {private static volatile int serialNumber=0;public static int nextSerialNumber(){return serialNumber++;}}

import java.util.Iterator;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;// : concurrency/SerialNumberChecker.java// Operations that may seem safe are not.// when threads are present.// {Args:4}/** * 为了测试SerialNumberGenerator,我们需要不会耗尽内存的集(Set),以防需要花费很长的时间来探测问题。 * 这里所示的GircularSet重用了存储int数值的内存,并假设在你生成序列数时,产生数值覆盖冲突的可能性极小。 * add()和contains()方法都是synchronized,以防止线程冲突。 *  * Reuses storage so we don't run out of memory: *  * @create @author Henry @date 2016-11-29 */class CircularSet {private int[] array;private int len;private int index = 0;public CircularSet(int size) {array = new int[size];len = size;// Initialize to a value not produced// by the SerialNumberGeneratorfor (int i = 0; i < size; i++)array[i] = -1;}public synchronized void add(int i) {array[index] = i;// Wrap index and write over lod elements;index = ++index % len;}public synchronized boolean contains(int val) {for (int i = 0; i < len; i++)if (array[i] == val)return true;return false;}}/** * SerialNumberChecker包含一个静态的CircularSet,它持有所产生的所有序列数;另外还包含一个内嵌的SerialChecker类, * 它可以确保序列数是唯一的。通过创建多个任务来竞争序列数,你将发现在和谐任务最终会得到重复的序列数,如果你运行的时间 * 足够长的话。为了解决这个问题,在nextSerialNumber()前面添加了synchronized关键字。 *  * 对基本类型的读取和赋值操作被认为是安全的原子性操作。但是,正如你在AtomicityTest.java中看到,当对象处于不稳定状态时, * 仍旧很可能使用原子性操作来访问它们。对这个问题做出假设是棘手而危险的,最明智的做法就是遵循Brian的同步规则。 *  * @create @author Henry @date 2016-11-29 *  */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 {@Overridepublic void run() {while (true) {int serial = SerialNumberGenerator.nextSerialNumber();if (serials.contains(serial)) {System.out.println("Duplicate: " + serial);System.exit(0);}serials.add(serial);}}}/** * 运行结果: * No duplicates detected * Duplicate: 3920303 * Duplicate: 3920302 *  * @param args * @throws Exception */public static void main(String[] args) throws Exception {for (int i = 0; i < SIZE; i++) {exec.execute(new SerialChecker());// Stop after n seconds if there's an argument;if (true) {// args.length>0TimeUnit.SECONDS.sleep(new Integer("4"));// args[0]System.out.println("No duplicates detected");//System.exit(0);}}}}


0 0
原创粉丝点击