易语言 字段重复
总览
易失性字段的预期行为是,它们在多线程应用程序中的行为应与在单线程应用程序中的行为相同。 禁止它们表现相同的方式,但是不能保证它们表现相同的方式。
Java 5.0+中的解决方案是使用AtomicXxxx类,但是这些类在内存(它们添加标头和填充),性能(它们添加引用和对其相对位置的控制很少)方面效率较低,从语法上讲,它们不是明确使用。
恕我直言,一个简单的解决方案,如果可变字段能够像预期的那样起作用,那么JVM必须在AtomicFields中支持的方式是当前JMM(Java内存模型)中所禁止的,但不能保证。
为什么要使字段
volatile字段的优点是,它们在线程中可见,并且某些避免重新读取它们的优化已被禁用,因此即使您没有更改它们,也始终要再次检查当前值。
例如不挥发
Thread 2: int a = 5;Thread 1: a = 6;
(后来)
Thread 2: System.out.println(a); // prints 5 or 6
具有挥发性
Thread 2: volatile int a = 5;Thread 1: a = 6;
(后来)
Thread 2: System.out.println(a); // prints 6 given enough time.
为什么不一直使用
易失的读写访问速度要慢得多。 当您写入易失性字段时,它将使整个CPU管道停顿下来,以确保已将数据写入缓存。 否则,即使在同一线程中,下一次读取该值也有可能会看到旧值(请参阅AtomicLong.lazySet(),这样可以避免停顿管道)
惩罚可能会慢10倍左右,您不想在每次访问时都这样做。
一个重要的限制是,即使您可能认为对字段的操作也不是原子的。 甚至比通常情况更糟,没有区别。 也就是说,它似乎可以工作很长时间甚至几年,并且由于偶然的更改(例如所用的Java版本),甚至对象加载到内存中而突然/随机中断。 例如,在运行程序之前加载了哪些程序。
例如更新值
Thread 2: volatile int a = 5;Thread 1: a += 1;
Thread 2: a += 2;
(后来)
Thread 2: System.out.println(a); // prints 6, 7 or 8 even given enough time.
这是一个问题,因为对a的读取和对a的写入是分别完成的,并且您可以获得竞争条件。 99%以上的时间它会表现出预期,但有时却不会。
你能为这个做什么?
您需要使用AtomicXxxx类。 这些将易失性字段包装为具有预期行为的操作。
Thread 2: AtomicInteger a = new AtomicInteger(5);Thread 1: a.incrementAndGet();
Thread 2: a.addAndGet(2);
(后来)
Thread 2: System.out.println(a); // prints 8 given enough time.
我有什么建议?
JVM具有一种按预期方式运行的方法,唯一令人惊讶的事情是您需要使用特殊的类来完成JMM不能保证的工作。 我建议更改JMM以支持当前由并发AtomicClasses提供的行为。
在每种情况下,单线程行为都是不变的。 没有看到竞争条件的多线程程序将表现相同。 区别在于,多线程程序不必查看竞争条件,而可以更改基本行为。
当前方法 | 建议语法 | 笔记 |
---|---|---|
x.getAndIncrement() | x ++或x + = 1 | |
x.incrementAndGet() | ++ x | |
x.getAndDecrment() | x–或x-= 1 | |
x.decrementAndGet() | -X | |
x.addAndGet(y) | (x + = y) | |
x.getAndAdd(y) | ((x + = y)-y) | |
x.compareAndSet(e,y) | (x == e?x = y,true:false) | 需要添加逗号语法 在其他语言中使用。 |
所有原始类型(例如布尔,字节,short,int,long,float和double)都可以支持这些操作。
可以支持其他赋值运算符,例如:
当前方法 | 建议语法 | 笔记 |
---|---|---|
原子乘法 | x * = 2; | |
原子减法 | x-= y; | |
原子分裂 | x / = y; | |
原子模量 | x%= y; | |
原子位移 | x << = y; | |
原子位移 | x >> = z; | |
原子位移 | x >>> = w; | |
原子和 | x&=〜y; | 清除位 |
原子或 | x | = z; | 设置位 |
原子异或 | x ^ = w; | 翻转位 |
有什么风险?
这可能会破坏依赖于这些操作的代码,这些代码有时会由于竞争条件而失败。
可能无法以线程安全的方式支持更复杂的表达式。 这可能会导致令人惊讶的错误,因为代码看起来像是正常的,但事实并非如此。 永远不会比当前状态更糟。
JEP 193 –增强挥发性
有一个JEP 193将此功能添加到Java。 一个例子是:
class Usage {volatile int count;int incrementCount() {return count.volatile.incrementAndGet();}
}
恕我直言,这种方法有一些限制。
- 语法是相当重要的变化。 更改JMM可能不需要更改Java语法,也可能不需要更改编译器。
- 这是一个不太通用的解决方案。 支持诸如体积+ =数量之类的操作可能很有用; 这些是双重类型。
- 开发人员要了解为什么他/她应该使用此代码而不是x ++ ,这会给开发人员带来更多负担;
我不相信使用更麻烦的语法可以更清楚地了解正在发生的事情。 考虑以下示例:
volatile int a, b;a += b;
要么
a.volatile.addAndGet(b.volatile);
要么
AtomicInteger a, b;a.addAndGet(b.get());
作为行,这些操作中的哪个是原子的。 他们都不回答,但是采用Intel TSX的系统可以使这些原子化,如果您要更改这些代码行中的任何行为,我都可以使a + = b; 而不是发明一种新的语法,该语法在大多数情况下会做同样的事情,但是可以保证一种语法,而不能保证另一种语法。
结论
如果JMM保证等效的单线程操作的行为符合多线程代码的预期,则可以消除使用AtomicInteger和AtomicLong的许多语法和性能开销。
可以通过使用字节码检测将该功能添加到Java的早期版本中。
翻译自: https://www.javacodegeeks.com/2014/07/making-operations-on-volatile-fields-atomic.html
易语言 字段重复