本篇我只想讲这一个方法,因为其他的CAS操作类似,只要把这个搞懂了,其他的就不是问题。举个最简单的例子,1000个线程要去修改一个值,但是这个值只能被修改一次,比如1000个人抢1个红包,但是红包就只有一个:
public class NoAtomicTest {
private static int money = 1;//红包
public static void main(String[] args) {
Thread[] persons = new Thread[1000];
for (int i = 0; i < 1000; i++) {
persons[i] = new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (money == 1) {
money = 0;
System.out.println(Thread.currentThread().getName() + "抢到红包");
}
},"会员"+i);
}
for (int i = 0; i < persons.length; i++) {
persons[i].start();
}
}
}
输出可能是:
会员0抢到红包
会员5抢到红包
会员1抢到红包
可以看到一个红包居然可以三个人抢到,就有问题啦,现在我们用原子操作试试:
public class AtomicTest {
private static AtomicInteger money = new AtomicInteger(1);
public static void main(String[] args) {
Thread[] persons = new Thread[1000];
for (int i = 0; i < 1000; i++) {
persons[i] = new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (money.compareAndSet(1, 0)) {
System.out.println(Thread.currentThread().getName() + "抢到红包");
}
}, "会员" + i);
}
for (int i = 0; i < persons.length; i++) {
persons[i].start();
}
}
}
结果永远都是1个人抢到。
例子举完了,我们得知道为什么用这个方法可以避免问题,我们要看看这个原子类AtomicInteger
的一些源码:
public final boolean compareAndSet(int expectedValue, int newValue) {
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
先不管参数,会发现是调用U
的compareAndSetInt
,我们看看U
是什么:
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
public static Unsafe getUnsafe() {
return theUnsafe;
}
private static final Unsafe theUnsafe = new Unsafe();
原来是内部的Unsafe
对象,这个方法简单来说就是可以操作底层硬件的,可以直接用C/C++语言,可以使用汇编语言的哦。
然后我们发现还有个VALUE
哪里来的:
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
public long objectFieldOffset(Class<?> c, String name) {
if (c == null || name == null) {
throw new NullPointerException();
}
return objectFieldOffset1(c, name);
}
private native long objectFieldOffset1(Class<?> c, String name);
原来是Unsafe
的方法获得的,看方法一是就是说获得对象某个属性的偏移,也就是value
属性的偏移。其实就是内存中这个属性的地址啦,你可以把内存地址理解成一个数组,value
属性就在数组里,你要获取是不是得有索引啊,索引就是相当于偏移,当然物理内存中不是那么简单存储。其实最后是调用了本地方法,我们来看看这个的方法到底是在什么地方,我下了JDK11
的源码:
是这句:
其实就是个宏定义,指向方法:
内部又调用了:
一堆C++的代码,其实我也太懂,我猜是调用了JavaFieldStream
的offset
方法来获得偏移量,这个是JavaFieldStream
父类FieldStreamBase
的方法,而且还有其他的方法:
内部是调用了FieldInfo
的方法:
核心的应该是这句,其实就是把两个unsigned short
拼成一个int
,然后右移2
位:
return build_int_from_shorts(_shorts[low_packed_offset], _shorts[high_packed_offset]) >> FIELDINFO_TAG_SIZE;
16位低位和16位高位(low_packed_offset
和high_packed_offset
)合起来成一个32位:
inline int build_int_from_shorts( jushort low, jushort high ) {
return ((int)((unsigned int)high << 16) | (unsigned int)low);
}
low_packed_offset
和high_packed_offset
这两个参数哪里来的呢,应该是初始化的时候设置好的:
其他的我就不多说啦,你只要知道这个偏移量是可以从底层的属性的偏移量中获取的。
最后还是本地方法:
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
这个的意思就是说针对某个对象o
,根据偏移量offset
找到属性值,跟我们所期望的值expected
是否一样,如果一样,就把这个属性值设置成x,返回true,否则false。
那我们看看本地方法吧:
同样调用的是Unsafe_CompareAndSetInt
:
对应的就是access.hpp
的:
具体的实现应该就是比如windows上的atomic_windows_x86.hpp
的Atomic::PlatformCmpxchg
方法:
template<>
template<typename T>
inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,
T volatile* dest,
T compare_value,
atomic_memory_order order) const {
STATIC_ASSERT(4 == sizeof(T));
// alternative for InterlockedCompareExchange
__asm {
mov edx, dest //把属性地址放进寄存器edx
mov ecx, exchange_value //把新的属性值放进寄存器ecx
mov eax, compare_value //属性的的期望值
//取寄存器edx中的值所对应的内存地址中的值和eax中的值作比较,如果相等就设置成ecx寄存器中的值。而且还用lock上锁了,要么就是总线锁,要么就是缓存锁.
//[edx]属于间接寻址,就是获取到寄存器edx中的值(内存地址)后再去内存中取值
//dword ptr 双字的指针指向内存地址
lock cmpxchg dword ptr [edx], ecx
}
}
好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。