并发专题之---Voliate引发的各种原理问题

mac2026-05-15  5

文章目录

前言JMMvoliate不保证原子性不保证原子性的解释AtomicInteger解决不保证原子性的问题为什么AtomicInteger可以解决原子性问题?CASCAS的内部原理CAS的缺点ABA问题原子引用解决ABA问题 禁止指令重排多线程环境下单例模式出现的问题双端检索机制解决办法双端检索机制的隐患解决双端检索机制的隐患

前言

这里的线程安全的问题大部分都能用synchronized解决,但是本章不会讨论这个重量型的解决方法,而是去探讨更好的方案和原理.本博客也有博主自己的一些理解. 本章的目录由于过于繁杂和详细,博主自己也是做了一个流程图给大家,以便大家更好的理解和整理.

JMM

voliate

voliate是java虚拟机提供的轻量级的同步机制.他保证了可见性,不保证原子性,禁止指令重排

不保证原子性

代码案例

package com.bwie.demo; /** * 不能保证原子性 * 理论来个每个线程+1000 结果为2000,但是结果却是小于2000的值 */ public class VolatileUnsafe2 { private volatile long count =0; public long getCount() { return count; } public void setCount(long count) { this.count = count; } //count进行累加 public void incCount(){ count++; } //线程 private static class Count extends Thread{ private VolatileUnsafe2 simplOper; public Count(VolatileUnsafe2 simplOper) { this.simplOper = simplOper; } @Override public void run() { for(int i=0;i<10000;i++){ simplOper.incCount(); } } } public static void main(String[] args) throws InterruptedException { VolatileUnsafe2 simplOper = new VolatileUnsafe2(); //启动两个线程 Count count1 = new Count(simplOper); Count count2 = new Count(simplOper); count1.start(); count2.start(); Thread.sleep(50); // 这个必须添加,不然你getcount 可能永远为0 System.out.println(simplOper.count);//20000? } }

不保证原子性的解释

根据JMM模型,我们可以知道,当加了voliate之后,每当在自己的内存空间写完后,需要通知其他线程自己做了修改,其他线程读取其他的线程.为什么还会有不正确的值呢?

注意:voliate 只保证可见性,重点侧重于: 每个可以拿到其他线程修改之后的最新值(但并不是意味,只要有新值就可以实时获取最新值)

在上面的列子中,如果当count = 100的时候,此时A,B 因为voliate的可见性,可以拿到主内存的新值,但是此时a,b两个线程他们做的都是100+1的操作,假如此时B线程完成了100+1的操作,设置到主内存,此时主内存为101,但是A线程还没有结束任务呀,此时他让仍然继续执行100+1的操作,相当于住内存的值 又覆盖了一遍101

有同学会问啦,不知加了voliate,会通知其他线程,读取主内存的最新值,为什么A线程还在进行100+1的操作呀,他不是应该中断这个操作,重新读取主内存的值101,然后进行101+1的操作?

如果你真的这么想,那么只能说明你对原子性和可见性含义搞蒙啦(当然,我之前 也懵了好久)

可见性:他只会保证每个线程读取的值,是上一个线程修改之后的最新值,也就是主内存的,比如count 进行100+1的操作,此时100必定是上一个线程操作之后的结果。此时他侧重的是:在我获取主内存的那一瞬间,主内存的值是最新的,注意关键词 获取主内存的那一瞬间,主内存的值最新的,就跟上面的列子一样,此时A,B他们获取到的值都是100,都是最新的,此时已经满足了可见性

此时加入B线程完成了100+1的操作,此时主内存已经是101,A之所以不会中断100+1的操作,重新读取主内存的值,是因为可见性的特点是:获取主内存的那一瞬间,主内存的值新的,而此时他已经不是获取主内存的时间,而是在自己的内存中进行修改,此时他就会把自己的设置的101又进行一遍住内存赋值,但是下一次他去主内存获取值的时候绝对是保证是102

那我考考大家,如果大家要解决这个问题该怎么做呀?

方案: 在进行比较的时候,看一下主内存的值,是不是我之前读取的值,如果不是,我就不把值set到主内存啦,我重新读取住内存的值,然后进行+1的操作。 聪明的你,一直是这么想的对不对!! 因为有了这个解决方案,我们的AtomicInteger 早就帮我考虑好了这个方案的封装

AtomicInteger解决不保证原子性的问题

package com.bwie.demo; import java.time.temporal.ValueRange; import java.util.concurrent.atomic.AtomicInteger; /** * 不能保证原子性 */ public class VolatileUnsafe2 { AtomicInteger count = new AtomicInteger(0); public void addCount(){ count.getAndIncrement(); } static class MyData extends Thread{ private VolatileUnsafe2 simplOper; public MyData(VolatileUnsafe2 simplOper) { this.simplOper = simplOper; } @Override public void run() { for (int i = 0; i <1000 ; i++) { simplOper.addCount(); } } } public static void main(String[] args) throws InterruptedException { VolatileUnsafe2 volatileUnsafe2 = new VolatileUnsafe2(); MyData myData = new MyData(volatileUnsafe2); MyData myData1 = new MyData(volatileUnsafe2); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } myData.start(); myData1.start(); Thread.sleep(5000); System.out.println(volatileUnsafe2.count); } }

