原子的本意是:“不能被进一步分割的最小粒子”,而原子操作的意思是:“不可被中断的一个或者一系列操作”。
首先先说说处理通过怎样的方式实现了原子操作:
使用总线锁保证原子性
首先,我们应该明白什么是总线锁
总线锁:使用处理器提供的一种LOCK #信号,当一个处理器在总线处理一个变量时,其他处理器的请求被阻塞,正在处理当前变量的处理器独占共享内存。
1、方法提出背景
当多个处理器对同一个共享变量进行读改写操作时,共享变量会被多个处理器同时操作,不能够保证操作的原子性。举个例子,有一个共享变量 i 现在要进行i++的操作,凉饿处理同时操作,我们的期望值应该是3,但是有可能记过是2。因为处理器在进行数据的操作时,都是在各自的缓存中获取到这个 i ,分别进行 + 1 操作,然后写入系统内存中。如果要保证操作的原子性,就必须保证在CPU1粗粒该变量时,CPU2就不能操作缓存了该变量的共享内存的缓存。这就引入了总线锁的概念。
使用缓存锁保证原子性操作
缓存锁定:内存区域如果被缓存到处理器的缓存行中,并且在Lock操作期间被锁定,当它执行锁操作写回内存时,处理器不会在总线上进行声言LOCK # 信号,而是修改内部的内存地址,通过它的缓存一致性原则来保证操作的原子性,当其他处理器写回已被锁定的缓存行数据时,会被缓存行失效。
JAVA实现原子操作
在JAVA中,一般通过锁和循环CAS来实现原子操作。
1)使用循环CAS实现原子操作(在JVM中该操作是利用了处理器提供的CMPXCHG指令来完成的)
public class Counter {
private int i = 0;
private AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) {
final Counter counter = new Counter();
//定义一个装有Thread的List集合
List<Thread> threadList = new ArrayList<>(600);
Long start = System.currentTimeMillis();
//创建100个线程进行同步操作
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
//每个线程中实现1000次的计数
for (int i = 0; i < 1000; i++) {
counter.count();
counter.safeCount();
}
}
});
//将当前线程加入到List集合中
threadList.add(t);
}
//让所有的线程运行
for (Thread t : threadList) {
t.start();
}
//等待所有线程执行完成
for (Thread t : threadList) {
try {
t.join();//将线程挂起
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//这里分别输出用两种方法进行计数的最终值
System.out.println(counter.i);
System.out.println(counter.atomicInteger.get());
//一个线程计数1000,100个就是100000
System.out.println("The Time is => " + (System.currentTimeMillis() - start));
}
/**
* 使用CAS实现线程安全的计数
*/
private void safeCount() {
for (; ; ) {
int i = atomicInteger.get();//获取到当前的i
/* public final boolean compareAndSet ( int expectedValue, int newValue){
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}*/
//调用上面的方法进行计数增加
boolean suc = atomicInteger.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
/**
* 非线程安全的计数器
*/
private void count() {
i++;
}
}
这样虽然保证了线程安全,但是忽略了一个问题——ABA问题
当一个处理器在处理一个变量 A 时,先将 A 变量改为 B,之后又改为 A,那么使用CAS检查值时,就会发现其实没有发生变化,但是,确实有操作进行了变量更改。解决这个问题的方法,就是可以加版本号,每更改一次就使其版本号加一,这样,检查时发现版本号不一致,就停止操作。正如上面所说的那个方法:compareAndSet首先检查当前引用是否等于与其引用,并检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和标志位的值进行替换。
还有CAS循环时间开销大、只能保证一个共享变量的原子操作等问题。
2)使用锁机制实现原子操作
该方法保证了只有获得锁的线程才能够操作锁定的内存区域。