0. 分享背景
- 回顾Java中并发编程的相关知识点
- 了解其内部实现机制原理
- 总结并讨论实际项目运用
1. Atomic类
Java.util.concurrent中提供了atomic原子包,可以实现原子操作(atomic operation)
- 相关类AtomicBoolean , AtomicInteger, AtomicLong, AtomicReference
2. AtomicBoolean Demo
Demo UML
- 线程安全类SafePerson
public class SafePerson extends Person{
private AtomicBoolean isReady = new AtomicBoolean(false);
public void doWork() {
Thread current = Thread.currentThread();
if (isReady.compareAndSet(false, true)) {
System.out.println(current.getId() + " Preparing....");
}else {
System.out.println(current.getId() + " Everything is ready, do something...");
}
}
}
- 非线程安全类UnsafePerson
public class UnsafePerson extends Person{
private boolean isReady = false;
public void doWork() {
Thread current = Thread.currentThread();
if (!isReady) {
System.out.println(current.getId() + " Preparing....");
isReady = true;
}else {
System.out.println(current.getId() + " Everything is ready, do something...");
}
}
}
- PersonRunnable类
public class PersonRunnable implements Runnable{
private Person person;
public PersonRunnable(Person person) {
this.person = person;
}
public void run() {
person.doWork();
}
}
- 验证Main方法
public static void main( String[] args )
{
int threadCount = 10;
Person unsafePerson = new UnsafePerson();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
for (int i = 0 ; i < threadCount; i++) {
fixedThreadPool.execute(new PersonRunnable(unsafePerson));
}
}
- 测试UnsafePerson类执行结果, 从执行结果中可以看出,本来仅想执行一次准备工作的代码,被多个线程所执行,并且,每次能够执行的线程数也是不确定的。
9 Preparing....
13 Preparing....
12 Preparing....
10 Preparing....
11 Preparing....
16 Everything is ready, do something...
15 Everything is ready, do something...
14 Everything is ready, do something...
17 Everything is ready, do something...
18 Everything is ready, do something...
- 测试SafePerson执行结果,仅有单个线程能够进入并执行准备阶段代码
9 Preparing....
13 Everything is ready, do something...
10 Everything is ready, do something...
12 Everything is ready, do something...
15 Everything is ready, do something...
16 Everything is ready, do something...
10 Everything is ready, do something...
13 Everything is ready, do something...
11 Everything is ready, do something...
9 Everything is ready, do something...
3. AtomicBoolean 源码跟踪
- JDK部分 sun/misc/Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return true if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
- JVM Hotspot部分
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);
// e 期望值,x 要更新值
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
hotspot/src/share/vm/runtime/atomic.cpp
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
assert(sizeof(jbyte) == 1, "assumption.");
uintptr_t dest_addr = (uintptr_t)dest;
uintptr_t offset = dest_addr % sizeof(jint);
volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
jint cur = *dest_int;
jbyte* cur_as_bytes = (jbyte*)(&cur);
jint new_val = cur; //期望值
jbyte* new_val_as_bytes = (jbyte*)(&new_val);
new_val_as_bytes[offset] = exchange_value;
while (cur_as_bytes[offset] == compare_value) {
jint res = cmpxchg(new_val, dest_int, cur);
if (res == cur) break;
cur = res;
new_val = cur;
new_val_as_bytes[offset] = exchange_value;
}
return cur_as_bytes[offset];
}
hotspot/src/os_cpu/linux_x86/vm
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
- "cmpxchgl"指令的作用,CAS
4. CAS原理(Compare And Swap)
4.1 CAS
CAS指令的三个操作数,当前内存值,预期值,新值。
当且仅当预期值和内存值相同时,将内存值修改为新值,否则什么都不做
4.2 CAS优点
- 非阻塞算法
- 原子操作成本低
4.3 CAS不足
ABA问题。
如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。(AKKA/GCC4.7引入STM)
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
只能保证一个共享变量的原子操作。JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
5.CPU如何保证原子性
关于CPU的锁有如下3种:
1. 处理器自动保证基本内存操作的原子性
首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
2. 使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,
原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
3. 使用缓存锁保证原子性
第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。
但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。
6. 总结
- 同步计数器等
- 简单类型的共享变量操作
- 结合volatile构建其他乐观锁
- read-modify-write来实现Lock-Free算法