Java重要知识点学习整理笔记。
序号 | 文章 |
---|---|
1 | Java并发的CAS原理详解 |
2 | Java并发的ABA原理详解 |
3 | Java的18种Queue |
4 | 一篇文章整理Java的volatile |
5 | Java集合的线程不安全 |
6 | Java中的21种锁 |
7 | JVM进阶之思维导图 |
8 | Java的HashMap原理总结(问答式学习) |
Java并发编程中的CAS原理是很重要的概念。
CAS加volatile关键字是实现并发包的基石。没有CAS就不会有并发包,synchronized是一种独占锁、悲观锁,java.util.concurrent中借助了CAS指令实现了一种区别于synchronized的一种乐观锁。
乐观锁和悲观锁的概念请参考Java中的21种锁。
在Java中java.util.concurrent.atomic包下面的原子变量就是使用了乐观锁的一种实现方式CAS实现。
在JDK 5之前Java语言是靠synchronized
关键字保证同步的,这会导致有锁。
锁机制存在以下问题:
volatile
是不错的机制,但是volatile
不能保证原子性。因此对于同步最终还是要回到锁机制上来。
乐观锁用到的机制就是CAS,Compare and Swap。
案例:甲看见一个三角形积木,觉得不好看,想替换成五边形,但是乙想把积木替换成四边形。(前提条件,只能被替换一次)
甲比较鸡贼,想到了一个办法:“我把积木带到另外一个房间里面去替换,并上锁,就不会被别人打扰了。”(这里用到了排他锁synchronized
)
乙觉得甲太不厚道:“房间上了锁,我进不去,我也看不见积木长啥样。(因上了锁,所以不能访问)”
于是甲、乙想到了另外一个办法:谁先抢到积木,谁先替换,如果积木形状变了,则不允许其他人再次替换。(比较并替换CAS
)
于是他们就开始抢三角形积木:
甲抢到,替换成五边形,乙不能替换
乙抢到未替换,甲替换成功
乙抢到,替换成三角形,甲替换成五边形,ABA问题
CAS的全称:Compare-And-Swap(比较并交换)
。比较变量的现在值与之前的值是否一致,若一致则替换,否则不替换。
CAS的作用:原子性
更新变量值,保证线程安全。
CAS指令:需要有三个操作数,变量的当前值(V)
,旧的预期值(A)
,准备设置的新值(B)
。
CAS指令执行条件:当且仅当V=A时,处理器才会设置V=B,否则不执行更新。
CAS的返回指:V的之前值。
CAS处理过程:原子操作,执行期间不会被其他线程中断,线程安全。
CAS并发原语:体现在Java语言中sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令
组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,所以CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,所以CAS是线程安全
的。
讲volatile时,讲到了如何使用原子整型类AtomicInteger
来解决volatile
的非原子性问题,保证多个线程执行num++的操作,最终执行的结果与单线程一致,输出结果为20000。
这次还是用AtomicInteger
。
AtomicInteger atomicInteger = new AtomicInteger(10);
atomicInteger.compareAndSet(10, 20);
设置成功,atomicInteger更新为20
atomicInteger.compareAndSet(10, 30);
设置失败,因atomicInteger的当前值为20,而比较值是10,所以比较后,不相等,故不能进行更新。
package com.jackson0714.passjava.threads;
import java.util.concurrent.atomic.AtomicInteger;
/** 演示CAS compareAndSet 比较并交换 */
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
Boolean result1 = atomicInteger.compareAndSet(10,20);
System.out.printf("当前atomicInteger变量的值:%d 比较结果%s\r\n", atomicInteger.get(), result1);
Boolean result2 = atomicInteger.compareAndSet(10,30);
System.out.printf("当前atomicInteger变量的值:%d, 比较结果%s\n" , atomicInteger.get(), result2);
}
}
当前atomicInteger变量的值:20 比较结果true
当前atomicInteger变量的值:20, 比较结果false
来对比看下原理图理解下上面代码的过程
上述的场景和我们用Git代码管理工具是一样的,如果有人先提交了代码到develop分支,另外一个人想要改这个地方的代码,就得先pull develop分支,以免提交时提示冲突。
源码调试
这里用atomicInteger
的getAndIncrement()
方法来讲解,这个方法里面涉及到了比较并替换的原理。
示例如下:
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(10);
Thread.sleep(100);
new Thread(() -> {
atomicInteger.getAndIncrement();
}, "aaa").start();
atomicInteger.getAndIncrement();
}
getAndIncrement
方法会调用unsafe的getAndAddInt
方法,
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
源码解释: 重点!!!
var1
:当前对象,我们定义的atomicIntegervar2
:当前对象的内存偏移量var4
:当前自增多少,默认为1,且不可设为其他值var5
:当前变量的值this.getIntVolatile(var1, var2)
:根据当前对象var1和对象的内存偏移量var2得到主内存中变量的值,赋值给var5,并在main线程的工作内存中存放一份var5的副本
至此,getAndIncrement方法的整个原子自增的逻辑就debug完了。所以可以得出结论:
先比较线程中的副本是否与主内存相等,相等则可以进行自增,并返回副本的值,若其他线程修改了主内存中的值,当前线程不能进行自增,需要重新获取主内存的值,然后再次判断是否与主内存中的值是否相等,以此往复。
aaa线程可能会出现循环多次的问题,因为其他线程可能将主内存的值又改了,但是aaa线程拿到的还是老的数据,就会出现再循环一次,就会给CPU带来性能开销。这个就是自旋
。
频繁出现自旋,循环时间长,开销大
(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
只能保证一个共享变量的原子操作
引出来ABA问题
(第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?)
参考:
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/183693.html原文链接:https://javaforall.cn