JUC 并发编程--05, Volatile关键字特性: 可见性, 不保证原子性,禁止指令重排, 代码证明过程. CAS了解么 , ABA怎么解决, 手写自旋锁和死锁
问: 了解volatile关键字么?
答: 他是java 的关键字, 保证可见性, 不保证原子性, 禁止指令重排
问: 你说的这三个特性, 能写代码证明么?
答: ....
问: 听说过 CAS么 他的缺点是什么? 什么是ABA, 怎么解决?
问: 请手写一个自旋锁?
可见性证明:

接下来看使用 了 volatile的结果

不保证原子性 证明:
private volatile Integer num = 0;
private AtomicInteger anum = new AtomicInteger(0);
@Test
public void test2() {
/**
* 证明不保证原子性: 原子性就是: 一个线程对共享变量操作,这个操作一旦开始,就会一直运行到结束,
* 不会被别的线程打断,切换到另一个线程, 这个操作是不可分割的
*/
// 这里用10个线程执行100次, 50个线程执行1000次, 50个线程执行100万次, 看最终num的值是否符合预期
for (int i = 0; i < 50; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
num = num +1;
//anum.getAndIncrement();
}
}).start();
}
while(Thread.activeCount()>2){}
System.out.println("anum:" +num);
//System.out.println("anum:" +anum.get());
//这里用10个线程执行100次 应该为1000 实际结果为900,
// 50个线程执行1000次, 应该为 50000, 实际结果为 49000
// 50个线程执行100万次, 应该为 5000万, 实际为 293028
//实际结果和 预期结果不一样,说明volatile 并不能保证原子性,当一个线程对共享变量操作的时候, 并不能保证这个操作不被中断,
}
造成这样的原因:
假设i = 0, 线程A读取0到自己的工作内存, A对该值加1操作,但正准备将1赋给i时,由于此时i的值并没有改变
线程B读取主存的值0到自己的工作内存, 并执行了加1操作,正准备将1赋给i时, 此时线程A将1赋给了i,由于volatile的影响
立即同步到主存, 主存中的值为1, 并是线程B工作内存的i失效, B执行第三步,虽然此时B工作内存中的i失效了, 但是第三部是将
1赋给i, 对B来说,我只是赋值操作, 并没有使用i这个动作, 所以这一步并不会取刷新主存, B将1赋值给i, 并立即同步到主存, 主存
中的值仍为1. 虽然A/B都执行了 加1操作,但主存却为 1, 这就是最终结果和预期不一致的原因
如何解决这个volatile不保证原子性问题呢? 使用原子类中的AtomicInteger 这个类来保证原子性.
为什么 普通的Integer 不行,使用了 AtomicInteger这个原子类就能保证原子性呢?? 是因为 CAS, atmoicInteger类中的CAS 底层原理是 unsafe类和 自旋锁,
源码:

CAS的缺点: 由于CAS底层是 unsafe类 和自旋锁, 可以看到unsafe类有很多native方法, 这些方法是c或者c++写的,转换为汇编指令,直接操作硬件,所以操作硬件是天生就是原子性的,这也就是atomic类解决原子性的原因.
下面解析CAS源码: getAndAddInt 方法的入参: var1:当前对象 var2: 当前对象在内存中的偏移量, 通过 var1 和 var2 就可以准确找到这个对象的值, 就好像 var1 是名字, var2是 你在教室座位的坐标, 通过这二个可以准确找到你再内存中的位置和值, var4: 增加的值, 一个 do_while 循环, 先从内存中找到当前对象的值, while循环判断: 如果 var5 和 var1,var2 对应的值相同,就将 var5+var4设置成新值, 这个可以这么理解: 主存中有个变量为5, 你先将5读到自己的工作内存,并修改为 6,在将要写回主存的时候, 你期望主存的值还是5, 与主存中的实际值相比较,如果主存中的实际值也是5, 说明没有被别的线程修改过,此时就 将6写回主存, 并跳出死循环, 可以看到cas底层是保证了 值的最终一致性, 这样会导致ABA问题, 同时cas是操作硬件的,这就保证了原子性, 可以根据这个特性,自己实现一个lock锁.

