一、问题引入
1、线程不安全实例
public interface Account {
/**
* 获取余额
* @return
*/
Integer getBalance();
/**
* 取款
* @param amount
*/
void withDraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元的操作
* 如果初始余额为 10000,那么正确的结果应该为 0
* @param account
*/
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");
}
}
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));
}
}
运行结果:
270 cost:148ms
// 正确结果应该为 0
2、为什么不安全
withDraw 方法
public void withDraw(Integer amount) {
balance -= amount;
}
//多线程下不安全
3、解决思路-加锁
首先想到是给 Account 对象加锁
public class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public synchronized Integer getBalance() {
return balance;
}
@Override
public synchronized void withDraw(Integer amount) {
balance -= amount;
}
public static void main(String[] args) {
Account.demo(new AccountUnsafe(10000));
}
}
运行结果:
0 cost:153ms
4、解决思路-无锁
public class AccountCas implements Account {
//并发原子类
private AtomicInteger balance;
public AccountCas(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public synchronized Integer getBalance() {
return balance.get();
}
@Override
public synchronized void withDraw(Integer amount) {
while (true) {
//获取余额最新值
int prev = balance.get();
//要修改的余额
int next = prev - amount;
//比较并返回,比较获取的值与主存上的是否一致,一致就将第二个参数替换主存上的值,返回true,否则返回false
//把修改的余额同步到主存上去,两个参数,获取的值,要修改的值,同步成功返回true
if (balance.compareAndSet(prev,next)){
break;
}
}
}
public static void main(String[] args) {
Account.demo(new AccountCas(10000));
}
}
运行结果:
0 cost:200ms
二、CAS 与 volatile
1、分析原因
上面无锁解决方式,内部没有用锁来保护共享变量的线程安全,是如何实现的呢?
public synchronized void withDraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
//比较并设置值,表面看是两件事,实际上是一件事,不可分割的原子操作
if (balance.compareAndSet(prev,next)){
break;
}
}
}
其中的关键是 compareAndSet,它的简称就是 CAS(也有 Compare And Swap 的说法),它必须是原子操作
图解说明:
线程1从 Account 对象中获取余额100,并执行 -10 操作,但此时线程2已经将余额修改为 90 了,线程1 执行 compareAndSet(100,90) 方法时,发现自己拿到的最新值 100 与 Account 共享变量上的最新结果 90 对比,发现不一致,因此这次 CAS 操作失败返回 false,再次进入循环。核心的思想就是采用不断尝试不断尝试直至成功的方式来保护共享变量的线程安全。
2、volatile
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,而是必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
注意:volatile 仅仅保证了共享变量的可见性,让其他线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。
3、为什么无锁效率高
1、无锁情况下,即使锁重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞状态。打个比喻线程就像是高速跑道上的赛车,高速运行时,速度快,一旦发生上下文切换,就好比赛车要减速、熄火,等到被线程唤醒的时候又得重新打火、启动、加速…恢复到高速运行,代价比较大。
2、但无锁情况下,因为线程要保持运行,需要额外 CPU 支持,CPU 在这里好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞状态,但是由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
4、CAS 的特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
-
CAS 是基于乐观锁的思想:
最乐观的估计,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止(不怕吃亏)。 -
synchronized 是基于悲观锁的思想:
最悲观的估计,得防着其他线程来修改共享变量,会导致其它所有需要锁的线程挂起,等待有锁的线程释放锁,他们才有机会获得锁。 -
CAS 体现的是无锁并发、无阻塞并发:
1、因为没有使用 synchronized ,所以线程不会陷入阻塞状态,这是效率提升的因素之一。
2、但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。
三、原子整数
1、整体介绍
Atomic 是 jdk 提供的一系列包的总称,这个大家族包括
原子整数(AtomicInteger,AtomicLong,AtomicBoolean)
原子引用(AtomicReference,AtomicStampedReference,AtomicMarkableReference)
原子数组(AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray)
更新器(AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater)
2、原子整数
JUC 并发包下提供了:AtomicBoolean、AtomicInteger、AtomicLong三种类型原子整数,功能类似,以AtomicInteger为例说明
public class Test06 {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
}
}
四、原子引用
1、为什么需要原子引用类型
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。
接口类:
public interface DecimalAccount {
//获取
BigDecimal getBlance();
//取款
void withdraw(BigDecimal amount);
static void demo(DecimalAccount account){
List<Thread> list = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
list.add(new Thread(()->{
account.withdraw(BigDecimal.TEN);
}));
}
list.forEach(Thread::start);
list.forEach(t -> {
try {
t.join();
} catch (InterruptedException e){
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println("余额:" + account.getBlance() + " 耗时:" + (end - start) / 1000_000 + "ms");
}
}
实现类和测试:
public class DecimalAccountSafe implements DecimalAccount {
AtomicReference<BigDecimal> reference;
public DecimalAccountSafe(BigDecimal balance) {
this.reference = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBlance() {
return reference.get();
}
@Override
public void withdraw(BigDecimal amount) {
while (true){
BigDecimal prev = reference.get();
BigDecimal next = prev.subtract(amount);
if (reference.compareAndSet(prev,next)) {
break;
}
}
}
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountSafe(new BigDecimal("10000")));
}
}
2、ABA问题
ABA 问题:
如果变量 V 初次读取的时候是 A,并且在准备赋值的时候检查到它仍然是 A,那就能说明它的值没有被其他线程修改过了吗?如果在这段期间曾经被改成 B,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。
ABA实例:
public class Test03 {
static AtomicReference<String> reference = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
//启动
System.out.println("main start...");
String prev = reference.get();
other();
sleep(1);
//尝试修改为C
boolean c = reference.compareAndSet(prev, "C");
System.out.println("change A -> C " + c);
}
public static void other() throws InterruptedException {
//将A修改为B
new Thread(()->{
System.out.println("change A -> B " + reference.compareAndSet(reference.get(),"B"));
},"t1").start();
sleep(1);
//将B再修改回A
new Thread(() -> {
System.out.println("change B -> A " + reference.compareAndSet(reference.get(), "A"));
},"t2").start();
}
}
运行结果:
main start...
change A -> B true
change B -> A true
change A -> C true
分析: 主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又改回 A 的情况。
3、ABA问题解决AtomicStampedReference
若主线程希望只要有其他线程改动过共享变量,那么自己的 CAS 操作就算失败,这时,仅比较值是不够的,需要再加一个版本号 AtomicStampedReference
。
AtomicStampedReference
可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A->B->A->C,通过AtomicStampedReference
,可以知道,引用变量中途被修改了几次。
public class Test03 {
static AtomicStampedReference<String> reference = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
//启动
System.out.println("main start...");
//获取共享变量的值
String prev = reference.getReference();
//获取版本号
int stamp = reference.getStamp();
System.out.println("version:" + stamp);
other();
sleep(1);
//尝试修改为C
boolean c = reference.compareAndSet(prev, "C",stamp,stamp + 1);
System.out.println("change A -> C:" + c);
}
public static void other() throws InterruptedException {
//将A修改为B
new Thread(()->{
System.out.println("change A -> B:" + reference.compareAndSet(reference.getReference(),"B",reference.getStamp(),reference.getStamp()+1));
},"t1").start();
sleep(1);
//将B再修改回A
new Thread(() -> {
System.out.println("change B -> A:" + reference.compareAndSet(reference.getReference(), "A",reference.getStamp(),reference.getStamp()+1));
},"t2").start();
}
}
运行结果:
main start...
version:0
change A -> B:true
change B -> A:true
change A -> C:false
4、ABA问题解决AtomicMarkableReference
有的时候,并不关心引用变量更改了几次,只是单纯的关心是否被修改过,所以就有了AtomicMarkableReference
AtomicMarkableReference 的唯一区别是不再用 int 标识引用,而是使用 boolean 变量表示引用变量是否被修改过。
示例:
public class Test05 {
static AtomicMarkableReference<String> reference = new AtomicMarkableReference<>("A",false);
public static void main(String[] args) {
//初始化之后获取值
String prev = Test05.reference.getReference();
//初始化之后获取标记
boolean marked = reference.isMarked();
System.out.println(Thread.currentThread().getName() + "线程获取初始化之后的共享变量的值:" + prev);
System.out.println(Thread.currentThread().getName() + "获取初始化之后的线程标识:" + marked);
//尝试修改为C,expectedMark是要比较的标识,newMark是成功后要将标识改的值
boolean c = reference.compareAndSet(prev, "C",true,true);
if (!c) {
System.out.println("标识不一致,无法修改值");
}
boolean d = reference.compareAndSet(prev, "B",false,true);
if (d) {
System.out.println("标识一致,修改成功");
}
System.out.println(Thread.currentThread().getName() + "线程获取修改之后的共享变量的值:" + reference.getReference());
System.out.println(Thread.currentThread().getName() + "获取修改之后的线程标识:" + reference.isMarked());
}
}
运行结果:
main线程获取初始化之后的共享变量的值:A
main获取初始化之后的线程标识:false
标识不一致,无法修改值
标识一致,修改成功
main线程获取修改之后的共享变量的值:B
main获取修改之后的线程标识:true
五、原子数组
1、前言
JUC 并发包下提供了3个原子数组,它们提供了原子更新数组中元素的能力,它们主要是借助 Unsafe 类实现其核心功能。
AtomicIntegerArray:
原子更新整型数组里的元素。
AtomicLongArray:
原子更新长整型数组里的元素。
AtomicReferenceArray:
原子更新引用类型数组里的元素。
2、通用检测数组是否安全的方法
public class Test1 {
/**
*
* @param arraySupplier:提供数组,可以是线程不安全数组或线程安全数组
* @param lengthFun:获取数组长度的方法
* @param putConsumer:自增方法,回传array,index
* @param printConsumer:打印数组方法
* @param <T>
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果,BiFunction(参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void,BiConsumer(参数1,参数2)->
private static <T> void demo(Supplier<T> arraySupplier, Function<T,Integer> lengthFun,
BiConsumer<T,Integer> putConsumer, Consumer<T> printConsumer){
//线程集合
List<Thread> list = new ArrayList<>();
//获取数组
T array = arraySupplier.get();
//数组长度
Integer length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
//每个线程对数组做 10000 次操作
list.add(new Thread(()->{
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array,j % length);
}
}));
}
//启动所有线程
list.forEach(t -> t.start());
list.forEach(t ->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});//等待所有线程结束
printConsumer.accept(array);
}
}
3、不安全数组
public static void main(String[] args) {
//多线程下数组不安全
demo(()->new int[10],(array)->array.length,(array,index)->array[index]++,
array-> System.out.println(Arrays.toString(array)));
}
运行结果:
[9005, 8897, 8941, 8928, 8924, 8942, 9035, 9027, 9001, 8998]
4、安全数组
public static void main(String[] args) {
demo(()->new AtomicIntegerArray(10),(array)->array.length(),
(array, index)->array.getAndIncrement(index), array-> System.out.println(array));
}
运行结果:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
六、字段更新器
1、作用
保护的是对象的属性(或成员变量)的安全性。可以针对对象的某个域(Field)进行原子操作。
2、原子类型字段更新器
JUC 并发包下三种原子类型字段更新器。
AtomicIntegerFieldUpdater:
基于反射的工具类,可以原子性的更新指定对象的指定 int 类型字段。
AtomicLongFieldUpdater:
基于反射的工具类,可以原子性的更新指定对象的指定 long 类型字段。
AtomicReferenceFieldUpdater:
基于反射的工具类,可以原子性的更新指定对象的指定引用类型字段。
3、使用规则
原子类型字段更新器在内部通过 Unsafe 类的 native 方法保证操作的原子性。
注意:
- 属性字段必须是 volatile 类型,用于保证可见性。
- 属性类型必须和原子类中的类型一致。
- 属性字段必须非 private、protected(如果是当前类是可以的)
- 属性字段只能是实例变量,不能是类变量(static)。
- 属性字段不能是 final 变量,因为这样的字段不可修改。
- 如果要处理 Integer 和 Long 类型,则需要使用 AtomicReferenceFieldUpdater。
4、实例
public class Tset2 {
private static AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
public static void main(String[] args) {
Student stu = new Student();
System.out.println(updater.compareAndSet(stu,null,"张三"));
System.out.println(stu);
}
}
class Student{
public volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
5、不使用 volatile 报错
java.lang.ExceptionInInitializerError
Caused by: java.lang.IllegalArgumentException: Must be volatile type
七、原子累加器
1、概述
原子类型累加器是 JDK 1.8 新增的部分,是对 AtomicLong 等类的改进。比如 LongAccumulator 与 LongAdder 在高并发环境下比 AtomicLong更高效,以 LongAdder 为例说明。
2、分类
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
3、性能比较 AtomicLong 和 LongAdder
public class Test3 {
public static void main(String[] args) {
//累加操作
for (int i = 0; i < 5; i++) {
demo(() -> new AtomicLong(),adder -> adder.getAndIncrement());
}
for (int i = 0; i < 5; i++) {
demo(() -> new LongAdder(),adder -> adder.increment());
}
}
/**
*
* @param adderSupplier ()->结果 提供累加器对象
* @param action (参数) -> 执行累加操作
* @param <T>
*/
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个线程,每人累加 50w
for (int i = 0; i < 4; i++) {
ts.add(new Thread(()->{
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(adder + " 消耗时间:" + (end - start) / 1000_1000);
}
}
运行结果:
###################AtomicLong运行消耗时间
2000000 消耗时间:4
2000000 消耗时间:4
2000000 消耗时间:3
2000000 消耗时间:4
2000000 消耗时间:4
###################LongAdder运行消耗时间
2000000 消耗时间:1
2000000 消耗时间:0
2000000 消耗时间:0
2000000 消耗时间:0
2000000 消耗时间:0
说明:性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Thread-0 累加 Cell[0] 这个单元,而 Thread-1 累加 Cell[1] 这个单元…直到最后将结果汇总。这样他们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高了性能。
4、基本方法
以 LongAdder 为例
LongAdder():
累加器只有一个无参的构造器,会构造一个 sum = 0 的实例对象。void increment():
自增。void decrement():
自减。void add(long x):
增量计算。long sum():
返回当前的总和。void reset():
重置将总和保持为零的变量。long sumThenReset():
计算sum的和并且重置sum为0。int intValue():
获取sum的int形式(向下转型)。float floatValue():
获取sum的float形式(向上转型)。double doubleValue():
获取sum的double形式(向上转型)。long longValue():
相当于sum()。
可以看出 LongAdder
提供的 API 和 AtomicLong
比较接近,两者都能以原子的方式对 long 型变量进行增减。但是 AtomicLong
提供的功能更丰富,尤其是 addAndGet
、decrementAndSet
、compareAndSet
这些方法。
addAndGet
、decrementAndGet
除了单纯的做自增自减外,还可以立即获取增减后的值,而 LongAdder
则需要做同步控制才能精确获取增减后的值。如果业务需求需要精确的控制计数,做计数比较,AtomicLong
也更合适。
另外,从空间方面考虑,LongAdder
其实是一种“空间换时间”的思想,从这一点来讲 AtomicLong
更适合。当然,如果你一定要跟我杠现代主机的内存对于这点消耗根本不算什么,那我也办法。
总之,低并发、一般的业务场景下 AtomicLong
是足够了。如果并发量很多,存在大量写多读少的情况,那 LongAdder
可能更合适。
5、LongAdder原理
LongAdder 是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行 CAS 操作。
1、示例:比如有三个线程,每个线程对 value 增加 10。
1、对于 AtomicLong 的最终结果计算形式是:
vlaue = 10 + 10 + 10
2、对于 LongAdder 来说,内部有一个 base
变量,一个 cell[]
数组
base 变量
:非竟态条件下,直接累加到该变量上。
cell[] 数组
:竟态条件下,累加到各个线程自己的槽 cell[i]
中。
2、LongAdder的内部结构
1、LongAdder 只有一个空构造器,其本身也没有什么特殊的地方,所有复杂的逻辑都在它的父类 Striped64 中
2、Striped64 实现一些核心操作,处理64位数据。它只有一个空构造器,初始化时,通过 Unsafe 获取到类字段的偏移值,以便后续 CAS 操作
abstract class Striped64 extends Number {
Striped64() {
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long BASE;
private static final long CELLSBUSY;
private static final long PROBE;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> sk = Striped64.class;
BASE = UNSAFE.objectFieldOffset
(sk.getDeclaredField("base"));
CELLSBUSY = UNSAFE.objectFieldOffset
(sk.getDeclaredField("cellsBusy"));
Class<?> tk = Thread.class;
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
//可以把threadLocalRandomProbe看成是线程的hash值
} catch (Exception e) {
throw new Error(e);
}
}
}
3、定义一个内部 Cell
类(也就是槽),每个 Cell 对象存有一个 value 值,可以通过 Unsafe 来 CAS 操作它的值
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
//最重要的方法,用来 cas 方式进行累加,cmp 表示旧值,val 表示新值
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);
}
}
}
4、其他字段
/**
* CPU核数,用来决定槽数组大小
*/
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* 槽数组,大小为2的幂次
*/
transient volatile Cell[] cells;
/**
* 基数,在两种情况下会使用
* 1、没有遇到并发竞争时,直接使用 base 累加数值
* 2、初始化cells数组时,必须要保证cells数组只能被初始化一次(即只有一个线程能对cells初始化),其他竞争失败的线程会将数值累加到base上
*/
transient volatile long base;
/**
* 锁标识
* cells初始化或扩容时,通过CAS操作将此标识设置为1(加锁状态),初始化或者扩容完毕时,将此标识设置为0(无锁状态)
*/
transient volatile int cellsBusy;
6、伪共享原理
@sun.misc.Contended 介绍
@sun.misc.Contended
:对某字段加上该注解则表示该字段会单独占用一个 缓存行(Cache Line)。这里的缓存行是指 CPU 缓存(L1、L2、L3)的存储单元,常见的缓存行大小为 64 字节。
单独使用一个缓存行有什么作用 — 避免伪共享
当一个 CPU 要修改某共享变量 A 时会先锁定自己缓存里 A 所在的缓存行,并且把其他 CPU 缓存上相关的缓存行设置为无效。但如果被锁定或失效的缓存行里,还存储了其他不相干的变量 B,其他线程此时就访问不了 B,或者由于缓存行失效需要重新从内存中读取加载到缓存里,这里造成了开销,所以让共享变量 A 单独使用一个缓存行就不会影响到其他线程的访问。
缓存和内存的速度比较
从 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 的缓存行失败,比如 Core-0 中 Cell[0] = 6000,Cell[1]=8000
要累加 Cell[0] = 6001,Cell[1]=8000
,这时会让 Core-1 的缓存行失效。
@sun.misc.Contended
用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。
7、LongAdder源码之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);
}
}
流程图:
8、LongAdder源码之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
for (;;) {
Cell[] as; Cell a; int n; long v;
// 已经存在 cells
if ((as = cells) != null && (n = as.length) > 0) {
// 还没有 cell
if ((a = as[(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()) {
boolean created = false;
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;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
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 (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// 如果 cells 长度已经超过了最大长度,或者已经扩容,改变线程对应的 cell 来重试 cas
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
// 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
else if (!collide)
collide = true;
// 加锁
else if (cellsBusy == 0 && casCellsBusy()) {
// 加锁成功, 扩容
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 改变线程对应的 cell
h = advanceProbe(h);
}
// 还没有 cells,还没有加锁,尝试给 cellsBusy 加锁,对应cells创建流程
// casCellsBusy() 把 cellsBusy 状态位从 0 变为 1
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// 加锁成功, 初始化 cells, 开始长度为 2, 并填充一个 cell
// 成功则 break;
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 上两种情况失败, 尝试给 base 累加
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
流程图cells:
流程图cell:
已经存在并创建流程图:
9、最终通过 sum 方法将结果统计
源码:
/**
* 返回累加的和,也就是“当前时刻”的计数值
*
* 此返回值不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,
* 方法的返回时刻和调用时刻不是同一个点,在有并发的情况下,这个值只是近似准确的计数值
*
* 高并发时,除非全局加锁,否则得不到程序运行中某个时刻绝对准确的值
*/
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
八、Unsafe
1、概述
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,要通过反射才能获得
public class Test2 {
public static void main(String[] args) throws Exception {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
//设置访问私有变量
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
System.out.println(unsafe);
}
}
2、使用Unsafe进行CAS操作
public class Test2 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
//设置访问私有变量
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
System.out.println(unsafe);
//1、获取域(属性)的偏移地址
long id = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
long name = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
//2、执行 cas 操作
Student s = new Student();
// 参数:对象,偏移量,初始值,要设置的值
unsafe.compareAndSwapInt(s,id,0,1); // 返回true
unsafe.compareAndSwapObject(s,name,null,"张三");
//3、验证
System.out.println(s.getName());//输出张三
}
}
@Data
class Student{
private Integer id;
private String name;
}