为什么AtomicInteger可以解决原子性问题?

CAS

什么是cas CAS:又称为CompareAndSwap 比较并且交换,在我们上面的分析中,我们可以知道,因为即便加了voliate还是会有原子性的问题,其核心问题所在就是希望设置主内存值进行设置的时候,期望没有人对其修改过.而cas就是在修改的时候, 有会一个期望值,如果和期望一样的话,就说明没有修改过.此时可以进行正常设置值,他是原语,天生安全 案例

AtomicInteger atomicInteger = new AtomicInteger(5); System.out.println(atomicInteger.compareAndSet(5,2019+atomicInteger.get())); //此时实际值已经不是5啦 是2019 System.out.println(atomicInteger.compareAndSet(5,1024+atomicInteger.get())); 输出结果 true false
CAS的内部原理

总结就是自旋锁和Unsafe类 Unsafe类 根据上面我们可以知道 getAndAddInt是unSafe的方法,而Unsafe类天生就是java原句,他是天生原子性,他的所有方法都是安全的.

//以下是getAndIncrement的内部源码 public final int getAndIncrement() { /** * arg0:当前对象 * arg1:内存偏移量 * arg2:自增量 * 大概意思就是当前对象的这个值为多少 / return unsafe.getAndAddInt(this, valueOffset, 1); }

下面这个源码是cas的核心精华所在

public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //get 方法, 根据当前对象的偏移量 得到具体的当前对象 var5 = this.getIntVolatile(var1, var2); //得到主内存的值 //var1 auomicInteger本身 //var2 该对象值得引用地址 //var4 需要改动的数量 //var5 是用过var1和var2找出的主内存中真实值 //知道修改成功啦.会跳出循环 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }

上面的代码可以阐述为如下图的场景

CAS的缺点

1:因为do While 循环,所以可能循环时间长,开销比较大; 2: 只能保证一个共享变量的原子操作.因为他的var1值得就是当前对象 3:引出ABA问题(核心)

ABA问题

AB两个线程做操作,主内存的值为1,此时他们进行拷贝,他们各自的空间的值都为1 A线程把主内存的值1改为2,然后又该1, 此时B过来来修改至根据cas的期望值,他发现1就是他所期望的值,他认为并没有人对主内存进行修改过.这是不符合CAS的原理的.他只管开头和结尾,不关心中心的内容,这是不对的

原子引用解决ABA问题

先介绍下原子引用: 前面的aticInteger只能解决int类型,但是如果是对象怎么办呢?

原子引用的简单代码了解

User user = new User("张三", 1); User user2 = new User("李四", 2); AtomicReference<User> atomicReference = new AtomicReference<>(); //主物理内存为张三 atomicReference.set(user); //张三变更为李四 //因为user 和 前面的主物理内存相同,所以根据cas可以修改 true-->结果com.bwie.demo.User@16b4a017 System.out.println(atomicReference.compareAndSet(user,user2)+"-->结果"+atomicReference.get().toString()); //因为前面的操作 此时 主内存为李四,而这里他期待的是张三 所以修改失败 false-->结果com.bwie.demo.User@16b4a017 System.out.println(atomicReference.compareAndSet(user,user2)+"-->结果"+atomicReference.get().toString());

时间戳的原子引用解决ABA问题 最好的办法就是在每次操作的时候加上版本号, 类似乐观锁,加入此时有两个线程T1 T2 但是每次线程修改的时候,都需加上一个版本字段,每次修改依次增加1 比如下面的列子

线程名称变量内容修改次数T11001T21001

此时T1做了修改吧100修改为101 ,然后在修改回来100,中间操作了2次,此时修改次数加2

T11003T21001

此时线程线程在进行检验的时候,不仅要检查修改的内容是否和预期相同,还要检查每次修改次数

public class SingletonDemo { static AtomicReference<Integer> atomicReference = new AtomicReference<>(100); public static void main(String[] args) { new Thread(() -> { System.out.println(Thread.currentThread().getName()+"==="+atomicReference.compareAndSet(100, 101));; System.out.println(Thread.currentThread().getName()+"==="+atomicReference.compareAndSet(101, 100));; }, "t1").start(); new Thread(() -> { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"==="+atomicReference.compareAndSet(100, 2019)); }, "t2").start(); } } 输出结果 t1===true t1===true t2===true

上面的例子并不能解决ABA问题,接下来,我们看一下解决方案