自旋锁, 看源码可以看到有个 do--while-方法,compareAndSwapInt(var1, var2, var5, var5 + var4)这个方法是比较并交换,va1,va2指的是内存中的对象, var5是期望值, 比较内存中的值和期望值是否相等,相等就把var5+var4赋值给内存的值,并返回true, 否则就返回false. 这里会循环比较,如果不相等就一直循环,知道相等才跳出. 这样好处是不阻塞,缺点是: 如果某个线程持有锁时间太长,导致别的线程循环次数太多,开销大. 另外 compareAndSwapInt() 这个方法会导致 ABA 问题..
问: 什么是ABA 问题, 怎么解决?
ABA 就是: 主存中i=A, 线程1将A 读到自己的工作内存中, 线程2也从主存中读取A到自己的工作内存中,修改为B,之后写回到主存. 线程3此时也抢过cpu执行权,从主存中读取值B到自己的工作内存中,修改为A后,回写到主存中, 线程1,最后执行回写主存,回写到主存是CAS原则, 由于主存中的值A,与线程1中的值A, 值相同,所以回写主存成功.
但是 此时 此A 非 彼A, 值相同,并不一定就是同一变量,这就会导致数据不一致问题
怎么解决ABA问题? 引入原子引用来解决ABA问题. 回写主存的时候,会调用compareAndSet()方法, 此时加上一个 版本号或者时间戳, 回写的时候,会比较版本号是否和期望的相同,相同才更新.
接下来代码演示: ABA问题, 和解决方法
class ABA {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) {
//这里演示 ABA问题
System.out.println("原始值为:" + atomicInteger.get());
new Thread(() -> {
atomicInteger.compareAndSet(0, 1);
System.out.println(Thread.currentThread().getName() + "步骤一: 改为1, 当前值为:" + atomicInteger.get());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicInteger.compareAndSet(1, 0);
System.out.println(Thread.currentThread().getName() + "步骤二: 改为0, 当前值为:" + atomicInteger.get());
}, "线程1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicInteger.compareAndSet(0, 10);
System.out.println(Thread.currentThread().getName() + "步骤三: 改为10, 当前值为:" + atomicInteger.get());
}, "线程2").start();
}
}
运行结果: 线程1 将值0, 改为1,之后又改回0, 线程2:比较主存中的0, 和期望值0,相同,所以改为10, 但是此时的 0 和 之前的0, 值相同,不一定是同一对象

引入-原子引用-来解决ABA:
class AtomicABA {
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(0, 1);
public static void main(String[] args) {
//这里演示 ABA问题, 解决方法: 引入原子引用
System.out.println("原始值为:" + atomicStampedReference.getReference() + "--版本号为:" + atomicStampedReference.getStamp());
new Thread(() -> {
atomicStampedReference.compareAndSet(0, 1, 1, 2);
System.out.println(Thread.currentThread().getName() + "步骤一: 修改后的值为:" + atomicStampedReference.getReference() + "--版本号为:" + atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(1, 0, 2, 3);
System.out.println(Thread.currentThread().getName() + "步骤二: 修改后的值为:" + atomicStampedReference.getReference() + "--版本号为:" + atomicStampedReference.getStamp());
}, "线程1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(0, 10, 1, 10);
System.out.println(Thread.currentThread().getName() + "步骤三: 修改后的值为:" + atomicStampedReference.getReference() + "--版本号为:" + atomicStampedReference.getStamp());
}, "线程2").start();
while (Thread.activeCount() > 2) {}
System.out.println("最终结果为:" + atomicStampedReference.getReference());
}
}
运行结果为: 引入原子引用之后, 回写内存时候, 调用 compareAndSet方法都会,先比较版本号, 相同之后才会更新

问: 刚刚你说 原子类的底层是unsafe类和自旋锁,能手写一个自旋锁么?
class SpinLockDemo {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println("这里是加锁" + thread.getName() + "----锁对象是:" + thread);
while(! atomicReference.compareAndSet(null,thread)){} //这里是自旋锁的实现
}
public void myUnlock(){
Thread thread = Thread.currentThread();
System.out.println("这里是解锁" + thread.getName() + "----锁对象是:" + thread);
atomicReference.compareAndSet(thread,null);
}
static Integer num = 0;
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLock = new SpinLockDemo();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
spinLock.myLock();
num++;
spinLock.myUnlock();
}).start();
}
while(Thread.activeCount() >2){}
System.out.println(num);//没有加锁时候,结果为9945(这是由于, 共享变量不是原子的类引起的). 加了锁之后,结果为 10000
}
}
死锁代码; 线程1持有锁a, 尝试获取锁b, 线程2持有锁b,尝试获取锁a
// 死锁案例
class CycleLock{
public static void main(String[] args) {
String lock1 = "123";
String lock2 = "abc";
new Thread(()->{
new B(lock1,lock2).getLock();
},"线程1").start();
new Thread(()->{
new B(lock2,lock1).getLock();
},"线程2").start();
}
}
class B{
private String lock1;
private String lock2;
public B(String lock1, String lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
public void getLock(){
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + "已经持有锁 "+lock1);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "已经持有锁 "+lock2);
}
}
}
}
运行结果你怎么确定这就是死锁,你是怎么定位的?
关于volatile 禁止指令重排,看这个博客
https://blog.csdn.net/weixin_45007916/article/details/108076954
使用命令行: 如果是Linux 使用Linux的指令,这里演示win系统下的命令 :1:jps -l 2:jstack 进程号

