线程的安全性分析

138 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

原子性、可见性、有序性

优化: CPU高速缓存的速度差异、分时复用CPU、优化指令执行顺序

硬件上的一些优化会导致这些问题

不可见性

image-20220920184315745

原子性

image-20220921184217510

有序性

处理器之间的指令编译重排序

Java内存模型

Java内存模型是一种抽象结构,他提供了合理的禁用缓存一级禁止重排序的方法来解决可见性、有序性问题

image.png

提供了合理的禁用缓存一级禁止重排序的方法来解决可见性、有序性问题

内存屏障

同步关键字synchronized

可以解决可见性、原子性、有序性

可以加在代码块上,也可以加在方法上

image-20220925111314477

image.png

锁实例对象

image-20220925112124846

image-20220925112942257

第二个框里的锁的范围可以控制

synchronized的本质

image-20220925131115333

synchronized的优化

image.png

volatile关键词分析

作用:

上面讲到不可见性的时候,定义了一个boolean值stop,但是线程读不到,所以停不下来,但是:

image-20220925132109758

image-20220925132120471

就变成可见性了

可见性

原理:

禁用缓存

将当前处理器缓存行的数据写回系统内存

这个写回的操作会使其他CPU里缓存的该内存地址的数据无效

什么情况下用到volatile

image.png

有序性

volatile如何解决有序性问题?

指令重排序

x86结构下面提供了CPU层面的内存屏障:

image.png

image-20220925134157833

本质上来说: volatile实际上是通过内存屏障来防止指令重排序以及 禁止cpu高速缓存来解决可见性问题。 而#ock指令,它本意上是禁止高速缓存解决可见性问题,但实际上在 这里,它表示的是种内存屏障的功能。也就是说针对当前的硬件环境, JMM层面采用Lock指令作为内存屏障来解决可见性问题

final域

image-20220925135354366

image-20220925141039544

image-20220925141058835

image-20220925141304002

final域的重排序规则:

在引用变量被任意线程可见之前,这个引用变量指向的对象的final域已经在构造函数中被正确初始化过

还有一个保障是在构造函数内部,不能让这个这个变量引用被其他线程可见

image-20220925145315354

Happens-Before规则

可以理解为编译器自带的一些禁止重排序的规则

image-20220925145358015

指令执行顺序会优化,导致可见性问题,但是HB规则的语句不会

image-20221001165919117

juejin.cn/post/687776…

程序顺序规则

as-if-serial

单线程中不管怎么重排序, 单线程的执行结果是不会改变的。

编译器、处理器都要遵守asifserial语义,不会对有数据依赖关系的数据重排序

监视器锁规则

image-20221001201151843

这个是理所当然的“规则”

volatile变量规则

对一个volatile域的写,hb于任意后续对这个volatile域的读

传递性

image-20221001201615878

start()规则

image-20221001202120834

join()

image-20221001202445293

其他线程读到之后,var还是66

原子类Atomic

无锁工具的典范

// 参数是初始大小
private static AtomicInteger atomicInteger = new AtomicInteger(0);
​
​
atomicInteger.incrementAndGet();

因为加锁势必会带来性能问题,所以无锁化编程

实现原理

image-20221001203409799

valueOFFset是: 这个原子类在内存中的偏移量

image-20221001203808806

unsafe中的方法:

image-20221001204250672

类似乐观锁

里面一直在循环,在执行,这里的性能消耗可以忽略

只有等offset(value)等于了expect才会跳出循环

image-20221001204847358

还有很多原子类:

image-20221001205309811

image-20221001205258162

ThreadLocal实现原理

Demo:

image-20221001205515604

这里的每个线程的结果都是不确定的

image-20221001205559121

比如这里希望每个线程刚开始的时候都拿到的是0

image-20221001205850280

image-20221001205912030

image-20221001205929250

可以看到这里线程之间产生隔离了,值设置回去了也不会对其他线程产生影响

实现原理

image-20221001211000080

如果map为空

image-20221001211156329

image-20221001211338649

image-20221001211450191

上面有计算hashcode的过程

也包含一些,动态扩容、清除空key的等等......

image-20221001211852953