1.原子基本类型
1>.J.U.C并发包提供了多个原子基本类型:
AtomicBoolean
AtomicInteger
AtomicLong
...
2>.以AtomicInteger为例:
public class TestAtomicIntegerDemo01 {
public static void main(String[] args) {
//原子整型类
AtomicInteger i = new AtomicInteger(0);
// 先获取再自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement()); //0
// 先自增再获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet()); //2
// 先自减再获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet()); //1
// 先获取再自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement()); //1
// 先获取再加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5)); //0
// 先加值再获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5)); //0
// 先获取再更新(i = 0, v 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(v -> v - 2)); //0
// 先更新再获取(i = -2, v 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(v -> v + 2)); //0
// 先获取再计算(i = 0, v 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (v, x) -> v + x)); //0
// 先计算再获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (v, x) -> v + x)); //0
}
}
2.原子引用类型
1>.为什么需要原子引用类型?
因为程序中要保护的共享数据并不一定都是基本数据类型,也有对象类型,此时就需要通过原子引用类型进行保护;
2>.J.U.C并发包提供了多个原子引用类型:
AtomicReference
AtomicMarkableReference
AtomicStampedReference
...
3>.以AtomicReference为例:
public class TestAtomicReferenceDemo1 {
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("200")));
}
}
class DecimalAccountCas implements DecimalAccount {
//余额,共享变量,通过原子引用类型保护某个基本类型的共享数据
//将基本数据类型引用本身进行修改
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<BigDecimal>(balance);
}
//获取余额
@Override
public BigDecimal getBalance() {
return this.balance.get();
}
//取款
@Override
public void withdraw(BigDecimal amount) {
while (true) {
//获取(最新)余额
BigDecimal prev = this.balance.get();
//取款计算
BigDecimal next = prev.subtract(amount);
//将取款后的余额通过CAS机制更新到主内存中
if (this.balance.compareAndSet(prev, next)) {
//更新成功,结束循环;否则,继续重试;
break;
}
}
}
}
interface DecimalAccount {
// 获取余额
BigDecimal getBalance();
// 取款
void withdraw(BigDecimal amount);
/**
* 方法内会启动 20 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 200 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 20; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}
3.ABA问题及解决
3.1.ABA问题描述
1>.CAS算法实现的一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,这个时间差(当前线程修改数据期间)会导致数据的变化;
假设如下事件序列:有1,2两个线程,线程1耗时长,线程2耗时时间短:
- ①.线程1从内存位置V中取出A;
- ②.线程2从内存位置V中取出A;
- ③.线程2进行了一些操作,通过CAS操作将B(操作后的值)写入位置V,此时主内存中的值由A变成了B;
- ④.线程2通过CAS操作将A再次写入位置V,此时主内存中变量的值由B又恢复到A了;
- ⑤.线程1进行CAS操作,通过比较发现位置V中仍然是A,然后替换操作成功,但其实主内存中变量V的值(在线程1修改变量V期间)已经被修改过了,虽然值还是一样的,但是毕竟被其他线程修改过了,只是线程1不知道而已,这并不符合JMM的内存可见性;
尽管线程1的CAS操作成功,但不代表这个过程没有问题——对于线程1来说,线程2对变量的修改已经丢失了!
2>.代码如下:
@Slf4j
public class TestABAProblemDemo1 {
//原子引用类型保护共享变量
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.info("main start...");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
// 如果中间有其它线程干扰,发生了ABA现象
other();
TimeUnit.SECONDS.sleep(2);
// 尝试改为 C
log.info("change A->C {}", ref.compareAndSet(prev, "C"));
}
private static void other() throws InterruptedException {
new Thread(() -> {
log.info("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
log.info("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
}
主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A改为B,然后又改回A 的情况,如果主线程希望:只要有其它线程"动过了"共享变量,那么自己本次的cas就算失败,这时仅比较值是不够的,需要再加一个"版本号";
3.2.ABA问题解决方案一
1>.使用一个带时间戳的原子引用类型--AtomicStampedReference
@Slf4j
public class TestABAProblemDemo1 {
//使用一个带时间戳的原子引用类型保护共享变量,时间戳默认值为0或者1;
static AtomicStampedReference<String> ref = new AtomicStampedReference<String>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.info("main start...");
// 获取原始值A
String prev = ref.getReference();
// 获取主内存中共享数据此时版本号
log.info("修改之前的版本{}", ref.getStamp());
other();
TimeUnit.SECONDS.sleep(2);
// 尝试改为 C
// 此时不仅仅是比较共享变量的值,还要比较stamp版本号,而且修改完成之后stamp版本号还要加1,表示当前线程已经修改了该共享变量;
log.info("修改之前的版本{}", ref.getStamp());
log.info("change A->C {}", ref.compareAndSet(prev, "C", ref.getStamp(), ref.getStamp() + 1));
log.info("修改之后的版本{}", ref.getStamp());
}
private static void other() throws InterruptedException {
new Thread(() -> {
// 此时不仅仅是比较共享变量的值,还要比较stamp版本号,而且修改完成之后stamp版本号还要加1,表示当前线程已经修改了该共享变量;
log.info("修改之前的版本{}", ref.getStamp());
log.info("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
log.info("修改之后的版本{}", ref.getStamp());
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
// 此时不仅仅是比较共享变量的值,还要比较stamp版本号,而且修改完成之后stamp版本号还要加1,表示当前线程已经修改了该共享变量;
log.info("修改之前的版本{}", ref.getStamp());
log.info("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
log.info("修改之后的版本{}", ref.getStamp());
}, "t2").start();
}
}
2>.分析:
①.AtomicStampedReference可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A -> C,通过AtomicStampedReference我们可以知道,引用变量中途被更改了几次;
②.但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference原子标记引用类型;
3.3.ABA问题解决方案二
1>.使用可以标记共享变量是否发生过修改的原子标记引用类型--AtomicMarkableReference
@Slf4j
public class TestABAProblemDemo2 {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("(旧)垃圾袋装满了垃圾");
// 参数2 mark可以看作一个标记,表示垃圾袋满了!
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.info("主线程 start...");
GarbageBag prev = ref.getReference();
log.info(prev.toString());
new Thread(() -> {
log.info("打扫卫生的线程 start...,将垃圾袋中的垃圾全部倒掉...");
bag.setDesc("(旧)空垃圾袋");
//还是使用之前的旧垃圾袋
//第一个bag是期望值,第二个bag是目标值,这里的两个bag都是同一个对象的实例!
while (!ref.compareAndSet(bag, bag, true, false)) {}
log.info(bag.toString());
},"t1").start();
TimeUnit.SECONDS.sleep(1);
log.info("主线程想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("(新)空垃圾袋"), true, false);
//由于主线程中已经修改了共享数据,对象的初始标记发生了变化,这里再去修改就会失败!
log.info("换了么?" + success);
log.info(ref.getReference().toString());
}
}
@Data
@NoArgsConstructor
@Accessors(chain = true)
class GarbageBag {
String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
}
2>.分析:
4.原子数组
1>.如果要修改的不是(类型)引用本身,而是要修改引用对象里面(内部)的内容
.例如数组,有些时候多个线程并不是要修改数组的引用地址,而是要修改数组内存储的元素,此时之前的原子引用类型对象就无法实现了,但是别担心,JUC提供了原子数组,保护数组元素在多线程环境下的线程安全!
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
2>.代码示例:
public class TestAtomicArray {
public static void main(String[] args) {
demo(
() -> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.incrementAndGet(index),
(array) -> System.out.println(array)
);
}
/**
* 参数1,提供数组、可以是线程不安全数组或线程安全数组
* 参数2,获取数组长度的方法
* 参数3,元素自增方法
* 参数4,打印数组的方法
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->void
private static <T> void demo(Supplier<T> arraySupplier, Function<T, Integer> lengthFun,BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer) {
List<Thread> ts = new ArrayList<>();
T array = arraySupplier.get();
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10; j++) {
putConsumer.accept(array, j % length);
}
}));
}
ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等所有线程结束
printConsumer.accept(array);
}
}
数组中的元素都加到了10,这是正确的!
5.原子更新器(字段更新器)
1>.利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合volatile修饰的字段使用,否则会出现异常
;
Exception in thread “main” java.lang.IllegalArgumentException: Must be volatile type
2>.JUC提供的字段更新器对象:
AtomicReferenceFieldUpdater //引用类型字段
AtomicIntegerFieldUpdater //整型字段
AtomicLongFieldUpdater //长整形字段
3>.示例代码:
@Slf4j
public class TestAtomicFieldUpdater {
public static void main(String[] args) {
Student student = new Student();
//为某个类中某个类型的字段创建更新器对象
AtomicReferenceFieldUpdater referenceFieldUpdater =
AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
//更新对象中的某个属性值
boolean flag = referenceFieldUpdater.compareAndSet(student, null, "张三");
if (flag){
log.info(student.getName()); //张三
}
}
}
@Data
@NoArgsConstructor
class Student {
volatile String name;
}