jstack 15336


这里引用别人的博客,以补充本文遗漏的地方, 感谢他
https://juejin.im/post/6859390417314512909
JUC 并发编程--05, Volatile关键字特性: 可见性, 不保证原子性,禁止指令重排, 代码证明过程. CAS了解么 , ABA怎么解决, 手写自旋锁和死锁的更多相关文章
- Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
- Java并发编程:volatile关键字解析(转载)
转自https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 ...
- Java并发编程:volatile关键字解析-转
Java并发编程:volatile关键字解析 转自海子:https://www.cnblogs.com/dayanjing/p/9954562.html volatile这个关键字可能很多朋友都听说过 ...
- 6、Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
- 转:Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字, ...
- [转载]Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
- (转)Java并发编程:volatile关键字解析
转:http://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或 ...
- Java并发编程:volatile关键字解析(学习总结-海子)
博文地址:Java并发编程:volatile关键字解析
- 【转】Java并发编程:volatile关键字解析
转自:http://www.importnew.com/18126.html#comment-487304 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备 ...
随机推荐
- Python脚本写端口扫描器(socket,python-nmap)
目录 Socket模块编写 扫描给定主机是否开放了指定的端口 python-nmap模块编写 扫描给定ip或给定网段内指定端口是否开放 一个用python写的简单的端口扫描器,python环境为 3. ...
- Docker阿里云镜像存储服务
阿里云镜像服务地址 https://cr.console.aliyun.com/cn-beijing/instances/repositories 免费免费免费 登陆之后可以免费创建,仓库.地址大 ...
- xxl-job滥用netty导致的问题和解决方案
netty作为一种高性能的网络编程框架,在很多开源项目中大放异彩,十分亮眼,但是在有些项目中却被滥用,导致使用者使用起来非常的难受. 本篇文章将会讲解xxl-job作为一款分布式任务调度系统是如何滥用 ...
- (CV学习笔记)看图说话(Image Captioning)-1
Background 分别使用CNN和LSTM对图像和文字进行处理: 将两个神经网络结合: 应用领域 图像搜索 安全 鉴黄 涉猎知识 数字图像处理 图像读取 图像缩放 图像数据纬度变换 自然语言处理 ...
- 修改linux默认文件创建权限-umask命令解析
umask值用于设置用户在创建文件时的默认权限,当我们在系统中创建目录或文件时,目录或文件所具有的默认权限就是由umask值决定的. 对于root用户,系统默认的umask值是0022:对于普通用户, ...
- 【mybatis】mybatis分页拦截器搭配bootstrap-table使用
提前说明: 这一种方式已被我自己pass掉了,已经被新的方式迭代了.但是记录下自己曾经的成果还是有必要的,而且里面的思想还是不变的,另外技术不就是在不断地迭代中升级吗.千万不要想着一步完美,那样会让你 ...
- Catalan数以及相关性质的证明
\(Catalan\) 数相关证明 Mushroom 2021-5-14 \(Catalan\)数的定义 给定一个凸\(n + 1\)边形, 通过在内部不相交的对角线,把它划分成为三角形的组合,不同的 ...
- c++vs类图
安装visual studio扩展开发工具 一定要勾选右侧栏中的类设计器 安装完成后在菜单栏点击视图--类视图,会出现类视图框,在框中右键项目--查看类视图,就自动生成了.
- WM_PAINT 与 WM_ERASEBKGND消息的深入分析
当WM_PAINT消息不是由函数InvalidateRect产生的时(即通过最大话,最小化,移动,下拉菜单等),系统会先产生连续产生若干个WM_ERASEBKGND消息,紧接着在产生WM_PAINT消 ...
- (四)Jira Api对接:缺陷分析和任务分析
迭代进行期间或者结束后,在我们的测试日报或者测试报告中需要体现缺陷详细情况,甚至大家工作效率情况.本文就讨论下如何通过jira api获取缺陷信息并进行分析,同时获取需求子任务情况来了解测试和开发的工 ...