Atomic包是Java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类。这个包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。
无锁
我们知道在进行线程切换的时候是需要进行上下文切换的,意思就是在切换线程的时候会保存上一任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。上下文切换也就是我们说的线程切换的时候所花费的时间和资源开销。因此,如何减少上下文切换是一种可以提高多线程并发效率的有效方案。这里的无锁正是一种减少上下文切换的技术。
对于并发控制而言,锁是一种悲观的策略。它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼。如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。而无锁是一种乐观的策略,它会假设对资源的访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。
那遇到冲突怎么办呢?无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
Java中的原子操作类
Java中的原子操作类大致可以分为4类:原子更新基本类型、原子更新数组类型、原子更新引用类型、原子更新属性类型。这些原子类中都是用了无锁的概念,有的地方直接使用CAS操作的线程安全的类型。
原子更新基本类型
- AtomicBoolean:原子更新布尔类型;
- AtomicInteger:原子更新整数类型;
- AtomicLong:原子更新长整型类型;
大致原理差不多,看看AtomicInteger:
每个方法都可以猜到其意思,下面演示一个例子
我们看看incrementAndGet()方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
通过调用unsafe类的方法完成+1并返回新值,Unsafe封装了一下不安全的操作,这是因为指针是不安全的,不正确的使用可能会造成意想不到的结果,因此JDK作者不希望用户使用这个类,只可以在JDK内部使用到。Atomic包里的类基本都是使用Unsafe这个类实现的。
原子更新引用类型
- AtomicReference:原子更新引用类型;
- AtomicStampedReference:原子更新带有版本号的引用类型;
- AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记为和引用类型;
AtomicReference是对普通的对象的引用,可以保证我们在修改对象应用的时候保证线程的安全性,举例如下:
private static AtomicReference<User> reference = new AtomicReference<>();
public static void main(String[] args) {
User user = new User("aa", "bb");
reference.set(user);
User newUser = new User("cc", "dd");
reference.compareAndSet(user, newUser);
}
static class User {
private String username;
private String pws;
public User(String username, String pws) {
this.username = username;
this.pws = pws;
}
}
这是一个简单的使用,但是有一个情况是需要注意的,因为在每次compareAndSet 的时候,假如我们预期的值被别的线程修改了,然后在又被其他线程修改会原来的状态了,如下图:
他不像操作AtomicInteger等一样,即使中间被修改,但是他是没有状态的,最后的记过不会受到影响,道理很简单,就是我们数学中的等式替换,但是对于AtomicReference 这种状态的迁移可能是一种灾难!例子如下:假设有一家咖啡店,为每一位会员卡余额小于20的会员一次性充值20元,以刺激消费。条件是只充值一次!
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
while (true) {//CAS保证更新成功
Integer m = money.get();
if (m < 20) {
if (money.compareAndSet(m, m + 20)) {
System.out.println("余额小于20,充值成功" + "余额为" + money.get());
break;
} else {
System.out.println("大于20,无需充值");
break;
}
}
}
}, "recharged thread" + i).start();
}
for (int i = 0; i < 200; i++) {
new Thread(() -> {
while (true) {
Integer m = money.get();
if (m > 10) {
System.out.println("大于10元,可以消费");
if (money.compareAndSet(m, m - 10)) {
System.out.println("消费成功,余额" + money.get());
break;
} else {
System.out.println("没有足够余额");
break;
}
}
}
}).start();
可以看出在账户充值的时候,会员可能正在消费,由于在充值的时候,判断的是账户余额是否小于20,如果是则进行充值,但是没有考虑到如何只充值一次的情况,因为他只是比较预期的值是否小于20,而无法判断该值的状态,所以账户被多次充值了,这就是因为AtomicReference无法表达状态的迁移!
AtomicStampedReference带有时间戳的对象引用类型
为了表述一个有状态迁移的AtomicReference而升级为带有时间戳的对象引用AtomicStampedReference,AtomicStampedReference 解决了上述对象在修改过程中,丢失状态信息的问题,使得对象的值不仅与预期的值相比较,还通过时间戳进行比较,这就可以很好的解决对象被反复修改导致线程无法正确判断对象状态的问题。把上述的代码改成使用AtomicStampedReference 的方式如下:
for (int i = 0; i < 10000; i++) {
final int timeStamp = money.getStamp();
new Thread(() -> {
while (true) {//CAS保证更新成功
Integer m = money.getReference();
if (m < 20) {
if (money.compareAndSet(m, m + 20, timeStamp, timeStamp + 1)) {
System.out.println("余额小于20,充值成功" + "余额为" + money.getReference());
break;
} else {
System.out.println("大于20,无需充值");
break;
}
}
}
}, "recharged thread" + i).start();
}
for (int i = 0; i < 200; i++) {
new Thread(() -> {
while (true) {
Integer m = money.getReference();
int stamp = money.getStamp();
if (m > 10) {
System.out.println("大于10元,可以消费");
if (money.compareAndSet(m, m - 10, stamp, stamp + 1)) {
System.out.println("消费成功,余额" + money.getReference());
break;
} else {
System.out.println("没有足够余额");
break;
}
}
}
}).start();
}
执行结果:
原子更新数组类型
- AtomicIntegerArray:原子更新整数型数组里的元素;
- AtomicLongArray:原子更新长整型数组里的元素;
- AtomicReferenceArray:原子更新引用类型数组里的元素;
private static int[] value = new int[]{1, 2, 3};
private static AtomicIntegerArray atomic = new AtomicIntegerArray(value);
public static void main(String[] args) {
atomic.getAndSet(2, 100);
System.out.println(atomic.get(2));
}
原子更新属性类型
如果需要原子地更新某个类里的某个字段时,就需要使用原子更新字段值,主要有下边三个:
- AtomicIntegerFieldUpdater:原子更新整数型字段;
- AtomicLongFieldUpdater:原子更新长整型字段;
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
private static AtomicIntegerFieldUpdater<User> atoic =
AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public static void main(String[] args) {
User user = new User("luoqiuyun", 23);
System.out.println(atoic.getAndIncrement(user));
System.out.println(atoic.get(user));
}
@AllArgsConstructor
static class User {
private String username;
private volatile int age;
}