预览
并发
问题 | 详解 |
---|---|
请谈谈你对volatile的理解 | link |
CAS你知道吗? | link |
原子类Atomiclnteger的ABA问题谈谈?原子更新引用知道吗? | link |
我们知道ArrayList是线程不安全,请编码写一个不安全的案例并给出解决方案 | link |
公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁 | link |
CountDownLatch/CyclicBarrier/Semaphore使用过吗? | link |
阻塞队列知道吗? | link |
线程池用过吗?ThreadPoolExecutor谈谈你的理解? | link |
线程池用过吗?生产上你如何设置合理参数 | link |
死锁编码及定位分析 | link |
JVM
问题 | 详解 |
---|---|
JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots | link |
你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值 | link |
你平时工作用过的JVM常用基本配置参数有哪些? | link |
强引用、软引用、弱引用、虚引用分别是什么?请谈谈你对OOM的认识 | link |
GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈怎么查看服务器默认的垃圾收集器是那个? | link |
生产上如何配置垃圾收集器的? | link |
谈谈你对垃圾收集器的理解?G1垃圾收集器 | link |
生产环境服务器变慢,诊断思路和性能评估谈谈? | link |
假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位 | link |
对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的? | link |
01_本课程前提要求和说明
一些大厂的面试题
蚂蚁花呗一面:
- Java容器有哪些?哪些是同步容器,哪些是并发容器?
- ArrayList和LinkedList的插入和访问的时间复杂度?
- java反射原理,注解原理?
- 新生代分为几个区?使用什么算法进行垃圾回收?为什么使用这个算法?
- HashMap在什么情况下会扩容,或者有哪些操作会导致扩容?
- HashMap push方法的执行过程?
- HashMap检测到hash冲突后,将元素插入在链表的末尾还是开头?
- 1.8还采用了红黑树,讲讲红黑树的特性,为什么人家一定要用红黑树而不是AVL、B树之类的?
- https和http区别,有没有用过其他安全传输手段?
- 线程池的工作原理,几个重要参数,然后给了具体几个参数分析线程池会怎么做,最后问阻塞队列的作用是什么?
- linux怎么查看系统负载情况?
- 请详细描述springmvc处理请求全流程?spring 一个bean装配的过程?
- 讲一讲AtomicInteger,为什么要用CAS而不是synchronized?
美团一面:
- 最近做的比较熟悉的项目是哪个,画一下项目技术架构图。
- JVM老年代和新生代的比例?
- YGC和FGC发生的具体场景?
- jstack,jmap,jutil分别的意义?如何线上排查JVM的相关问题?
- 线程池的构造类的方法的5个参数的具体意义?
- 单机上一个线程池正在处理服务如果忽然断电怎么办(正在处理和阻塞队列里的请求怎么处理)?
- 使用无界阻塞队列会出现什么问题?接口如何处理重复请求?
百度一面:
- 介绍一下集合框架?
- hashmap hastable 底层实现什么区别?hashtable和concurrenthashtable呢?
- hashmap和treemap什么区别?低层数据结构是什么?
- 线程池用过吗都有什么参数?底层如何实现的?
- sychnized和Lock什么区别?sychnize 什么情况情况是对象锁?什么时候是全局锁为什么?
- ThreadLocal 是什么底层如何实现?写一个例子呗?
- volitile的工作原理?
- cas知道吗如何实现的?
- 请用至少四种写法写一个单例模式?
- 请介绍一下JVM内存模型?用过什么垃圾回收器都说说呗线上发送频繁full gc如何处理?CPU使用率过高怎么办?如何定位问题?如何解决说一下解决思路和处理方法
- 知道字节码吗?字节码都有哪些?Integer x =5,int y =5,比较x =y 都经过哪些步骤?讲讲类加载机制呗都有哪些类加载器,这些类加载器都加载哪些文件?
- 手写一下类加载Demo
- 知道osgi吗?他是如何实现的?
- 请问你做过哪些JVM优化?使用什么方法达到什么效果?
- classforName(“java.lang.String”)和String classgetClassLoader() LoadClass(“java.lang.String”)什么区别啊?
今日头条
- HashMap如果一直put元素会怎么样? hashcode全都相同如何?
- ApplicationContext的初始化过程?
- GC 用什么收集器?收集的过程如何?哪些部分可以作为GC Root?
- Volatile关键字,指令重排序有什么意义 ?synchronied,怎么用?
- Redis数据结构有哪些?如何实现sorted set?
- 并发包里的原子类有哪些,怎么实现?
- MvSql索引是什么数据结构? B tree有什么特点?优点是什么?
- 慢查询怎么优化?
- 项目: cache,各部分职责,有哪些优化点
京东金融面试
- Dubbo超时重试;Dubbo超时时间设置
- 如何保障请求执行顺序
- 分布式事务与分布式锁(扣款不要出现负数)
- 分布式Session设置
- 执行某操作,前50次成功,第51次失败a全部回滚b前50次提交第51次抛异常,ab场景分别如何设计Spring (传播特性)
- Zookeeper有却些作用
- JVM内存模型
- 数据库垂直和水平拆分
- MyBatis如何分页;如何设置缓存;MySQL分页
蚂蚁金服二面
- 自我介绍、工作经历、技术栈
- 项目中你学到了什么技术?(把三项目具体描述了很久)
- 微服务划分的粒度
- 微服务的高可用怎么保证的?
- 常用的负载均衡,该怎么用,你能说下吗?
- 网关能够为后端服务带来哪些好处?
- Spring Bean的生命周期
- HashSet是不是线程安全的?为什么不是线程安全的?
- Java 中有哪些线程安全的Map?
- Concurrenthashmap 是怎么做到线程安全的?
- HashTable你了解过吗?
- 如何保证线程安全问题?
- synchronized、lock
- volatile 的原子性问题?为什么i++这种不支持原子性﹖从计算机原理的设计来讲下不能保证原子性的原因
- happens before 原理
- cas操作
- lock和 synchronized 的区别?
- 公平锁和非公平锁
- Java读写锁
- 读写锁设计主要解决什么问题?
02_volatile是什么
volatile是JVM提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排(保证有序性)
03_JMM内存模型之可见性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
可见性
通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
04_可见性的代码验证说明
import java.util.concurrent.TimeUnit;
/**
* 假设是主物理内存
*/
class MyData {
//volatile int number = 0;
int number = 0;
public void addTo60() {
this.number = 60;
}
}
/**
* 验证volatile的可见性
* 1. 假设int number = 0, number变量之前没有添加volatile关键字修饰
*/
public class VolatileDemo {
public static void main(String args []) {
// 资源类
MyData myData = new MyData();
// AAA线程 实现了Runnable接口的,lambda表达式
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 线程睡眠3秒,假设在进行运算
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改number的值
myData.addTo60();
// 输出修改后的值
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
}, "AAA").start();
// main线程就一直在这里等待循环,直到number的值不等于零
while(myData.number == 0) {
}
// 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
// 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
System.out.println(Thread.currentThread().getName() + "\t mission is over");
}
}
由于没有volatile
修饰MyData
类的成员变量number
,main
线程将会卡在while(myData.number == 0) {}
,不能正常结束。若想正确结束,用volatile
修饰MyData
类的成员变量number
吧。
volatile类比
没有volatile修饰变量效果,相当于A同学拷贝了老师同一课件,A同学对课件进一步的总结归纳,形成自己的课件,这就与老师的课件不同了。
有volatile修饰变量效果,相当于A同学拷贝了老师同一课件,A同学对课件进一步的总结归纳,形成自己的课件,并且与老师分享,老师认可A同学修改后的课件,并用它来作下一届的课件。
05_volatile不保证原子性
原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。
volatile不保证原子性案例演示:
class MyData2 {
/**
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;
public void addPlusPlus() {
number ++;
}
}
public class VolatileAtomicityDemo {
public static void main(String[] args) {
MyData2 myData = new MyData2();
// 创建10个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while(Thread.activeCount() > 2) {
// yield表示不执行
Thread.yield();
}
// 查看最终的值
// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
}
}
最后的结果总是小于20000。
06_volatile不保证原子性理论解释
number++
在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++
被编译成3条指令。
假设我们没有加 synchronized
那么第一步就可能存在着,三个线程同时通过getfield
命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd
命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd
命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。
07_volatile不保证原子性问题解决
可加synchronized解决,但它是重量级同步机制,性能上有所顾虑。
如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger。
import java.util.concurrent.atomic.AtomicInteger;
class MyData2 {
/**
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;
AtomicInteger number2 = new AtomicInteger();
public void addPlusPlus() {
number ++;
}
public void addPlusPlus2() {
number2.getAndIncrement();
}
}
public class VolatileAtomicityDemo {
public static void main(String[] args) {
MyData2 myData = new MyData2();
// 创建10个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.addPlusPlus2();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while(Thread.activeCount() > 2) {
// yield表示不执行
Thread.yield();
}
// 查看最终的值
// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
System.out.println(Thread.currentThread().getName() + "\t finally number2 value: " + myData.number2);
}
}
输出结果为:
main finally number value: 18766
main finally number2 value: 20000
08_volatile指令重排案例1
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
重排案例
public void mySort{
int x = 11;//语句1
int y = 12;//语句2
× = × + 5;//语句3
y = x * x;//语句4
}
可重排序列:
- 1234
- 2134
- 1324
问题:请问语句4可以重排后变成第一个条吗?答:不能。
重排案例2
int a,b,x,y = 0
线程1 | 线程2 |
---|---|
x = a; | y = b; |
b = 1; | a = 2; |
x = 0; y = 0 |
如果编译器对这段程序代码执行重排优化后,可能出现下列情况:
线程1 | 线程2 |
---|---|
b = 1; | a = 2; |
x = a; | y = b; |
x = 2; y = 1 |
这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
09_volatile指令重排案例2
观察以下程序:
public class ReSortSeqDemo{
int a = 0;
boolean flag = false;
public void method01(){
a = 1;//语句1
flag = true;//语句2
}
public void method02(){
if(flag){
a = a + 5; //语句3
}
System.out.println("retValue: " + a);//可能是6或1或5或0
}
}
多线程环境中线程交替执行method01()
和method02()
,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
禁止指令重排小总结
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序,
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
线性安全性获得保证
-
工作内存与主内存同步延迟现象导致的可见性问题 - 可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
-
对于指令重排导致的可见性问题和有序性问题 - 可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
10_单例模式在多线程环境下可能存在安全问题
懒汉单例模式
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
// 这里的 == 是比较内存地址
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
}
}
输出结果:
main 我是构造方法singletonDemo
true
true
true
true
但是,在多线程环境运行上述代码,能保证单例吗?
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
输出结果:
4 我是构造方法SingletonDemo
2 我是构造方法SingletonDemo
5 我是构造方法SingletonDemo
6 我是构造方法SingletonDemo
0 我是构造方法SingletonDemo
3 我是构造方法SingletonDemo
1 我是构造方法SingletonDemo
显然不能保证单例。
解决方法之一:用synchronized
修饰方法getInstance()
,但它属重量级同步机制,使用时慎重。
public synchronized static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
11_单例模式volatile分析
解决方法之二:DCL(Double Check Lock双端检锁机制)
public class SingletonDemo{
private SingletonDemo(){
}
private volatile static SingletonDemo instance = null;
public static SingletonDemo getInstance() {
if(instance == null) {
synchronized(SingletonDemo.class){
if(instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
}
DCL中volatile解析
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。instance = new SingletonDemo();
可以分为以下3步完成(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //1.分配对象内存空间
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
12_CAS是什么
Compare And Set
示例程序
public class CASDemo{
public static void main(string[] args){
AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . ..
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
System.out.println(atomicInteger.compareAndset(5, 1024)+"\t current data: "+atomicInteger.get());
}
}
输出结果为
true 2019
false 2019
13_CAS底层原理-上
Cas底层原理?如果知道,谈谈你对UnSafe的理解
atomiclnteger.getAndIncrement();
源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex); }
}
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
...
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
UnSafe
1 Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
2 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3 变量value用volatile修饰,保证了多线程之间的内存可见性。
CAS是什么
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)
14_CAS底层原理-下
继续上一节
UnSafe.getAndAddInt()
源码解释:
var1
AtomicInteger对象本身。var2
该对象值得引用地址。var4
需要变动的数量。var5
是用过var1
,var2
找出的主内存中真实的值。- 用该对象当前的值与
var5
比较:- 如果相同,更新
var5+var4
并且返回true
, - 如果不同,继续取值然后再比较,直到更新完成。
- 如果相同,更新
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
- Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
- 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
- 线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
- 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
- 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。
底层汇编
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)
UnsafeWrapper("Unsafe_CompareAndSwaplnt");
oop p = JNlHandles::resolve(obj);
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e))== e;
UNSAFE_END
//先想办法拿到变量value在内存中的地址。
//通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。
小结
CAS指令
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
15_CAS缺点
循环时间长开销很大
// ursafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
}while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
return var5;
}
我们可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
引出来ABA问题
16_ABA问题
ABA问题怎么产生的
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
17_AtomicReference原子引用
import java.util.concurrent.atomic.AtomicReference;
class User{
String userName;
int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return String.format("User [userName=%s, age=%s]", userName, age);
}
}
public class AtomicReferenceDemo {
public static void main(String[] args){
User z3 = new User( "z3",22);
User li4 = new User("li4" ,25);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
}
}
输出结果
true User [userName=li4, age=25]
false User [userName=li4, age=25]
18_AtomicStampedReference版本号原子引用
原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。
19_ABA问题的解决
ABA问题程序演示及解决方法演示:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
/**
* 普通的原子引用包装类
*/
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
// 传递两个值,一个是初始值,一个是初始版本号
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("============以下是ABA问题的产生==========");
new Thread(() -> {
// 把100 改成 101 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
/
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
/
System.out.println("============以下是ABA问题的解决==========");
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t3一秒钟
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 传入4个值,期望值,更新值,期望版本号,更新版本号
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:"
+ atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());
}, "t4").start();
}
}
输出结果
============以下是ABA问题的产生==========
true 2019
============以下是ABA问题的解决==========
t3 第一次版本号1
t4 第一次版本号1
t3 第二次版本号2
t3 第三次版本号3
t4 修改成功否:false 当前最新实际版本号:3
t4 当前实际最新值100
20_集合类不安全之并发修改异常
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.Vector;
public class ArrayListNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//List<String> list = new Vector<>();
//List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
上述程序会抛java.util.ConcurrentModificationException
解决方法之一:Vector
解决方法之二:Collections.synchronizedList()
21_集合类不安全之写时复制
上一节程序导致抛java.util.ConcurrentModificationException的原因解析
先观察下抛错打印栈堆信息:
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at com.lun.collection.ArrayListNotSafeDemo.lambda$0(ArrayListNotSafeDemo.java:20)
at java.lang.Thread.run(Thread.java:748)
可看出toString(),Itr.next(),Itr.checkForComodification()后抛出异常,那么看看它们next(),checkForComodification()源码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
...
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;//modCount在AbstractList类声明
Itr() {
}
...
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();//<---异常在此抛出
}
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//添加时,修改了modCount的值
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
...
}
public abstract class AbstractList<E