并发编程——原子操作
0.1. 什么是原子操作?
原子操作(atomic operation)是指不会被线程调度机制打断的操作。
- 一个整体,不会被切割
- 一旦开始,直到结束,不会被切换
- 顺序不会被打乱
0.2. 为什么使用原子操作?
- 实现轻量级同步
- CPU指令级别提供支持
- 场景:对少量资源进行数据更新操作
0.3. 课程概要
- 原子操作的实现原理
- 基本类型原子更新类
- 数组类型原子更新类
- 引用类型原子更新类
- 对象属性原子更新类
- 总结
0.4. 如何学习原子操作的知识?
- 熟记概念和原理
- 记住常用类,并练习常用方法
- 学会查API使用其它功能
0.5. 学习目标
- 理解原子操作的概念
- 能够描述出Java原子操作类的基本原理
- 能够正确使用常见的原子操作类
1. 原子操作的实现原理
1.1. 如何理解操作的原子性?
下面第二行代码,进行了几个动作?
int i = 0;
int x = i + 1; // 几个动作?
- 读取变量 i 的值
- 对变量 i 进行加 1 运算
- 赋值给变量 x
- 结论:该操作不具有原子性
1.2. 原子操作是如何如何实现的?
-
处理器保证基本内存操作的原子性
总线锁
缓存锁
-
循环CAS(Compare And Swap,比较并交换)
Java中的原子操作正是利用了处理器提供的CMPXCHG指令实现的:
CMPXCHG指令 = 处理器锁 + 循环CAS
1.3. CAS操作的实现思路
1.4. CAS操作的问题
- 只能保证一个共享变量的操作
- CSA操作长时间不成功
- ABA问题(A->B->A):版本号
2. 基本类型原子更新类
- AtomicInteger
- AtomicLong
- AtomicBoolean
2.1. 基本类型原子操作类演示:AtomicInteger
2.1.1. 示例代码:
// public static int number = 0; // 共享变量
public static AtomicInteger number = new AtomicInteger(0);
public static void main(String[] args) {
Runnable runnable = () -> {
for (int i = 0; i < 1000; i++){
// synchronized (PrimitiveTypeAtomic.class) {
// number++; // number = number + 1
// }
// number.getAndIncrement(); // number = number++
number.incrementAndGet(); // number = ++number
}
// System.out.println(Thread.currentThread().getName() + ":" + number);
System.out.println(Thread.currentThread().getName() + ":" + number.get());
};
for (int i = 0; i < 15; i++) {
new Thread(runnable).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终结果" + number.get());
}
2.1.2. 输出结果:
Thread-3:3443
Thread-10:6000
Thread-4:5000
Thread-7:12000
Thread-9:8688
Thread-5:7000
Thread-14:13000
Thread-8:9200
Thread-6:11433
Thread-0:3123
Thread-11:10000
Thread-12:15000
Thread-2:4000
Thread-1:3124
Thread-13:14661
最终结果15000
2.1.3. 结论:
- 数据准确,不会出现错误
- 相对于同步机制,效率更高(参考下个小节“并发工具类”中的代码
WaitNotifyLatch.java)
3. 数组类型原子更新类
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
3.1. 数组类型原子操作类演示:AtomicIntegerArray
3.1.1. 示例代码
int[] intArr = {6, 2, 4, 1, 5, 9};
// array.clone();拷贝原数组到类的成员的位置
AtomicIntegerArray atomicArr = new AtomicIntegerArray(intArr);
atomicArr.compareAndSet(3, 1, 7);
System.out.println(atomicArr.get(3)); // 7
System.out.println(intArr[3]); // 1
3.1.2. 输出结果
7
1
3.1.3. 结论
AtomicIntegerArray执行了数组的拷贝,更新相关数据,对原数组没有影响。见下面源码:
public AtomicIntegerArray(int[] array) {
// Visibility guaranteed by final field guarantees
this.array = array.clone();
}
3.2. 数组类型原子操作类演示:AtomicReferenceArray
3.2.1. 示例代码
UserInfo user1 = new UserInfo(149, "张三丰");
UserInfo user2 = new UserInfo(80, "东方不败");
UserInfo[] userArr = {user1, user2};
// this.array = Arrays.copyOf(array, array.length, Object[].class);
AtomicReferenceArray<UserInfo> userAtomicArr = new AtomicReferenceArray<>(userArr);
UserInfo user3 = new UserInfo(88, "任我行");
userAtomicArr.compareAndSet(1, user2, user3);
System.out.println(userAtomicArr.get(1));
System.out.println(userArr[1]);
/**
* 用户信息实体类
*
* @author itcast
*/
public class UserInfo {
private int age;
private String name;
volatile String hobby; // 爱好,用于支持对象属性原子更新操作
public UserInfo(){}
public UserInfo(int age, String name) {
this.age = age;
this.name = name;
}
public UserInfo(int age, String name, String hobby) {
this.age = age;
this.name = name;
this.hobby = hobby;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "UserInfo{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
3.2.2. 输出结果
UserInfo{age=88, name='任我行'}
UserInfo{age=80, name='东方不败'}
3.2.3. 结论
AtomicReferenceArray执行了数组的拷贝,更新相关数据,对原数组没有影响。见下面源码:
public AtomicReferenceArray(E[] array) {
// Visibility guaranteed by final field guarantees
this.array = Arrays.copyOf(array, array.length, Object[].class);
}
4. 引用类型原子更新类
- AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
4.1. 引用类型原子操作类演示:AtomicReference
4.1.1. 示例代码:
UserInfo user = new UserInfo(5, "博学谷");
AtomicReference<UserInfo> atomicUser = new AtomicReference<>();
atomicUser.set(user);
UserInfo newUser = new UserInfo(17, "传智播客");
atomicUser.compareAndSet(user, newUser);
System.out.println(atomicUser.get().getName() + "-" + atomicUser.get().getAge());
System.out.println(user.getName() + "-" + user.getAge());
4.1.2. 输出结果:
传智播客-17
博学谷-5
4.1.3. 结论:
引用类型原子更新操作,将新的对象通过CAS操作赋值给更新器的成员变量,不会对原引用有影响。参加下面源码:
private volatile V value; // 初始化引用,存储在此成员变量上
public final void set(V newValue) {
// 通过set方法将对象赋值给成员变量,后续的CAS操作,也是赋值给此变量,对原引用没有影响
value = newValue;
}
4.2. 引用类型原子操作类演示:AtomicStampedReference
演示引用类型带版本戳的原子更新类的操作——带版本戳的引用类型原子更新类:AtomicStampedReference。
演示A - B - A问题:
- 首先演示基本操作
- 演示版本戳的效果
- 演示改回最初引用的效果
4.2.1. 示例代码:
// 1.首先演示基本操作
UserInfo userA = new UserInfo(18, "博小谷");
AtomicStampedReference<UserInfo> stampedUser = new AtomicStampedReference<>(userA, 0);
System.out.println("初始版本号:" + stampedUser.getStamp());
System.out.println("初始引用对象:" + stampedUser.getReference());
// 2.演示版本戳的效果
Runnable exchangeUserRunner = () -> {
// 使用循环,反复更新AtomicStampedReference中的引用实例
UserInfo lastUser = userA;
for (int i = 1; i <= 3; i++) {
UserInfo userB = new UserInfo(50 + i, "马爸爸" + i);
stampedUser.compareAndSet(lastUser, userB,
stampedUser.getStamp(), stampedUser.getStamp() + 1);
lastUser = userB;
}
};
Thread changeStampedThread = new Thread(exchangeUserRunner);
changeStampedThread.start();
changeStampedThread.join();
System.out.println("---------------------------");
System.out.println("线程1修改后的版本号:" + stampedUser.getStamp());
System.out.println("线程1修改后的引用对象:" + stampedUser.getReference());
// 3.演示改回最初引用的效果
Runnable changeToARunner = () -> {
stampedUser.compareAndSet(stampedUser.getReference(), userA,
stampedUser.getStamp(), stampedUser.getStamp() + 1);
};
Thread changeToAThread = new Thread(changeToARunner);
changeToAThread.start();
changeToAThread.join();
System.out.println("---------------------------");
System.out.println("线程2修改回最初引用后的版本号:" + stampedUser.getStamp());
System.out.println("线程2修改回最初引用后的引用对象:" + stampedUser.getReference());
System.out.println(stampedUser.getReference() == userA);
4.2.2. 输出结果:
初识版本号:0
初识引用对象:UserInfo{age=18, name='博小谷'}
---------------------------
线程1修改后的版本号:3
线程1修改后的引用对象:UserInfo{age=53, name='马爸爸3'}
---------------------------
线程2修改回最初引用后的版本号:4
线程2修改回最初引用后的引用对象:UserInfo{age=18, name='博小谷'}
true
4.2.3. 结论:
每修改一次,版本号加1
每次修改都会通过版本号进行记录,使用AtomicStampedReference类关注更改次数
4.3. 引用类型原子操作类演示:AtomicMarkableReference
4.3.1. 示例代码:
UserInfo userOld = new UserInfo(18, "张三");
UserInfo userNew = new UserInfo(20, "李四");
// 和AtomicStampedReference的区别在于,关注的问题:是否被修改过
AtomicMarkableReference<UserInfo> amrUser =
new AtomicMarkableReference<>(userOld, false);
// amrUser.compareAndSet(userOld, userNew, true, true); // 修改标记不匹配,无法修改成新值
amrUser.compareAndSet(userOld, userNew, false, false);
System.out.println(amrUser.getReference());
System.out.println(amrUser.isMarked());
4.3.2. 输出结果:
UserInfo{age=20, name='李四'}
false
4.3.3. 结论:
-
使用AtomicMarkableReference类进行CAS操作,需要传递四个参数:
-
- 旧值(期望值);2. 新值;3. 旧的标记(期望标记);4. 新的标记;
- 旧值(期望值)与更新器中存储的值不同,更新失败;
- 旧的标记(期望标记)与更新器中存储的标记不同,更新失败;
- 新值 == 更新器中存储的值,并且 新的标记 == 更新器中存储的标记,不做任何操作;
- 否则,执行更新操作
-
-
每次修改都会通过版本号进行记录,使用AtomicMarkableReference类关注是否被修改过
5. 对象属性原子更新类
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
5.1. 对象属性原子操作类演示:AtomicReferenceFieldUpdater
此类用于更新对象的引用类型属性,整型属性的更新不再赘述。
5.1.1. 示例代码:
// 创建了一个对象属性的原子更新器
AtomicReferenceFieldUpdater fieldUpdater = AtomicReferenceFieldUpdater
.newUpdater(UserInfo.class, String.class, "hobby");
UserInfo userInfo = new UserInfo(22, "博小谷", "爱编程");
System.out.println(fieldUpdater.get(userInfo)); // 获取指定对象的hobbit属性值
fieldUpdater.getAndUpdate(userInfo, str -> "爱Java");
System.out.println(fieldUpdater.get(userInfo)); // 获取指定对象的hobbit属性值
5.1.2. 输出结果:
爱编程
爱Java
5.1.3. 结论:
-
使用AtomicReferenceFieldUpdater类进行CAS操作,需要传递三个参数:
1.要操作的对象的类型;2. 对象的属性类型;3. 属性名称;
-
泛型
- T - 要更新的对象的类型
- V - 要更新的对象的属性的类型
-
通过反射机制找到对象的属性,然后进行操作
-
检查访问权限:对象更新器与要操作的属性的访问权限是否匹配
-
必须是volatile关键字修饰的属性:变量的读一致性(确保每个线程读取该变量时都是最新数据)
-
要操作的变量不能用static修饰
-
要操作的变量不能用final修饰
参考源码:
public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,
Class<W> vclass,
String fieldName) {
return new AtomicReferenceFieldUpdaterImpl<U,W>
(tclass, vclass, fieldName, Reflection.getCallerClass());
}
AtomicReferenceFieldUpdaterImpl(final Class<T> tclass,
final Class<V> vclass,
final String fieldName,
final Class<?> caller) {
final Field field;
final Class<?> fieldClass;
final int modifiers;
try {
// 通过反射机制获取属性
field = AccessController.doPrivileged(
new PrivilegedExceptionAction<Field>() {
public Field run() throws NoSuchFieldException {
return tclass.getDeclaredField(fieldName);
}
});
modifiers = field.getModifiers();
sun.reflect.misc.ReflectUtil.ensureMemberAccess(
caller, tclass, null, modifiers);
ClassLoader cl = tclass.getClassLoader();
ClassLoader ccl = caller.getClassLoader();
if ((ccl != null) && (ccl != cl) &&
((cl == null) || !isAncestor(cl, ccl))) {
sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass); // 访问权限
}
fieldClass = field.getType();
} catch (PrivilegedActionException pae) {
throw new RuntimeException(pae.getException());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
if (vclass != fieldClass)
throw new ClassCastException();
if (vclass.isPrimitive()) // 不能是基本数据类型
throw new IllegalArgumentException("Must be reference type");
if (!Modifier.isVolatile(modifiers)) // 必须是volatile修饰
throw new IllegalArgumentException("Must be volatile type");
// Access to protected field members is restricted to receivers only
// of the accessing class, or one of its subclasses, and the
// accessing class must in turn be a subclass (or package sibling)
// of the protected member's defining class.
// If the updater refers to a protected field of a declaring class
// outside the current package, the receiver argument will be
// narrowed to the type of the accessing class.
this.cclass = (Modifier.isProtected(modifiers) &&
tclass.isAssignableFrom(caller) &&
!isSamePackage(tclass, caller))
? caller : tclass;
this.tclass = tclass;
this.vclass = vclass;
this.offset = U.objectFieldOffset(field); // 内存操作
}
// 获取对象属性(Field实例)的偏移量(地址值)
public long objectFieldOffset(Field f) {
if (f == null) {
throw new NullPointerException();
}
// 仅支持操作非静态属性。获取静态属性偏移量使用:staticFieldOffset(f)
return objectFieldOffset0(f);
}
5.2. volatile关键字
5.2.1. volatile关键字的基本概念
-
含义:易变的,不稳定的
-
作用:
用于修饰变量;
禁止编译器优化:让线程每次都从主内存中读/写数据,修改完成后,立即写回
使用场景:一个线程写,多个线程读
-
注意事项(误区):
volatile是非线程安全的,并不是用来实现同步的
保证变量的可见性:线程每次读取都是最新的值(因为每次都是从主内存中读取)
不能保证原子性:多线程操作volatile修饰的变量,不能保证原子性
5.2.2. volatile关键字的工作原理
5.2.3. volatile关键字可以保证可见性
示例代码:
// private static int num = 10; // 共享变量
private volatile static int num = 10; // 共享变量
public static void main(String[] args) {
// 多个线程读
Runnable reader = () -> {
while(num == 10) {
// nothing to do
}
String name = Thread.currentThread().getName();
System.out.println("线程" + name + "读取到的是:" + num);
};
for (int i = 1; i <= 3; i++) {
new Thread(reader).start();
}
try {
System.out.println("主线程休眠3秒...");
Thread.sleep(3000);
System.out.println("主线程休眠结束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
// 一个线程写
new Thread(() -> {
num = 15;
System.out.println("写线程完成写入。。。");
}).start();
}
输出结果:
主线程休眠3秒...
主线程休眠结束...
写线程完成写入。。。
线程Thread-0读取到的是:15
线程Thread-1读取到的是:15
线程Thread-2读取到的是:15
结论:
线程获取到主内存中的数据之后,会放到线程本地存储(ThreadLocalStorage,寄存器中),下次使用直接从线程本地存储中获取,要比从主内存中获取数据的效率高很多。当主内存中的变量用volatile修饰之后,线程每次使用数据,都必须重新读取主内存,这样就能够保证多个线程间获取的数据是一致的。volatile关键字使缓存失效,来确保可见性。
volatile关键字可以保证可见性。
5.2.3. volatile关键字不能保证原子性
示例代码:
private static volatile int num = 0;
public static void main(String[] args) {
Runnable add = () -> {
for (int i = 0; i < 1000; i++) {
num++; // 非原子操作
}
System.out.println(Thread.currentThread().getName() + "执行结果是:" + num);
};
for (int i = 0; i < 3; i++) {
new Thread(add).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终运算结果:" + num);
}
输出结果:
Thread-1执行结果是:1663
Thread-0执行结果是:2352
Thread-2执行结果是:1214
最终运算结果:2352
结论:
多线程操作共享变量时,由于该操作非原子操作,该操作的执行过程的任何一个时间点都有可能被CPU剥夺执行权,其它线程拿到执行权之后,获取到的数据可能还是前一个线程未完成写回的原数据,造成同一个值被多个线程多次读取,最终结果小于预期值。
volatile关键字不能保证原子性。
5.3. AtomicLong与LongAdder的区别
原理:
示例代码:
AtomicLong atomicLong = new AtomicLong();
LongAdder longAdder = new LongAdder();
long start = System.currentTimeMillis(); // 计时开始
Runnable adder = () -> {
for (int j = 0; j < 1000000; j++) {
// TODO 分别执行AtomicLong和LongAdder的自增方法
// atomicLong.incrementAndGet();
longAdder.increment();
}
};
for (int i = 0; i < 50; i++) {
new Thread(adder).start();
}
while (Thread.activeCount() > 2) { // ctrl-break; main;
}
// TODO 打印运算结果
// System.out.println("运算结果是:" + atomicLong.get());
System.out.println("运算结果是:" + longAdder.sum());
System.out.println("耗时:" + (System.currentTimeMillis() - start));
输出结果:
// LongAdder的输出结果
运算结果是:50000000
耗时:244
// LongAdder的输出结果
运算结果是:50000000
耗时:1130
结论:
将一个变量拆分成多个变量,多个线程各自操作自己的数据,大大减少多线程竞争CAS操作的情况,降低发生自旋的概率,从而提高效率。
在运算量较少的情况下,AtomicLong的性能可能会好于LongAdder,所以,后者适用于超高运算量的场景。