示例引入
我们需要执行一个高并发削减账户余额的逻辑。为方便起见,我们将账户类设计为一个抽象类,并且只对中的静态demo()
方法进行了实现。
在demo()
方法中,我们会生成若干线程,每个线程执行同样的逻辑:扣除账户中的余额。当全部线程执行完毕后,方法会打印账户的余额数量以及执行程序的用时。
abstract class Account{
abstract String getName();
abstract int getBalance();
abstract void withdraw(int amount);
static void demo(Account account, int listNum, int preAmount){
// 启动时间延迟5秒,确保所有线程全部启动,同时执行
long start = System.currentTimeMillis() + 5000;
Runnable task = () -> {
try {
Thread.sleep(start - System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
account.withdraw(preAmount);
}
};
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < listNum; i++) {
threadList.add(new Thread(task, "t"+i));
}
// 启动所有线程
threadList.forEach(Thread::start);
// 等待所有线程结束
threadList.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
log.info("{} : balance = {}, use {} ms",account.getName(),account.getBalance(), end - start);
}
}
不加锁的实现方式
此方法没有在扣除余额时加锁,可能会造成线程安全问题。
class UnsafeAccount extends Account{
private int balance;
public UnsafeAccount(int balance) {
this.balance = balance;
}
@Override
String getName() {
return "Unsafe";
}
@Override
int getBalance() {
return balance;
}
@Override
void withdraw(int amount) {
balance -= amount;
}
}
使用synchronized进行加锁
使用synchronized对变量balance
的读写进行加锁,可以保证线程安全。
class SynchronizedAccount extends Account{
private int balance;
public SynchronizedAccount(int balance) {
this.balance = balance;
}
@Override
String getName() {
return "Synchronize";
}
@Override
synchronized int getBalance() {
return balance;
}
@Override
synchronized void withdraw(int amount) {
balance -= amount;
}
}
使用原子变量
原子变量底层使用CAS机制,它能够保证共享数据在多线程间安全的同时,也无需加锁。
class CasAccount extends Account{
private AtomicInteger balance;
public CasAccount(int balance) {
this.balance = new AtomicInteger(balance);
}
@Override
String getName() {
return "Cas";
}
@Override
int getBalance() {
return balance.get();
}
@Override
void withdraw(int amount) {
balance.addAndGet(-amount);
}
}
实验
我们在主函数中定义初始余额为50000,一共创建5000个线程去扣减余额,每次扣减的数额为1。由于每个线程会扣减10次,因此最后正确的余额应该为0才对。
public class AccountSubDemo{
public static void main(String[] args) {
int initBalance = 50000;
int listNum = 5000;
int preAmount = 1;
Account unsafeAccount = new UnsafeAccount(initBalance);
Account.demo(unsafeAccount,listNum,preAmount);
Account synchronizedAccount = new SynchronizedAccount(initBalance);
Account.demo(synchronizedAccount,listNum,preAmount);
Account casAccount = new CasAccount(initBalance);
Account.demo(casAccount,listNum,preAmount);
}
}
下面是函数的执行结果。
[5290 ms] [INFO][main] i.k.e.c.ex.Account : Unsafe : balance = 122, use 138 ms
[10641 ms] [INFO][main] i.k.e.c.ex.Account : Synchronize : balance = 0, use 348 ms
[15715 ms] [INFO][main] i.k.e.c.ex.Account : Cas : balance = 0, use 74 ms
从结果中容易看出,未加锁时,会出现线程安全问题,导致最终余额不为0。使用synchronized加锁的代码,能够正确执行结果,但是它的耗时比不加锁的长了一倍。使用原子变量的代码,在能够正确执行结果的同时,其耗时也十分优秀。
CAS工作方式
在前面原子变量的解决方案中,其方法内部并不是通过加锁来保护共享变量的线程安全的。其内部保护线程安全的关键为CAS(Compare And Set或者Compare And Swap),该操作必须是原子的。
cas需要两个重要参数:exceptedValue和newValue。当且仅当目标变量的值为exceptedValue时,他才会将其修改为newValue。下面通过一个例子进行展示。
CAS底层原理
CAS的底层是lock cmpxch指令(X86架构),在单核CPU和多核CPU下都能够保证CAS的原子性。
在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,因此是原子的。
CAS与volital
CAS操作需要volital的支持:获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。
CAS必须借助volatile才能读取到共享变量的最新值来实现比较-交换操作。
CAS的效率分析
- 无锁情况下,即使重试失败,线程依然在高速运行,没有停歇。
- 而synchronized会让线程在没有获得锁的时候发生上下文切换,进入阻塞。
另一方面:
- 无锁情况下,因为线程要保持运行,需要额外CPU的支持,如果线程没有分到时间片,仍然会发生上下文切换。
因此:
- 要想最大限度发挥CAS的优势,必须要多线程支持,而且线程数少于CPU的核心数。
特点
CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改贡献变量,就算改了也没有关系,自己吃亏再重试就行。
synchronized是基于悲观锁的思想:最悲观的估计,得防着其他变量来修改共享变量,我上了锁你们都别想该,我用完了解开锁,别人才有机会。
CAS是无锁并发,无阻塞并发。
- 无锁并发:不是使用加锁的方式来保护共享资源,而是采用不断重试的方式来保证共享资源的正确性;
- 无阻塞并发:线程可以持续运行,不会等待其它线程完成或者释放锁。
原子变量
在JUC包下,Java基于CAS提供了一些原子变量工具类。
原子整数
原子整数包括AtomicBoolean
、AtomicInteger
、AtomicLong
。由于它们功能比较类似,这里选取AtomicInteger
进行讲解。
基础CAS操作
AtomicInteger i = new AtomicInteger(0);
// 只有当i为0时,才将其赋值为1
i.compareAndSet(0,1);
CAS封装的简单API
i.getAndIncrement(); // 等价于 i++
i.incrementAndGet(); // 等价于 ++i
i.getAndDecrement(); // 等价于 i--
i.decrementAndGet(); // 等价于 --i
i.getAndAdd(5);// 先取值,再求和
i.addAndGet(5);// 先求和,再取值
CAS封装的复杂API
上面的简单API只能完成加法、减法的操作,如果业务需要跟复杂的计算(比如乘除法),则需要使用getAndUpdate()
和getAndAccumulate()
方法getAndUpdate()
的签名如下,它需要传入一个IntUnaryOperator
对象。
public final int getAndUpdate(IntUnaryOperator updateFunction)
点进IntUnaryOperator
类中,可以发现它是一个注解为@FunctionalInterface
接口,需要实现的方法为int applyAsInt(int operand);
。因此,我们可以配合lambda表达式使用方法getAndUpdate()
。
举例:例如,我们希望让结果乘2,可以按照如下的方式使用
i.getAndUpdate(a -> a*2);
方法getAndUpdate()
的签名如下,它需要传入一个IntUnaryOperator
对象。
public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)
点进IntBinaryOperator
类中,可以发现它是一个注解为@FunctionalInterface
接口,需要实现的方法为int applyAsInt(int left, int right);
。因此,我们可以配合lambda表达式使用方法getAndAccumulate()
。这里表示要想计算最终结果,还需要另一个参数x
。
举例:例如,我们希望让结果乘x,可以按照如下的方式使用
i.getAndAccumulate(3, (pre,x) -> pre*x);
注意:再运算时,原数值pre
在左,新参数x
在右。
如果计算需要的参数超过一个,改怎么办?老老实实用compareAndSet()
方法吧。
举例:例如,我们希望让结果乘x再加上y,可以按照如下的方式使用
while (true){
int pre = i.get();
int newValue = pre*x+y;
if (i.compareAndSet(pre,newValue)){
break;
}
}
此外,getAndUpdate()
和getAndAccumulate()
返回的均为计算前的数值,而updateAndGet()
和accumulateAndGet()
返回的均为计算后的数值
原子引用
有时,被保护的数据类型不一定是基本数据类型,也有可能是类似BigDecimal
这样的数据类型。此时,可以使用原子引用来保护其中的共享变量。
原子引用主要有:AtomicReference
、AtomicStampedReference
、AtomicMarkableReference
这里选取AtomicReference
进行讲解。
初始化
在AtomicReference
初始化时,最好将原始数据传入进去。
BigDecimal decimal = new BigDecimal("11.11");
AtomicReference<BigDecimal> reference = new AtomicReference<>(decimal);
基础CAS操作
基础CAS操作与AtomicInteger
基本相同。但是这里需要注意,由于引用类型与基本数据类型不同,在compareAndSet()
中,比较的是引用地址,即只有引用地址相等才认为两者相等。
while (true) {
BigDecimal preValue = reference.get();
BigDecimal newValue = preValue.subtract(new BigDecimal("2.22"));
if(reference.compareAndSet(preValue, newValue)){
break;
}
}
下面这段例子深刻体现了只比较引用这一个特点:虽然integerAtomicReference
中的值确实是11000,但是由于compareAndSet()
时java会为11000创建一个新的对象,导致两者引用并不相同,进而始终不满足修改条件。
Integer integer = 11000;
AtomicReference<Integer> integerAtomicReference = new AtomicReference<>(integer);
while (true) {
if(integerAtomicReference.compareAndSet(11000, 22222)){
break;
}
}
CAS封装的API
与AtomicInteger
相同,AtomicReference
也存在getAndUpdate()
和getAndAccumulate()
方法,只是它们传入的操作函数变为了UnaryOperator<V>
和BinaryOperator<V>
类型,其中泛型V
为构造AtomicReference
时传入的类型。
ABA问题
从之前的案例可以看出,AtomicReference
在执行compareAndSet()
时会比较引用,但是无法感知到手否有其它线程修改过该变量。
在下面的例子中,主线程希望将A改为C,但是在other()
方法中,他先将A改为B,又将B改为A,实际上变量已经发生了变动,但是主线程感知不到。
public class AtomicReferenceDemo {
public static void main(String[] args) throws InterruptedException {
AtomicReference<String> reference = new AtomicReference<>("A");
String pre = reference.get();
other(reference);
boolean a2c = reference.compareAndSet(pre, "C");
log.info("A -> C : {}",a2c);
}
private static void other(AtomicReference<String> reference) {
boolean a2b = reference.compareAndSet(reference.get(), "B");
log.info("A -> B : {}",a2b);
boolean b2a = reference.compareAndSet(reference.get(), "A");
log.info("B -> A : {}",b2a);
}
}
日志打印结果如下
[164 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : A -> B : true
[168 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : B -> A : true
[168 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : A -> C : true
带版本号的原子引用
只要有其它线程动过了该共享变量,那么自己的CAS就会失败。此时,仅比较引用是不过的,还需要一个版本号。Java中AtomicStampedReference
可以完成此功能。
在AtomicStampedReference
中,可以通过getReference()
获取引用,通过getStamp()
获取版本号。此外,在初始化时,除了传递引用外,还需要设置一个初始版本号。
同时,在修改时,除了传新旧引用外,还需要传递新旧版本号。
我们可以利用AtomicStampedReference
对ABA问题进行修改。
public class AtomicReferenceDemo {
public static void main(String[] args) throws InterruptedException {
AtomicStampedReference<String> reference = new AtomicStampedReference<String>("A",0);
int stamp = reference.getStamp();
String pre = reference.getReference();
other(reference);
boolean a2c = reference.compareAndSet(pre, "C",stamp,stamp + 1);
log.info("A -> C : {}",a2c);
}
private static void other(AtomicStampedReference<String> reference) {
int stamp1 = reference.getStamp();
boolean a2b = reference.compareAndSet(reference.getReference(), "B",stamp1, stamp1 + 1);
log.info("A -> B : {}",a2b);
int stamp2 = reference.getStamp();
boolean b2a = reference.compareAndSet(reference.getReference(), "A",stamp2, stamp2 + 1);
log.info("B -> A : {}",b2a);
}
}
此时日志打印结果如下,A到C的修改已经不能成功
[157 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : A -> B : true
[160 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : B -> A : true
[161 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : A -> C : false
带标记的原子引用
有时,我们并不关心引用变量更改了几次,只单纯关心它是否被更改过,此时我们可以使用AtomicMarkableReference
。
在AtomicMarkableReference
中,标记位并非整数,而是一个boolean值。
原子数组
由于原子引用在执行compareAndSet()
时只会比较引用,因此它无法对对象里面的内容进行线程安全的保护。针对数组,java在JUC中为我们提供了原子数组来解决这一问题。
原子数组包括AtomicIntegerArray
、AtomicLongArray
、AtomicReferenceArray
。
使用示例
为了统一测试,我们可以使用函数式接口来统一执行流程,而具体逻辑则在测试方法外部实现。
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFunction,
BiConsumer<T, Integer> consumer,
Consumer<T> printer
) throws InterruptedException {
ArrayList<Thread> threads = new ArrayList<>();
// 创建数组
T array = arraySupplier.get();
// 获取数组长度
Integer length = lengthFunction.apply(array);
for (int i = 0; i < length ;i++) {
// 每个线程将10000次自增均摊到每个元素上(共享变量没有锁保护)
threads.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
consumer.accept(array, j%length);
}
},"t-"+i));
}
// 启动并等待线程
threads.forEach(Thread::start);
for (Thread thread : threads) {
thread.join();
}
// 打印数组元素
printer.accept(array);
}
在demo()
函数中,我们将10000次逻辑均摊到了数组的每个元素上,而且线程执行过程中并没有加锁。
下面我们可以对比常规数组和原子数组在demo()
上的表现。
public static void main(String[] args) throws InterruptedException {
demo(
() -> new int[10],
(array) -> array.length,
(array,index) -> array[index]++,
(array) -> System.out.println(Arrays.toString(array))
);
demo(
() -> new AtomicIntegerArray(10),
(array) -> array.length(),
(array,index) -> array.getAndUpdate(index, i -> i+1),
(array) -> System.out.println(array)
);
}
输出结果如下所示,可见,常规数组因为线程安全问题,其每个元素的最终结果并没有达到10000
[8396, 8423, 8416, 8424, 8438, 8432, 8421, 8414, 8449, 8449]
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
字段更新器
原子数组保护的是数组中的元素,字段更新器保护的是某个对象的成员变量。
字段更新器包括AtomicReferenceFieldUpdater
、AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
。
初始化
字段更新器的初始化与其它原子变量不同,它们不针对某一个被保护的对象,AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
的初始化方法如下
newUpdater(Class<U> tclass, String fieldName)
其中tclass
表示需要保护的字段的所在类,fieldName
需要保护的字段的名称,该字段必须为整形或长整型。
而对于AtomicReferenceFieldUpdater
,其初始化方法为
newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)
其中,vclass
表示字段本身的类型,其它两个的含义与AtomicIntegerFieldUpdater
相同。
注意:由于被保护的字段需要在多个线程之间共享,因此它应该被设置为volatile
的,同时,由于字段需要被其他类访问,因此它不应该设置为private
。
CAS操作
由于字段更新器在初始化时不针对某一个被保护的对象,因此在CAS时需要将相应的对象传入。
下面的代码是一段使用示例,它通过CAS的方式修改了一个学生的名字。
public class AtomicFeildDemo {
public static void main(String[] args) {
AtomicReferenceFieldUpdater<Student, String> nameUpdater =
AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
Student student = new Student("小明");
System.out.println(student);
boolean b = nameUpdater.compareAndSet(student, student.getName(), "小李");
System.out.println("小明 -> 小李 : "+b);
System.out.println(student);
}
}
class Student{
protected volatile String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Student{");
sb.append("name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}
原子累加器
指对整数进行累加操作。
JDK1.8后,新增了几个专门用于累加的原子类,它的性能要比传统的原子整形更好。
原子累加器包括:LongAdder
和DoubleAdder
。这里以LongAdder
为例。
初始化
在LongAdder
进行初始化时,无需传任何参数,其默认起始值为0,如果需要修改起始值,可以在创建后使用add()
方法进行修改。
LongAdder longAdder = new LongAdder();
longAdder.add(10L);
常用API
API | 用法 |
---|---|
add(long l) | 将和加上l |
increment() | 对整形进行自增1 |
sum() | 给出当前时刻的和,并不一定完全准确 |
reset | 将和重置到0 |
longValue() | 同sum() |
性能比较
为方便比较,我们使用了统一的demo()
函数,其中累加器的提供和具体执行由外部实现,并通过函数式接口传递进来。在demo()
中,我们会使用5个线程,每个线程对累加器操作2000000次,累加器的最终结果应该为10000000。
static <T> void demo(
Supplier<T> adderSupplier,
Consumer<T> function
) throws InterruptedException {
T adder = adderSupplier.get();
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < 5; i++) {
threads.add(new Thread(() -> {
for (int j = 0; j < 2000000; j++) {
function.accept(adder);
}
},"t-"+i));
}
long start = System.currentTimeMillis();
threads.forEach(Thread::start);
for (Thread thread : threads) {
thread.join();
}
long end = System.currentTimeMillis();
log.info("adder: {}, use {} ms",adder,end - start);
}
为了公平起见,我们同时比较AtomicLong
与LongAdder
,并将它们各执行了10次。
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
demo(
() -> new AtomicLong(0),
adder -> adder.incrementAndGet()
);
}
log.info("-------------------------");
for (int i = 0; i < 10; i++) {
demo(
() -> new LongAdder(),
adder -> adder.increment()
);
}
}
最终结果如下所示,显然LongAdder
比AtomicLong
有更好的性能表现。
[317 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 164 ms
[490 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 170 ms
[710 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 220 ms
[869 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 159 ms
[1074 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 205 ms
[1230 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 156 ms
[1432 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 202 ms
[1627 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 194 ms
[1832 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 205 ms
[2033 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 201 ms
[2033 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : -------------------------
[2059 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 25 ms
[2078 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 18 ms
[2099 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 20 ms
[2116 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2133 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2151 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2168 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2185 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2202 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2220 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 18 ms
性能提升的原因
虽然AtomicLong使用CAS算法,但是CAS失败后还是通过无限循环的自旋锁不多的尝试,这就是高并发下CAS性能低下的原因所在。
高并发下N多线程同时去操作一个变量会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。既然AtomicLong性能问题是由于过多线程同时去竞争同一个变量的更新而降低的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源,这样的思想驱动下锁分段理念下的LongAddr孕育而生。
LongAdder源码分析
CAS锁
利用CAS,可以实现加锁与解锁操作。但请勿将其运用于生产实践,因为他对CPU的消耗很大。
public class LockCas {
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);
}
}
关键域与Cell类
注意:我们这里以JDK8为例,这里的源码在之后的版本有变化
LongAdder 类有几个关键域
// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;
其中,volatile
保证可见性,transient
则表示它们不会被用于序列化。
// 防止缓存行伪共享
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
缓存行伪共享
CPU缓存与内存级别与速度差异
从 cpu 到 | 大约需要的时钟周期 |
---|---|
寄存器 | 1 cycle (4GHz 的 CPU 约为0.25ns) |
L1 | 3~4 cycle |
L2 | 10~20 cycle |
L3 | 40~45 cycle |
内存 | 120~240 cycle |
因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)。
但是,缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效。
造成的问题
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为24字节(16 字节的对象头+ 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:
- Core-0 要修改 Cell[0]
- Core-1 要修改 Cell[1]
无论谁修改成功,都会导致对方 Core 的缓存行失效。
@Contended解决缓存行伪共享
它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
add()
方法解析
在LongAdder
类中,increment()
方法的本质是add(1L)
,因此我们主要研究add()
方法
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);
}
}
}
流程图
longAccumulate()
解析
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
if ((h = getProbe()) == 0) {
// 初始化 probe
ThreadLocalRandom.current(); // force initialization
// h 对应新的 probe 值, 用来对应 cell
h = getProbe();
wasUncontended = true;
}
// collide 为 true 表示需要扩容
boolean collide = false; // True if last slot nonempty
done: for (;;) {
Cell[] cs; Cell c; int n; long v;
// 已经有了 cells
if ((cs = cells) != null && (n = cs.length) > 0) {
// 还没有 cell
if ((c = cs[(n - 1) & h]) == null) {
// 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
// 成功则 break, 否则继续 continue 循环
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
break done;
}
} finally {
cellsBusy = 0;
}
continue; // Slot is now non-empty
}
}
collide = false;
}
// 有竞争, 改变线程对应的 cell 来重试 cas
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
else if (c.cas(v = c.value,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break;
// 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
else if (n >= NCPU || cells != cs)
collide = false; // At max size or stale
// 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
else if (!collide)
collide = true;
// 尝试加锁
else if (cellsBusy == 0 && casCellsBusy()) {
// 加锁成功, 扩容
try {
if (cells == cs) // Expand table unless stale
cells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 改变线程对应的 cell
h = advanceProbe(h);
}
// 还没有 cells, 尝试给 cellsBusy 加锁
else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
// 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
// 成功则 break;
try { // Initialize table
if (cells == cs) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
break done;
}
} finally {
cellsBusy = 0;
}
}
// 上两种情况失败, 尝试给 base 累加
else if (casBase(v = base,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break done;
}
}
在longAccumulate()
中,我们可以将代码分为三个主要部分:cells的创建、cell的创建与cas cell。
sum()
解析
在LongAdder
类中,sum()
方法会将cells中的所有cell进行累加,并最终加上base的值,作为求和的结果。
public long sum() {
Cell[] cs = cells;
long sum = base;
if (cs != null) {
for (Cell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
Unsafe
Unsafe 对象提供了非常底层的,操作内存、线程的方法,在之前的原子变量中,大量用到了Unsafe 对象来进行内存级别的操作。
但是不要被类的名字所迷惑,这里的“不安全”(unsafe)并非指的是线程不安全,而是指该类过于底层,开发者如果擅自使用不安全。因此,不建议大家擅自使用此类。
Unsafe对象的获取
Unsafe 对象采用了单例模式,但是它既不能创建,也不能通过getUnsafe()
方法获取。
如果在代码中直接采用getUnsafe()
,会抛出异常java.lang.SecurityException
。
Unsafe采用了单例模式,其对象存储在内部变量theUnsafe
上,我们可以通过反射来获取该对象。
public class UnsafeDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Unsafe unsafe = getUnsafe();
System.out.println(unsafe);
}
public static Unsafe getUnsafe() {
Field field = null;
try {
field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
Unsafe对象的CAS方法
Unsafe对象的CAS方法有如下3种,每种方法需要4个参数
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
参数名 | 含义 |
---|---|
var1 | 变量所在的对象 |
var2 | 域(成员变量)的偏移地址 |
var4 | 原值 |
var5 | 新值 |
为了使用CAS,就需要先获得成员变量的偏移地址。使用unsafe
的objectFieldOffset()
可以获得成员变量的偏移地址。因此,我们可以通过如下方式使用Unsafe对象的CAS方法。
static class Student{
int age;
String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Student{");
sb.append("age=").append(age);
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}
public static void main(String[] args) throws NoSuchFieldException {
Unsafe unsafe = getUnsafe();
if (unsafe == null){
return;
}
long ageOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("age"));
long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
Student student = new Student(20, "小明");
System.out.println(student);
unsafe.compareAndSwapInt(student,ageOffset,20,17);
unsafe.compareAndSwapObject(student,nameOffset,"小明","小李");
System.out.println(student);
}
打印结果如下,可见,Unsafe对象成功使用CAS方法修改了student
中各成员变量的值。
Student{age=20, name='小明'}
Student{age=17, name='小李'}