为了开发者更方便的编写多线程程序,Java提供了一些原子操作类,在java.util.concurrent.atomic包下。
什么是原子操作?
操作:实现特定功能的1行或N行代码,或一个方法。
原子操作:多线程下,某个线程在执行该操作时,不允许被其他线程打断。
程序运行时,CPU在多个线程中快速的切换,每个线程在运行下一条指令前,都有可能失去CPU的执行权。
对于一个方法或代码块,如果不做任何处理,那么它肯定不是原子操作。
如何保证非原子操作的安全性?
- synchronized
- 加显式锁
但是这两种方式,开销都太大了,
synchronized:基于阻塞的锁机制
- 被阻塞的线程优先级很高
- 热点资源,大量锁竞争,消耗CPU
- 产生死锁,以及其他线程安全问题。
对于一些简单的操作,使用synchronized或者显式锁开销太大了,为了解决这个问题,Java提供了原子操作类。
原子操作类
java.util.concurrent.atomic包下提供了4种类型的操作类:
- 原子更新基本类型
- 原子更新数组
- 原子更新引用
- 原子更新属性
常用API
-
V get()
获取值 -
void set(V v)
赋值 -
void lazySet(V v)
懒赋值,不能保证对其他线程的“可见性” -
V getAndSet(V v)
返回旧值,并赋新值 -
boolean compareAndSet(V v1, V v2)
比较并替换,返回是否替换成功
简单例子
public class AtomicBooleanDemo {
AtomicInteger ai = new AtomicInteger(0);
int i = 0;
void add(){
//休眠1ms 结果更明显
SleepUtil.sleep(1);
//原子操作
System.out.println(ai.incrementAndGet());
//非原子操作
System.out.println(++i);
}
public static void main(String[] args) {
AtomicBooleanDemo demo = new AtomicBooleanDemo();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
demo.add();
}).start();
}
}
}
开启1000个线程去对i和ai加1,++i不是原子操作,输出结果最大值小于1000,incrementAndGet()是原子操作,输出结果最大值等于1000。
CAS
什么是CAS?
CAS:Compare and Swap,译为:比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
如果V和A相等,那么就用B来替换。
用Java代码描述如下:
public class CASDemo {
int V;
boolean compareAndSet(int A, int B) {
if (V == A) {
V = B;
return true;
}
return false;
}
}
这里的compareAndSet肯定是非原子操作。
CAS可以做到:在不加锁的情况下,确保“比较和替换”操作是原子的。
CAS操作是原子的,但不是锁。
它是利用现代处理器中都支持的原子指令来实现的,Java本身不能实现,需要调用本地方法,利用C语言/汇编来实现。
对于简单的“比较和替换”操作,就不要去使用锁,开销太大了,使用CAS性能更高。
存在的问题
-
ABA问题
通过版本号解决 -
开销问题
-
只能保证一个共享变量的原子操作
使用AtomicReference解决。
解决ABA问题
ABA问题:假设初始值为A,线程1首先将A改为B,线程2又将B改为A,线程3无法判断A是否被修改过。
Java提供了两个类来解决ABA问题:
-
AtomicMarkableReference
记录是否被修改过。 -
AtomicStampedReference
记录每次修改的版本号。
AtomicStampedReference除了比较期望值和原值是否相等外,还会判断版本号是否一致,只有都一致,才会进行修改。
AtomicStampedReference例子
public class AtomicStampedReferenceDemo {
//构建AtomicStampedReference实例 并设置:初始值和初始版本号
AtomicStampedReference<String> asr = new AtomicStampedReference("A", 0);
public static void main(String[] args) {
AtomicStampedReferenceDemo demo = new AtomicStampedReferenceDemo();
//初始版本号
int defaultStamp = demo.asr.getStamp();
//初始值
String defaultValue = demo.asr.getReference();
//第一次修改 A > B 版本号 0 > 1 true
boolean oneChange = demo.asr.compareAndSet(defaultValue, "B", defaultStamp, defaultStamp+1);
//第二次修改 B > A 版本号 1 > 2 true
boolean twoChange = demo.asr.compareAndSet("B", defaultValue, defaultStamp+1, defaultStamp+2);
//第三次修改 A > B 版本号 0 > 1 false
boolean threeChange = demo.asr.compareAndSet(defaultValue, "B", defaultStamp, defaultStamp + 1);
}
}
解决只能一个共享变量的问题
- AtomicReference
AtomicReference使用和原子基本类型相似,区别是AtomicReference保存和比较的是对象引用的内存地址。
public class AtomicReferenceDemo {
private static class Person{
String name;
int age;
}
public static void main(String[] args) {
Person person = new Person();
person.name = "admin";
person.age = 18;
AtomicReference<Person> ar = new AtomicReference<>();
ar.set(person);
Person newPerson = new Person();
newPerson.name = "Lisa";
newPerson.age = 23;
//替换成功 ar内部指向newPerson
ar.compareAndSet(person, newPerson);
Person arPerson = ar.get();
System.out.println(arPerson == newPerson);//true
System.out.println(arPerson.name);//Lisa
System.out.println(person.name);//admin
}
}