public class SingletonDemo { static AtomicReference<Integer> atomicReference = new AtomicReference<>(100); static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1); public static void main(String[] args) { new Thread(() -> { int stamp = atomicStampedReference.getStamp(); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"第一次版本号"+stamp); //暂停一秒 System.out.println(Thread.currentThread().getName()+"===>"+atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));; System.out.println("第二次版本号"+atomicStampedReference.getStamp()+"实际内容"+atomicStampedReference.getReference()); System.out.println(Thread.currentThread().getName()+"===>"+atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));; System.out.println("第三次版本号"+atomicStampedReference.getStamp()+"实际内容"+atomicStampedReference.getReference()); }, "t1").start(); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); //让他们都拿到初始的值 try { Thread.sleep(8000); // 等待,让t2看不到aba出现的过程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"的期望值为"+stamp); System.out.println(Thread.currentThread().getName()+"===>"+atomicStampedReference.compareAndSet(100, 2019,stamp,stamp+1)); System.out.println("第三次版本号"+atomicStampedReference.getStamp()+"实际内容"+atomicStampedReference.getReference()); }, "t2").start(); } } t1第一次版本号1 t1===>true 第二次版本号2实际内容101 t1===>true 第三次版本号3实际内容100 t2的期望值为1 t2===>false 第三次版本号3实际内容100

禁止指令重排

我们首先看一下例子 案例一 数据依赖性:在多线程的也可以能2134. 因为x和y的声明互相不影响对方,所以可以指令重排,但是不能3124, 因为必须现有x的声明,才能进行x的操作,所以此时不能进行指令重排 案例二 在下面的代码里面,因为指令重排很可能会出现如下的问题; 案例3 以上的代码如果出现1,2的位置互换,在多线程的环境下,此时另一个线程调用method2的时候,此时他会先读取flag=true,而此时的 a并没有赋值,此时默认为0

多线程环境下单例模式出现的问题

package com.bwie.demo; import java.time.temporal.ValueRange; import java.util.concurrent.atomic.AtomicInteger; /** * 不能保证原子性 */ public class SingletonDemo { public static SingletonDemo instance = null; public SingletonDemo() { System.out.println("我是构造方法"); } private static SingletonDemo getInstance(){ if (instance==null){ instance = new SingletonDemo (); } return instance; } public static void main(String[] args) { SingletonDemo instance = SingletonDemo.getInstance(); SingletonDemo instance1 = SingletonDemo.getInstance(); System.out.println(instance.equals(instance1)); } }

通过上面的案例,我们可以知道因为是单例模式,所以构造方法只会输出一次,但是多线程的环境下,则不然啦 多线程的环境下

public class SingletonDemo { public static SingletonDemo instance = null; private SingletonDemo() { System.out.println("我是构造方法"); } private static SingletonDemo getInstance() { if (instance == null) { instance = new SingletonDemo(); } return instance; } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.submit(() -> { instance = SingletonDemo.getInstance(); }); } executorService.shutdown(); } }

输出结果

我是构造方法 我是构造方法 我是构造方法

双端检索机制解决办法

双端检索机制

public class SingletonDemo { public static SingletonDemo instance = null; private SingletonDemo() { System.out.println("我是构造方法"); } private static SingletonDemo getInstance() { if (instance == null) { synchronized (SingletonDemo.class) { if (instance==null){ instance = new SingletonDemo(); } } } return instance; } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.submit(() -> { instance = SingletonDemo.getInstance(); }); } executorService.shutdown(); } }

双端检索机制的隐患

从上面的结果我们可以看出来,好像加了双端检索机制貌似就没有出现问题啦, 而且运行检验的时候,他的确是只输出了一次构造方法吗? 但是问题真的就这么解决了吗?

DCL(双端检索机制)机制不一定安全,因为有指令重排的存在,加入voliate则可以禁止指令重排 原因是在某个线程执行到第一次检测,读取到instance为null的时候,instance对象没有完成对象的初始化,而 instance = new SingletonDemo();又可以分为三步 memory = allocate() 分配内存空间 语句1 instance(memory ) 初始化对象 语句2 instance = memory 实例指向内空间 语句3 因为语句2和语句3没有数据依赖性,所以 他们的顺序可以指令重排, 如果为132顺序的话.在对象还没有完成对象的初始化的时候,直接把null的对象指向内存空间,会导致出现null的结果

解决双端检索机制的隐患

加上voliate

public class SingletonDemo { public static volatile SingletonDemo instance = null; private SingletonDemo() { System.out.println("我是构造方法"); } private static SingletonDemo getInstance() { if (instance == null) { synchronized (SingletonDemo.class) { if (instance==null){ instance = new SingletonDemo(); } } } return instance; } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.submit(() -> { instance = SingletonDemo.getInstance(); }); } executorService.shutdown(); } }
最新回复(0)