原子性操作
原子即为不可再分的,原子操作即要么所有操作全部完成 要么全不完成。
用synchronized包围的代码块或方法就是原子操作。对于线程来讲,synchronized包围代码,只会全部完成,不会执行一半而中断。
synchronized是一个很重的操作,如果执行代码很简单,例如i++,很多线程都阻塞在外面很划不来,为了解决这种问题,引出了CAS(Compare And Swap),比较并且交换。系统提供了很多原子变量,Atomic开头的变量都是实现了CAS,例如AtomicBoolean、AtomicInteger等。
CAS
CAS原理我们下面举个例子:
假如我们现在有多个线程执行count++这个操作。
- 首先从内存中取出count的值。(假如这时是0)
- 然后进行累加操作。(count变为了1)
- 这时在从内存中取出count的值,如果与第一步取到的值相等,则将累加操作后的值写入内存,否则 说明有别的线程改过了,这时再重复第一步操作,直到完成赋值操作。
总结:
获取内存中的值 ,进行操作,再写入内存的时候,进行判断当前内存中的值是否与之前取出的值是否一致,不一致的话以内存中的新值,重新计算,反复执行(自旋,其实就是死循环),直到内存中的值没有在经过修改,才进行写入操作。
这里就引出了两个概念:
- 悲观错 先锁再操作(synchronized)
- 乐观锁 先操作再判断是否进行修改
CAS和synchronized性能比较:
正常生产环境下CAS的效率是要高于synchronized,因为synchronized会阻塞线程,线程阻塞的时候会发生上下文切换(3-5ms),CAS执行指令的时间大概在0.6ns。
高度竞争,特意设计的情况下synchronized会优于CAS。
为什么有CAS还需要synchronized
- ABA问题:
假设当前有两个线程ThreadA和ThreadB,一个变量A
ThreadA想把A修改为B,根据上面介绍的CAS原理,我们知道,修改的时候会判断A是否被修改过,用段伪代码表示也就是if(A==A) A = B
ThreadB比ThreadA跑的快,ThreadA把A修改为C,然后又改回为A。
可是ThreadA认为A没被修改过。
解决办法:版本戳 要求每个线程修改值的时候 加入一个版本戳
jdk中提供了相关的操作
AtomicMarkReference(有没有变过) AtomicStampedReference(变了几次)
public class UserAtomicMarkable {
static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("red",0);
public static void main(String[] args) throws InterruptedException {
final int oldStamp = atomicStampedReference.getStamp();
final String oldReference = atomicStampedReference.getReference();
System.out.println(oldReference + "----------" + oldStamp);
Thread threadA = new Thread(){
@Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + "当前变量值 " + oldReference + "-当前版本戳 " + oldStamp );
atomicStampedReference.compareAndSet(oldReference,oldReference + "Java",oldStamp,oldStamp + 1);
}
};
Thread threadB = new Thread(){
@Override
public void run() {
super.run();
String reference = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "当前变量值 " + reference + "-当前版本戳 " + stamp );
atomicStampedReference.compareAndSet(reference,reference + "C",stamp,stamp + 1);
String reference1 = atomicStampedReference.getReference();
int stamp1 = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "当前变量值 " + reference1 + "-当前版本戳 " + stamp1 );
}
};
threadA.start();
threadA.join();
threadB.start();
threadB.join();
System.out.println(atomicStampedReference.getReference() + "----------" + atomicStampedReference.getStamp());
}
}
-
开销问题
当竞争激烈的时候,会存在长时间完成不了操作 ,造成自旋,一直重试,会占用CPU资源。
解决办法:换成synchronized。 -
只能保证一个共享变量的原子操作
CAS操作时,只能针对某个内存地址上的值进行修改,而一个地址往往只能保存一个变量。
解决办法:AtomicReference把多个变量打包到一个对象中 替换对象
public class UseAtomicReference {
static AtomicReference<User> atomicReference;
public static void main(String[] args) {
User user = new User("xiaoming",22);
atomicReference = new AtomicReference<>(user);
User updateUser = new User("xiaohong",16);
atomicReference.compareAndSet(user,updateUser);
System.out.println(atomicReference.get());
System.out.println(user);
}
static class User{
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
网友评论