1. 无锁解决
- CAS与volatile
- 原子整数
- 原子引用
- 原子累加器
- Unsafe
1.1 提出问题
有如下需求,保证 accounet.withdraw
取款方式的线程安全
/**
* 无锁
*/
public class test1 {
public static void main(String[] args) {
AccountUnsafe account = new AccountUnsafe(10000);
Account.demo(account);
}
}
class AccountUnsafe implements Account{
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
// 临界区
return this.balance;
}
@Override
public void withdraw(Integer amount) {
// 临界区
this.balance -= amount;
}
}
interface Account{
Integer getBalance();
void withdraw(Integer amount);
static void demo(Account account){
ArrayList<Thread> ts = new ArrayList<>();
/*
这里开启1000个线程,每个线程在启动时都取走了10;最后结果输出应该是0
*/
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(()->{account.withdraw(10);},"t"+i));
}
long start = System.nanoTime();
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)/100_000 +"ms");
}
}
- 解决问题
在临界区的两个方法上使用synchronized
修饰即可
@Override
public synchronized Integer getBalance() {
return this.balance;
}
@Override
public synchronized void withdraw(Integer amount) {
// 临界区
this.balance -= amount;
}
1.1.1 无锁解决
其他代码不变,更改实现类
class AccountCas implements Account{
private AtomicInteger balance;
public AccountCas(int balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true){
// 获取余额的最新值
int prev = balance.get();
// 要修改的金额
int next = prev - amount;
// 真正修改;第一个参数:修改的值;第二个参数:更换的新值
if (balance.compareAndSet(prev, next)){
break;
}
}
}
}
1.2 CAS 与 volatile
1.2.1 CAS
AtomicInteger解决方案,内部并没有用锁来保护共享变量的线程安全。
那么它底层是如何实现无锁即线程安全呢?
public void withdraw(Integer amount) {
while (true){
// 获取余额的最新值
int prev = balance.get();
// 要修改的金额
int next = prev - amount;
// 真正修改;第一个参数:修改的值;第二个参数:更换的新值
if (balance.compareAndSet(prev, next)){
break;
}
}
}
其中的关键是compareAndSet,它的简称就是CAS(Coompare And Swap),它必须是 原子操作
compareAndSet 会先进行判断,判断什么?
- 判断获得的值与共享变量是否一致
- 如果一致:返回true,并修改值
- 如果不一致:返回false,不做修改
CAS的底层使用的是:lock cmpxchg 指令(X86架构)
在单核CPU和多核CPU下都能够保证【比较-交换】的原子性
在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。
在这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的
1.2.2 volatile
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰
它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
即一个线程对volatile变量的修改,对另一个线程可见
需要注意的是:volatile仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
CAS必须借助 volatile 才能读取到共享变量的最新值来实现【compareAndSet】的效果
1.2.3 为什么无锁效率高
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
- 就如一辆开车起步熄火一样,起步又得做一套点火、挂挡等操作
- 而一直缓慢行驶则能够保持较好运行轨迹
- 无锁情况下,因为线程需要保持运行,需要额外CPU的支持,CPU在这里就好比高速跑道。
- 没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入课运行状态,还是会导致上下文切换
1.2.4 CAS的特点
结合CAS和volatile可以实现无锁并发,适用于线程数较少、多核CPU的场景下
- CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,会进行重试
- synchronized是基于悲观锁的思想:最悲观的估计,需要防止其他线程来修改关共享变量,只有自己将任务做完才会释放锁,让其他人使用
- CAS体现的是无锁并发、无阻塞并发。
- 因为没有使用synchronized,所以线程不会陷入阻塞,这时效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
1.3 原子对象
1.3.1 原子整数
JUC并发包中提供了
- AtomicBoolean
- AtomicInteger
- AtomicLong
基本使用都是一致的,以其中一种为例
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
- 基本使用
public class test2 {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
System.out.println(i.incrementAndGet()); // 自增并获取值 - ++i
System.out.println(i.getAndIncrement()); // i++
System.out.println(i.decrementAndGet()); // --i
System.out.println(i.getAndDecrement()); // i--
i.getAndAdd(5); // i+=5
i.addAndGet(5);// 先增加再赋值
System.out.println(i.get());
}
}
1.3.1.1 updateAndGet() & getAndUpdate()
i.updateAndGet(value->value / 2);
i.updateAndGet(value->{
int num = value * 2;
return num / 4;
});
方法内部是提供一个函数式接口(lambda表达式)
它所返回值是做出原子操作后的结果
其中这两个方法没有本质区别,只不过是先赋值还是先读取罢了
- 接口底层
private volatile int value;
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
// 使用接口的抽象方法来处理更新值
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
public final int get() {
return value;
}
int applyAsInt(int operand);
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
底层还是使用compareAndSwapInt()方法来进行计算
1.3.2 原子引用
AtomicReference
AtomicMarkableReference
AtomicStampedreferenece
package com.renex.c7;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* 无锁
*/
public class test3 {
public static void main(String[] args) {
DecimalCas account = new DecimalCas(new BigDecimal(10000));
AccountBigDecimal.demo(account);
}
}
class DecimalCas implements AccountBigDecimal{
// 使用原子引用类型:这里引用 BigDecimal 类型
private AtomicReference<BigDecimal> balance;
public DecimalCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
while (true){
// 获取余额的最新值
BigDecimal prev = balance.get();
// 要修改的金额
BigDecimal next = prev.subtract(amount);
// 真正修改;第一个参数:修改的值;第二个参数:更换的新值
if (balance.compareAndSet(prev, next)){
break;
}
}
}
}
interface AccountBigDecimal{
BigDecimal getBalance();
void withdraw(BigDecimal amount);
static void demo(AccountBigDecimal account){
ArrayList<Thread> ts = new ArrayList<>();
/*
这里开启1000个线程,每个线程在启动时都取走了10;最后结果输出应该是0
*/
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(()->{account.withdraw(BigDecimal.TEN);},"t"+i));
}
long start = System.nanoTime();
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)/100_000 +"ms");
}
}
其实原子对象的使用方式都基本一致
1.3.2.1 ABA问题
static AtomicReference<String> ref = new AtomicReference<String>("A");
public static void main(String[] args) throws InterruptedException {
log.info("main start....");
String prev = ref.get();
other();
Thread.sleep(1000);
log.info("change A->C {}",ref.compareAndSet(ref.get(),"C"));
}
private static void other(){
new Thread(()->{
log.info("change A->B {}",ref.compareAndSet(ref.get(),"B"));
},"t1").start();
new Thread(()->{
log.info("change B->A {}",ref.compareAndSet(ref.get(),"A"));
},"t2").start();
}
[main] INFO test4 - main start....
[t1] INFO test4 - change A->B true
[t2] INFO test4 - change B->A true
[main] INFO test4 - change A->C true
无法感知其他线程对共享变量是否做出更改。
虽然不会影响运行,但是会存在一种隐患
主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A改为B又改回A的情况,如果主线程希望:
- 只要有其他线程【动过了】共享变量,那么它自己的cas就算失败,这时仅比较值是不够的,需要再加一个版本号
1.3.2.2 AtomicStampedreferenece
除了需要比较值的一致,还需要比较版本号一致
- 只有两者都处于一致状态,才会发生修改
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
log.info("main start....");
String prev = ref.getReference();
// 版本号
int stamp = ref.getStamp();
other();
Thread.sleep(1000);
log.info("change A->C {}",ref.compareAndSet(ref.getReference(),"C",stamp,stamp+1));
System.out.println("main "+stamp);
}
private static void other(){
new Thread(()->{
int stamp = ref.getStamp();
log.info("change A->B {}",ref.compareAndSet(ref.getReference(),"B",stamp,stamp+1));
System.out.println("=========t1============"+stamp);
},"t1").start();
new Thread(()->{
int stamp = ref.getStamp();
log.info("change B->A {}",ref.compareAndSet(ref.getReference(),"A",stamp,stamp+1));
System.out.println("===========t2=========="+stamp);
},"t2").start();
}
///
[main] INFO test5 - main start....
[t1] INFO test5 - change A->B true
[t2] INFO test5 - change B->A true
=========t1============0
===========t2==========1
main 0
[main] INFO test5 - change A->C false
main线程获得的stamp还是初始值 0,但是获取到初始值后,main线程就开始了休眠
能够看到,t1线程获得stamp版本号还是初始值0,当做完修改后版本号+1(stamp=1)
因为t2线程开始时先获取了一次stamp版本号,所以t2拿取的版本号依旧是t1线程更改后的 1
那么t2就可以判断一致,所以也进行了更改
当main线程经过休眠继续执行时,就会发现版本号不一致,那么这时候就无法继续修改
1.3.2.3 AtomicMarkableReference
AtomicStampedreferenece 可以给原子引用加上版本号,追踪原子引用整个的变化过程
通过 AtomicStampedreferenece 可以知道,引用变量在中途中被更改过几次
- 而在有些时候,并不关心引用变量更改了几次,只是单纯的关系是否被更改过,所有就有了 AtomicMarkableReference
@Slf4j(topic = "test5")
public class test6 {
public static void main(String[] args) throws InterruptedException {
demo data = new demo("OK");
AtomicMarkableReference<demo> ref = new AtomicMarkableReference<>(data,false);
log.info("start....");
demo prev = ref.getReference();
log.info("result:{}", prev.toString());
new Thread(() -> {
log.info("thread start");
data.setOk("NOT OK");
boolean b = ref.compareAndSet(data, data, true, false);
log.info(data +"__"+b);
},"t1").start();
Thread.sleep(1000);
boolean newOk = ref.compareAndSet(prev, new demo("NEW OK"), true, false);
log.info("change result:{}", newOk);
log.info(ref.getReference().toString());
}
@AllArgsConstructor
@NoArgsConstructor
@Data
static class demo{
private String ok;
}
}
///
[main] INFO test5 - start....
[main] INFO test5 - result:test6.demo(ok=OK)
[t1] INFO test5 - thread start
[t1] INFO test5 - test6.demo(ok=NOT OK)__false
[main] INFO test5 - change result:false
[main] INFO test5 - test6.demo(ok=NOT OK)
AtomicMarkableReference 没有那么复杂,它仅做bool值的判断,但值不一致时就判断共享变量遭遇了变动
1.3.3 原子数组
原子数组类型,这个其实和AtomicInteger等类似,只不过在修改时需要指明数组下标。
CAS是按照==来根据地址进行比较。数组比较地址,肯定是不行的,只能比较下标元素。而比较下标元素,就和元素的类型有关系了。
原子类型数组有以下四种:
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
AtomicBooleanArray
AtomicIntegerArray
和 AtomicLongArray
的使用方式差别不大,AtomicReferenceArray
因为他的参数为引用数组,所以跟前两个的使用方式有所不同。AtomicBooleanArray
在生产中使用的很少。
public static void main(String[] args) {
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
for (int i = 0; i < atomicIntegerArray.length(); i++) {
// 这里执行,i的目标初始值是0,因为在for循环前定义了i这个变量的值是0
// 第一个参数:更新的数组的索引;第二个参数:确认值;第三个参数:实行更改的值
boolean b = atomicIntegerArray.compareAndSet(i, 0, i + 10);
System.out.println(b);
}
// 这里目标确认值是0是错误的,因为在for循环中已经成功更改过了,所以这里的目标确认值应是 13
System.out.println(atomicIntegerArray.compareAndSet(3, 0, 30));
// System.out.println(atomicIntegerArray.compareAndSet(3, 13, 30));
System.out.println(atomicIntegerArray.get(2));
System.out.println(atomicIntegerArray.get(3));
}
///
true
true
true
true
true
true
true
true
true
true
false
12
30
1.3.4 字段更新器
AtomicReferenceFieldUpdater
// 域 字段AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合volatile修饰的字段使用,否则会出现异常
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.<init>(AtomicReferenceFieldUpdater.java:348)
at java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(AtomicReferenceFieldUpdater.java:110)
at com.renex.c8.test2.main(test2.java:16)
示例代码:
public class test2 {
public static void main(String[] args) {
Student stu = new Student();
/**
* newUpdater()
* 第一个参数:保护的类的类型
* 第二个参数:保护的具体变量类型
* 第三个参数:修改的变量名
*/
AtomicReferenceFieldUpdater<Student, String> updater = AtomicReferenceFieldUpdater
.newUpdater(Student.class, String.class, "name");
/**
* updater.compareAndSet()
* 第一个参数:修改的对象
* 第二个参数:确定目标值
* 第三个参数:修改为什么值
*/
System.out.println(updater.compareAndSet(stu, null, "zhangsan"));;
System.out.println(stu);
}
}
class Student{
volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
/
true
Student{name='zhangsan'}
1.3.5 原子累加器
1.3.5.1 LongAdder 累加器性能比较
package com.renex.c8;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class test3 {
public static void main(String[] args) {
demo(()->new AtomicLong(0),
(adder)->adder.getAndIncrement()
);
//
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();
ArrayList<Thread> ts = new ArrayList<>();
for (int i = 0; i < 4; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 50000; j++) {
action.accept(adder);
}
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(adder+"cost: "+(end-start)/100_000+" ms");
}
}
/
200000cost: 115 ms
200000cost: 69 ms
性能提升的原因很简单,就是在有竞争的时候,设置多个累加单元,Thread-0 累加 Cell[0],而Thread-1 累加 Cell[1]…最后将结果汇总即可
这样它们在累加时操作着不同的Cell变量,因此减少了CAS重试失败,从而提高性能
1.3.6 Unsafe
Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得
// 找到unsafe类名,通过反射找到对应的类
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true); // 不检查
Unsafe unsafe = (Unsafe) theUnsafe.get(null);// 默认就是null
1.3.7 Unsafe CAS 操作
package com.renex.c8;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class test5 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 找到unsafe类名,通过反射找到对应的类
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true); // 不检查
Unsafe unsafe = (Unsafe) theUnsafe.get(null);// 默认就是null
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
long idOffset = unsafe.objectFieldOffset(id);
long nameOffset = unsafe.objectFieldOffset(name);
Student student = new Student();
// 基本就是原子操作那一套
unsafe.compareAndSwapObject(student, idOffset, 0, 22);
unsafe.compareAndSwapObject(student, nameOffset, null, "张三");
}
@AllArgsConstructor
@NoArgsConstructor
@Data
static class Student{
volatile int id;
volatile String name;
}
}
2. 👍JUC 专栏 - 前篇回顾👍
- (Java并发编程—JUC)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!
- (Java并发编程——JUC)共享问题解决与synchronized对象锁分析!全程干货!!快快收藏!!
- (Java并发编程——JUC)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!
- (Java并发编程——JUC)从JMM内存模型的角度来分析CAS并发性问题