有如下需求,能保证 account.withdraw 取款方法的线程安全嘛?
package com.jsxs.Test;
import java.util.ArrayList;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/10/11 14:58
* @PackageName:com.jsxs.Test
* @ClassName: Account
* @Description: TODO
* @Version 1.0
*/
public interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end - start) / 1000_000 + " ms");
}
}
原有实现并不是线程安全的
package com.jsxs.Test;
/**
* @Author Jsxs
* @Date 2023/10/11 14:59
* @PackageName:com.jsxs.Test
* @ClassName: AccountUnsafe
* @Description: TODO
* @Version 1.0
*/
public class AccountUnsafe implements Account{
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withdraw(Integer amount) {
balance -= amount;
}
// 进行测试
public static void main(String[] args) {
Account.demo(new AccountUnsafe(10000));
}
}
某次的执行结果
withdraw
方法: 对共享资源产生了竞态条件。
public void withdraw(Integer amount) {
balance -= amount;
}
对应的字节码
ALOAD 0 // <- this
ALOAD 0
GETFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // <- this.balance
INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱
ALOAD 1 // <- amount
INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱
ISUB // 减法
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; // 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // -> this.balance
多线程执行流程
ALOAD 0 // thread-0 <- this
ALOAD 0
GETFIELD cn/itcast/AccountUnsafe.balance // thread-0 <- this.balance
INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱
ALOAD 1 // thread-0 <- amount
INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱
ISUB // thread-0 减法
INVOKESTATIC java/lang/Integer.valueOf // thread-0 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance // thread-0 -> this.balance
ALOAD 0 // thread-1 <- this
ALOAD 0
GETFIELD cn/itcast/AccountUnsafe.balance // thread-1 <- this.balance
INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱
ALOAD 1 // thread-1 <- amount
INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱
ISUB // thread-1 减法
INVOKESTATIC java/lang/Integer.valueOf // thread-1 结果装箱
PUTFIELD cn/itcast/AccountUnsafe.balance // thread-1 -> this.balance
首先想到的是给 Account 对象加锁
package com.jsxs.Test;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author Jsxs
* @Date 2023/10/11 15:15
* @PackageName:com.jsxs.Test
* @ClassName: AccountSafe
* @Description: TODO
* @Version 1.0
*/
public class AccountSafe implements Account {
// 1. AtomicInteger 原子性整形 ⭐
private AtomicInteger balance;
public AccountSafe(Integer balance) {
this.balance = new AtomicInteger(balance); // ⭐⭐ 通过AtomicInteger 进行构造
}
@Override
public Integer getBalance() {
return balance.get(); // ⭐⭐⭐ 调用原子类的get方法
}
@Override
public void withdraw(Integer amount) {
while (true) {
// 获取余额的最新值 调用get方法 ⭐⭐⭐
int prev = balance.get();
// 修改后的余额=余额的最新值减去取款金额 ⭐⭐⭐⭐
int next = prev - amount;
// 局部变量都是存储在线程的工作内存上(并不是主存上), 调用这个方法就是讲内存和主存同步 ⭐⭐⭐⭐⭐
if (balance.compareAndSet(prev, next)) { // 第一个参数: 修改前的值,第二个参数: 修改后的值
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
public static void main(String[] args) {
Account.demo(new AccountSafe(10000));
}
}
前面看到的 AtomicInteger
的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
public void withdraw(Integer amount) {
// 需要不断尝试,直到成功为止
while (true) {
// 比如拿到了旧值 1000
int prev = balance.get();
// 在这个基础上 1000-10 = 990
int next = prev - amount;
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
- 不一致了,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 990
那么本线程的这次 990 就作废了,进入 while 下次循环重试
- 一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
解释:
比如说线程1先获取余额为100,然后减去10结果为90,此时还没有来得及进行cas的操作,线程2已经把原来100的值修改为了90。经比较线程2的将prev90更新到主存,主存prev为90与线程1获取的prev值100是不相等,那么返回fasle,线程1再次进行重新-10操作.
注意
其实 CAS
的底层是 lock cmpxchg
指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换
】的原子性。
在多核状态下,某个核执行到带 lock
的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
1. 设置线程数为1,然后在CAS的地方进行断点
2.查看断点达到的信息,并且给共享变量赋值9000
3.获取最新值再次比较,如果其他线程没有再次改变那么就返回true
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
它可以用来修饰成员变量
和静态成员变量
,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile
变量的修改,对另一个线程可见。
注意
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,也能保证程序的有序性,但不能解决指令交错问题(不能保证原子性)
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
线程数不超过CPU的核定线程数都很高效,假如超过了效率就会大幅降低。
结合 CAS
和 volatile
可以实现无锁并发
,适用于线程数少、多核 CPU 的场景下。
J.U.C 并发包提供了:
以 AtomicInteger 为例
原子类的所有方法都是线程安全的,都具有原子性且CAS自旋的操作。所以刚才的取账问题我们可以用 addAndGet() 方法进行替换。
package com.jsxs.Test;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author Jsxs
* @Date 2023/10/11 17:11
* @PackageName:com.jsxs.Test
* @ClassName: Test09
* @Description: TODO
* @Version 1.0
*/
public class Test09 {
public static void main(String[] args) {
AtomicInteger integer = new AtomicInteger(1);
System.out.println("先获取再自增的操作" + integer.getAndIncrement()); // i++
System.out.println("先自增再获取的操作:" + integer.incrementAndGet()); // ++i
System.out.println("先自减再获取的操作:"+integer.decrementAndGet()); // --i
System.out.println("先获取再自检的操作:"+integer.getAndDecrement()); // i--
System.out.println("最终得值为:"+integer);
System.out.println("================================");
System.out.println("自定义后增加:"+integer.getAndAdd(4)); // 先获取再+4
System.out.println("最终得值为:"+integer);
System.out.println("自定义先增加:"+integer.addAndGet(4)); // 先自增4 在获取
System.out.println("最终得值为:"+integer);
System.out.println("================================");
}
}
package com.jsxs.Test;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author Jsxs
* @Date 2023/10/11 17:11
* @PackageName:com.jsxs.Test
* @ClassName: Test09
* @Description: TODO
* @Version 1.0
*/
public class Test09 {
public static void main(String[] args) {
AtomicInteger integer = new AtomicInteger(1);
// 这里是一个接口类型返回值类型是int,且参数类型是int, value是读取到的值, value*10是设置的值 会发生CAS自旋的操作
System.out.println(integer.updateAndGet((value)->{return value*10;})); // 先修改在获取之
System.out.println(integer.getAndUpdate((value)->{return value*10;})); // 先获取值再修改
}
}
为什么需要原子引用类型?
有如下方法
package com.jsxs.Test;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/10/11 19:28
* @PackageName:com.jsxs.Test
* @ClassName: DecimalAccount
* @Description: TODO
* @Version 1.0
*/
public interface DecimalAccount {
// 获取余额: 钱币类型
BigDecimal getBalance();
// 取款
void withdraw(BigDecimal amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; 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());
}
}
package com.jsxs.Test;
import java.math.BigDecimal;
/**
* @Author Jsxs
* @Date 2023/10/11 19:30
* @PackageName:com.jsxs.Test
* @ClassName: DecimalAccountUnsafe
* @Description: TODO
* @Version 1.0
*/
public class DecimalAccountUnsafe implements DecimalAccount {
BigDecimal balance;
public DecimalAccountUnsafe(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountUnsafe(new BigDecimal("10000")));
}
}
package com.jsxs.Test;
import java.math.BigDecimal;
/**
* @Author Jsxs
* @Date 2023/10/11 20:09
* @PackageName:com.jsxs.Test
* @ClassName: DecimalAccountSafeLock
* @Description: TODO
* @Version 1.0
*/
public class DecimalAccountSafeLock implements DecimalAccount{
private final Object lock= new Object();
BigDecimal balance;
public DecimalAccountSafeLock(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
synchronized (lock){
BigDecimal balance = this.getBalance();
// 旧余额 - 取款 = 新余额
this.balance = balance.subtract(amount);
}
}
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountSafeLock(new BigDecimal("10000")));
}
}
package com.jsxs.Test;
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Author Jsxs
* @Date 2023/10/11 19:38
* @PackageName:com.jsxs.Test
* @ClassName: DecimalAccountSafeCas
* @Description: TODO
* @Version 1.0
*/
public class DecimalAccountSafeCas implements DecimalAccount {
// 1.我们这里使用原子引用类型进行修饰 ⭐
AtomicReference<BigDecimal> ref;
public DecimalAccountSafeCas(BigDecimal balance) {
// 2.创建我们的原子引用类 ⭐⭐
ref = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
// 3.调用get()返回最新值 ⭐⭐⭐
return ref.get();
}
@Override
public void withdraw(BigDecimal amount) {
// 4. CAS ⭐⭐⭐⭐
while (true) {
// 获取现在的余额 (旧值)
BigDecimal prev = ref.get();
// 返回的新余额(新值) = 现在的余额 - 取款金额
BigDecimal next = prev.subtract(amount);
if (ref.compareAndSet(prev, next)) { // 进行我们的CAS操作, 主存与新值进行比较,一样则false。
break;
}
}
}
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountSafeCas(new BigDecimal("10000"))); // 构造金额为10000
}
}
- 无线程冲突的时候
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Author Jsxs
* @Date 2023/10/11 20:18
* @PackageName:com.jsxs.Test
* @ClassName: Test10
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test10")
public class Test10 {
// 1. 设置原子引用类型
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.get();
Thread.sleep(1000);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
}
- ABA问题的展现
A->B->A->C
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Author Jsxs
* @Date 2023/10/11 20:18
* @PackageName:com.jsxs.Test
* @ClassName: Test10
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test10")
public class Test10 {
// 1. 设置原子引用类型
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
other();
Thread.sleep(1000);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "A"));
}
private static void other() throws InterruptedException {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
}
- 估计将A的地址进行修改之后
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Author Jsxs
* @Date 2023/10/11 20:18
* @PackageName:com.jsxs.Test
* @ClassName: Test10
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test10")
public class Test10 {
// 1. 设置原子引用类型
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
other();
Thread.sleep(1000);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "A"));
}
private static void other() throws InterruptedException {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.get(), new String("A")));
}, "t2").start();
}
}
我们发现ABA问题将不成立!!!!
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号。
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @Author Jsxs
* @Date 2023/10/11 20:18
* @PackageName:com.jsxs.Test
* @ClassName: Test10
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test10")
public class Test10 {
// 1. 设置原子版本引用类型
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号 (提前获取⭐在其他线程进入前)
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
Thread.sleep(1000);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1)); // 判断stamp当前版本号,与主存中的版本号是否一致
}
private static void other() throws InterruptedException {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();
}
}
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。
但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference.
package com.jsxs.Test;
/**
* @Author Jsxs
* @Date 2023/10/11 21:23
* @PackageName:com.jsxs.Test
* @ClassName: GarbageBag
* @Description: TODO
* @Version 1.0
*/
public class GarbageBag {
String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return super.toString() + " " + desc;
}
}
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicMarkableReference;
/**
* @Author Jsxs
* @Date 2023/10/11 21:24
* @PackageName:com.jsxs.Test
* @ClassName: TestABAAtomicMarkableReference
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test007")
public class TestABAAtomicMarkableReference {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,true表示垃圾袋满了 ⭐
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.debug("主线程 start...");
// 1.获取余额
GarbageBag prev = ref.getReference();
log.debug(prev.toString());
// 2.保洁阿姨打扫卫生...
new Thread(() -> {
log.debug("打扫卫生的线程 start...");
bag.setDesc("空垃圾袋");
// 3. 保洁阿姨对判断是否是空垃圾袋 (也就是新的和主存是否一致)
while (!ref.compareAndSet(bag, bag, true, false)) {
}
log.debug(bag.toString());
}).start();
Thread.sleep(1000);
log.debug("主线程想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(ref.getReference().toString());
}
}
依然是同一个垃圾袋...
原子数组主要保护的是我们数组中的元素!!!
package com.jsxs.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* @Author Jsxs
* @Date 2023/10/13 19:15
* @PackageName:com.jsxs.Test
* @ClassName: Test11
* @Description: TODO
* @Version 1.0
*/
public class Test11 {
public static void main(String[] args) {
demo(
()->new int[10], // ⭐非原子数组的创建
(array)->array.length,
(array,index)->array[index]++,
(array)-> System.out.println(Arrays.toString(array))
);
}
/**
* 参数1,提供数组、可以是线程不安全数组或线程安全数组
* 参数2,获取数组长度的方法
* 参数3,自增方法,回传 array, index
* 参数4,打印数组的方法
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T> void demo(
Supplier<T> arraySupplier, // 1.没有参数一个结果
Function<T, Integer> lengthFun, // 2. 一个参数一个结果
BiConsumer<T, Integer> putConsumer, // 3.两个参数,一个结果的
Consumer<T> printConsumer) { // 4.一个参数,但没有返回值 (用作打印)
List<Thread> ts = new ArrayList<>();
// 1.获取数组
T array = arraySupplier.get();
// 2.设置数组的长度
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; 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);
}
}
package com.jsxs.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* @Author Jsxs
* @Date 2023/10/13 19:15
* @PackageName:com.jsxs.Test
* @ClassName: Test11
* @Description: TODO
* @Version 1.0
*/
public class Test11 {
public static void main(String[] args) {
demo(
()->new AtomicIntegerArray(10), // 1.创建数组
(array)->array.length(), // 2. 提供长度
(array,index)->array.getAndIncrement(index), // 3.对原子数组中的元素(下标)进行 +1 的操作
(array)-> System.out.println(array)
);
}
/**
* 参数1,提供数组、可以是线程不安全数组或线程安全数组
* 参数2,获取数组长度的方法
* 参数3,自增方法,回传 array, index
* 参数4,打印数组的方法
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T> void demo(
Supplier<T> arraySupplier, // 1.没有参数一个结果
Function<T, Integer> lengthFun, // 2. 一个参数一个结果
BiConsumer<T, Integer> putConsumer, // 3.两个参数,一个结果的
Consumer<T> printConsumer) { // 4.一个参数,但没有返回值 (用作打印)
List<Thread> ts = new ArrayList<>();
// 1.获取数组
T array = arraySupplier.get();
// 2.设置数组的长度
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; 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);
}
}
字段更新器保护的是对象的属性和成员变量!!!
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* @Author Jsxs
* @Date 2023/10/13 20:10
* @PackageName:com.jsxs.Test
* @ClassName: Test12
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test12")
public class Test12 {
public static void main(String[] args) {
Student student = new Student();
// 1.第一个参数: 类名; 第二个参数: 参数类型; 第三个: 参数名
AtomicReferenceFieldUpdater updater =AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
// 1.第一个参数: 对象名; 第二个参数:属性的原始值; 第三个参数: 修改成什么值
updater.compareAndSet(student,null,"张三");
}
}
class Student {
String name; // 我们这里没有使用 voliate
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
为什么要添加voliatile进行修饰呢? 为了保证我们保护的成员变量的可见性!!!
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* @Author Jsxs
* @Date 2023/10/13 20:10
* @PackageName:com.jsxs.Test
* @ClassName: Test12
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test12")
public class Test12 {
public static void main(String[] args) {
Student student = new Student();
// 1.第一个参数: 类名; 第二个参数: 参数类型; 第三个: 参数名
AtomicReferenceFieldUpdater updater =AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
// 1.第一个参数: 对象名; 第二个参数:属性的原始值; 第三个参数: 修改成什么值
updater.compareAndSet(student,null,"张三");
System.out.println(student.toString());
}
}
class Student {
volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* @Author Jsxs
* @Date 2023/10/13 20:10
* @PackageName:com.jsxs.Test
* @ClassName: Test12
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test12")
public class Test12 {
public static void main(String[] args) {
Student student = new Student();
// 1.第一个参数: 类名; 第二个参数: 参数类型; 第三个: 参数名 ⭐
AtomicReferenceFieldUpdater updater =AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
// 2.第一个参数: 对象名; 第二个参数:属性的原始值; 第三个参数: 修改成什么值 ⭐⭐
updater.compareAndSet(student,null,"张三");
System.out.println(student.toString());
// 3.原始值指的正确,那么会成功。⭐⭐⭐
updater.compareAndSet(student,"张三","张四");
System.out.println(student.toString());
// 4.原始值指的更改过的,那么会失败。 底层原理还是CAS进行比较的 ⭐⭐⭐⭐
updater.compareAndSet(student,"张三","张五");
System.out.println(student.toString());
}
}
class Student {
volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
累加器性能比较
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* @Author Jsxs
* @Date 2023/10/13 20:33
* @PackageName:com.jsxs.Test
* @ClassName: Test13
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test13")
public class Test13 {
public static void main(String[] args) {
demo(
()->new AtomicLong(0), // 1.设置我们的原子长整型
(adder)->adder.getAndIncrement() //2.进行自增的操作
);
System.out.println("======上面是原子长整型类,下面是长整型累加器=======");
demo(
()->new LongAdder(),
(adder)->adder.increment()
);
}
/**
*
* @param adderSupplier : ()->结果 ,提供累加器对象
* @param action : (参数)-> 无返回结果, 提供累加操作
* @param
*/
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
long start = System.nanoTime();
List<Thread> ts = new ArrayList<>();
// 4 个线程,每人累加 50 万
for (int i = 0; i < 40; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
ts.forEach(t -> t.start());
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(adder + " cost:" + (end - start) / 1000_000);
}
}
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。相当于不共享一个累加单元,每一个线程都有一个自己的累加单元!!!
LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧
LongAdder 类有几个关键域
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;
CAS锁的源码实践,但是实际开发中我们不用用下面这样的代码进行开发
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author Jsxs
* @Date 2023/10/15 11:45
* @PackageName:com.jsxs.Test
* @ClassName: LockCas
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test")
public class LockCas {
// 0的时候表示没有加锁,1的时候表示已经加锁
private AtomicInteger state = new AtomicInteger(0);
public void lock() {
while (true) {
if (state.compareAndSet(0, 1)) {
break;
}
}
}
public void unlock() {
log.debug("unlock...");
state.set(0);
}
public static void main(String[] args) {
LockCas lock = new LockCas();
// 1.线程1执行完毕解锁之后,线程2才能继续执行
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
log.debug("begin...");
lock.lock();
try {
log.debug("lock...");
} finally {
lock.unlock();
}
}).start();
}
}
其中 Cell 即为累加单元
@sun.misc.Contended //⭐注解: 防止缓存行伪共享
static final class Cell {
volatile long value;
Cell(long x) {
value = x;
}
// 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
final boolean cas(long prev, long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
// 省略不重要代码
}
得从缓存说起,缓存与内存的速度比较
因为 CPU 与 内存 的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。 也就是相当于取两份。
CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效。
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:
无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效
@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。
public void add(long x) {
// as 为累加单元数组
// b 为基础值
// x 为累加值
Cell[] as;
long b, v;
int m;
Cell a;
// 进入 if 的两个条件
// 1. as 有值, 表示已经发生过竞争, 进入 if
// 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
if ((as = cells) != null || !casBase(b = base, b + x)) {
// uncontended 表示 cell 没有竞争
boolean uncontended = true;
if (
// as 还没有创建
as == null || (m = as.length - 1) < 0 ||
// 当前线程对应的 cell 还没有
(a = as[getProbe() & m]) == null ||
// cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
!(uncontended = a.cas(v = a.value, v + x))
) {
// 进入 cell 数组创建、cell 创建的流程
longAccumulate(x, null, uncontended);
}
}
}
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得。
package com.jsxs.Test;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @Author Jsxs
* @Date 2023/10/18 21:00
* @PackageName:com.jsxs.Test
* @ClassName: Test14
* @Description: TODO
* @Version 1.0
*/
public class Test14 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 1.通过反射指定属性名获取具体属性
Field unsafe = Unsafe.class.getDeclaredField("theUnsafe");
// 2.因为是私有的成员变量,所以我们需要先设置Accessible
unsafe.setAccessible(true);
// 3.返回我们的真正的UnSafe对象
Unsafe o = (Unsafe)unsafe.get(null);
System.out.println(o);
}
}
package com.jsxs.Test;
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @Author Jsxs
* @Date 2023/10/18 21:00
* @PackageName:com.jsxs.Test
* @ClassName: Test14
* @Description: TODO
* @Version 1.0
*/
public class Test14 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 1.通过反射指定属性名获取具体属性
Field o = Unsafe.class.getDeclaredField("theUnsafe");
// 2.因为是私有的成员变量,所以我们需要先设置Accessible
o.setAccessible(true);
// 3.返回我们的真正的UnSafe对象
Unsafe unsafe = (Unsafe)o.get(null);
System.out.println(o);
// 1.获取欲的偏移地址 : 获取Student1中id属性的偏移地址
long idOffset = unsafe.objectFieldOffset(Student1.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Student1.class.getDeclaredField("name"));
Student1 student1 = new Student1();
// 2.执行 cas 操作
unsafe.compareAndSwapInt(student1,idOffset,0,1); // 第一个: 对象, 第二个: id的偏移地址,第三个: 旧值,第四个: 新值
unsafe.compareAndSwapObject(student1,nameOffset,null,"张三"); // 第一个: 对象, 第二个: id的偏移地址,第三个: 旧值,第四个: 新值
// 3.查看我们是否成功!!
System.out.println(student1.id+" "+ student1.name);
}
}
@Data
class Student1 {
volatile int id;
volatile String name;
}
使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现。
1.Account接口
package com.jsxs.Test;
import java.util.ArrayList;
import java.util.List;
/**
* @Author Jsxs
* @Date 2023/10/11 14:58
* @PackageName:com.jsxs.Test
* @ClassName: Account
* @Description: TODO
* @Version 1.0
*/
public interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
// 每次取10块钱
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end - start) / 1000_000 + " ms");
}
}
2.实现
package com.jsxs.Test;
import com.jsxs.utils.UnSafeUtils;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @Author Jsxs
* @Date 2023/10/18 21:32
* @PackageName:com.jsxs.Test
* @ClassName: Test15
* @Description: TODO
* @Version 1.0
*/
public class Test15 {
public static void main(String[] args) {
Account.demo(new MyAtomicInteger(10000));
}
}
class MyAtomicInteger implements Account{
volatile int value;
static final long valueOffset;
static Unsafe unsafe;
static {
try {
// 1.通过反射机制进行获取UnSafe ⭐
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
// 2.获取我们偏移域
valueOffset=unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
public int getValue() {
return value;
}
public void decrement(int amount){
while (true){
// 1.旧值
int prev = this.value;
// 2.新值
int next=prev-amount;
// 3.利用 UnSafe 进行设置 ⭐⭐
if (unsafe.compareAndSwapInt(this,valueOffset,prev,next)){
break;
}
}
}
@Override
public Integer getBalance() {
return getValue();
}
public MyAtomicInteger(int value) {
this.value = value;
}
@Override
public void withdraw(Integer amount) {
decrement(amount);
}
}
下面的代码在运行时,由于 SimpleDateFormat 不是线程安全的。
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
/**
* @Author Jsxs
* @Date 2023/10/19 20:17
* @PackageName:com.jsxs.Test
* @ClassName: Test16
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "test16")
public class Test16 {
public static void main(String[] args) {
// 1.进行我们的序列化操作
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 2.开启十个线程进行解析
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
有很大几率出现 java.lang.NumberFormatException (数字类型错误
) 或者出现不正确的日期解析结果,例如:
这样虽能解决问题,但带来的是性能上的损失,并不算很好:
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
/**
* @Author Jsxs
* @Date 2023/10/19 20:17
* @PackageName:com.jsxs.Test
* @ClassName: Test16
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test16")
public class Test16 {
public static void main(String[] args) {
// 1.进行我们的序列化操作
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 2.开启十个线程进行解析
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (sdf) { // ⭐
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}
}).start();
}
}
}
如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊
!这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:
package com.jsxs.Test;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* @Author Jsxs
* @Date 2023/10/19 20:17
* @PackageName:com.jsxs.Test
* @ClassName: Test16
* @Description: TODO
* @Version 1.0
*/
@Slf4j(topic = "c.test16")
public class Test16 {
public static void main(String[] args) {
// 1. 利用 线程安全的方法
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
log.debug("{}", date);
}).start();
}
}
}
另一个大家更为熟悉的 String 类也是不可变的,以它为例,说明一下不可变设计的要素
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/**
* The value is used for character storage.
*/
private final char value[];
/**
* Cache the hash code for the string
*/
private int hash; // Default to 0
// ...
}
发现该类、类中所有属性都是 final 的
但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 substring 为例:
深拷贝!!!: 实质上也就是创建了一个新的类。
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出了修改:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】。
定义 英文名称:Flyweight pattern
. 当需要重用数量有限的同一类对象时。
- 包装类
在JDK中 Boolean
,Byte
,Short
,Integer
,Long
,Character
等包装类提供了 valueOf 方法,例如 Long 的 valueOf
会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象:
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int) l + offset];
}
return new Long(l);
}
-Djava.lang.Integer.IntegerCache.high
来改变例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
class Pool {
// 1. 连接池大小
private final int poolSize;
// 2. 连接对象数组
private Connection[] connections;
// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;
// 4. 构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i + 1));
}
}
// 5. 借连接
public Connection borrow() {
while (true) {
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if (states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 如果没有空闲连接,当前线程进入等待
synchronized (this) {
try {
log.debug("wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
synchronized (this) {
log.debug("free {}", conn);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection {
// 实现略
}
理解了 volatile 原理,再对比 final 的实现就比较简单了
public class TestFinal {
final int a = 20;
}
字节码
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
<-- 写屏障
10: return
发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况。 因为finnal不能进行修改的特点。
Monitor 原理
在 web 阶段学习时,设计 Controller 时为了保证其线程安全,都会有这样的建议,不要为 Controller 设置成员变量,这种没有任何成员变量的类是线程安全的。
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】