线程读写数据
如果只有单一线程对物理内存里的数据进行计算操作,是不会造成数据差异的。但当有多个线程同时进行读写操作时,就可能会发生数据不一致的问题。
那仅有一个Cpu的情况下,多线程操作会发生数据不一致的问题吗?
如果不加任何读写上的限制,是会产生不正确的数据的。每个线程竞争Cpu的时间来进行操作和线程上下文切换机制,有可能进行到写操作的时候被另一个线程占用,另一个线程也进行了读写操作,造成数据不正确。
互相抢来抢去:
当然,单核Cpu执行多个线程没有太大必要,线程不断地上下文切换会造成Cpu时间的过渡消耗。
缓存一致性
现在主流多核Cpu来运行进程,Cpu从主存直接拉取数据,远小于从高速缓存Cache
中读写数据,对于各自临时变量,从主存读来读去开销过大。
如果Cpu只是各有各的Cache
的话,不同Cpu操作共享数据的时候,容易产生偏差。
为了针对这个问题,从而出现了缓存一致性。
Cpu有各自的一级缓存,还有共享的二级缓存。
假设从主存中的数据x、y,先读取到二级缓存中,然后Cpu1和Cpu2因任务需要读取x、y。
二级缓存:x、y
Cpu1的缓存:x、y
Cpu2的缓存:x、y
Cpu1修改了x的值,并想写入到主存,首先是写入到二级缓存中的。二级缓存的x发生了变化,此时会让其他读取了它的Cpu这个x域失效,需要再从缓存中读取一次。
此时:
二级缓存:x’、y
Cpu1的缓存:x’、y
Cpu2的缓存:x 、y
Java CAS操作
用Java自带的CAS操作,是一种无锁原子性操作,来保证数据操作的有序性,可见性,一致性。Java不像C++,可以从编写上直接对内存控制,从这一方面来保证相应的安全性。需要用到sun.misc的Unsafe
类来实现相应操作。
compareAndSwap
这里我们先来看一下Unsafe类中的
boolean compareAndSwapInt(Object class, long valueOffset, int expect, int update );
用native方法先去比较expect预期的值和缓存中的值是否相同,如相同更新为update,返回true,否则返回false。当要去更改变量的值时,都会去循环等待CAS的返回值。退出循环的时候,也就保证了数据的正确性。
具体实现
获取Unsafe的对象
由于Unsafe对象处于安全考虑,并不能直接通过getInstance()
来获取,如尝试此,会抛出SecurityException()
。
用Java反射机制:
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = (Unsafe) unsafeField.get(Unsafe.class);
获取变量的内存偏移量
private volatile static int count = 0;
static {
try {
....
valueOffSet = unsafe.staticFieldOffset
(Test.class.getDeclaredField("count"));
} catch (Exception ex) {
throw new Error(ex);
}
}
多线程进行CAS操作
public class Test {
...
static class testUnsafe implements Runnable {
@Override
public void run() {
int tempCount, memoryCount;
for(int i = 1; i <= 10 ; i++) {
tempCount = Test.count;
System.out.println(Thread.currentThread().getName() + ":tempCount = " + tempCount);
memoryCount = unsafe.getInt(Test.class, valueOffSet);
System.out.println(Thread.currentThread().getName() + ":memoryCount = " + memoryCount);
unsafe.getAndAddInt(Test.class, valueOffSet, 1);
System.out.println(Thread.currentThread().getName() + ":increased = " + unsafe.getInt(Test.class, valueOffSet));
}
}
}
public static void main(String[] args) {
new Thread(new testUnsafe(), "Thread-1").start();
new Thread(new testUnsafe(), "Thread-2").start();
}
}
Output
Thread-1:tempCount = 0
Thread-2:tempCount = 0
Thread-1:memoryCount = 0
Thread-1:increased = 1
Thread-1:tempCount = 1
Thread-1:memoryCount = 1
Thread-1:increased = 2
Thread-1:tempCount = 2
Thread-1:memoryCount = 2
Thread-1:increased = 3
Thread-1:tempCount = 3
Thread-1:memoryCount = 3
Thread-1:increased = 4
Thread-1:tempCount = 4
Thread-2:memoryCount = 4
Thread-1:memoryCount = 4
Thread-1:increased = 6
Thread-1:tempCount = 6
Thread-1:memoryCount = 6
Thread-1:increased = 7
Thread-2:increased = 5
发现Thread-2
一开始持有变量,由于Thread-1
的更改,持有的变量发生变化,Thread-2
需要读一次,然后再判断更改。
缺点与不足
- ABA问题,就是变量改变的历史,CAS操作是不可见的,但是可以给变量加上版本号。
- 循环等待时间可能过长,自旋CAS如果循环等待时间过长,可能给Cpu带来过渡的消耗。
本文仅供学习记录之用,如有错误欢迎指正。