多线程【三】:线程安全与不安全

线程安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

线程不安全

一、线程不安全的原因

1.原子性

例:卖票
多线程【三】:线程安全与不安全
A-1和A-2不具有原子性,导致代码行之间插入了并发/并行执行的其他代码(B-1)
造成的结果:业务逻辑处理出现问题
当客户端A检查还有一张票时,将票卖掉,还没有执行更新数据库时,客户
端B检查了票数,发现大于0,于是又卖了一-次票。然后A将票数更新回数据
库。这是就出现了同一张票被卖了两次。

2.可见性

为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
多线程【三】:线程安全与不安全
例:
多线程【三】:线程安全与不安全
多线程【三】:线程安全与不安全
主内存SUM=0;
A-1
A-2 (+1操作—>线程A的工作内存中,SUM=1,主内存SUM=0)
B-1 (从主内存复制SUM=0)
B-2 (+1操作—>线程B的工作内存中,SUM=1, 主内存SUM=0)
B-3 (线程B中的SUM变量,将值写回主内存SUM=1)
A-3 (线程A中的SUM变量,写回主内存SUM=1)
造成线程不安全:共享变量发生了修改的丢失(线程B的+ +操作,发生丢失)

3.代码的顺序性

重排序:程序执⾏的时候,CPU、编译器可能会对执⾏顺序做⼀
些调整,导致执⾏的顺序并不是从上往下的。从⽽出现了⼀些意想不到的效果。线程内代码是JVM、CPU都进行重排序,给我们的感觉是线程内代码是有序的,是因为重排序优化方案会保证线程内代码执行的依赖关系。
线程内看自己的代码运行,都是有序的,但是看其他线程代码运行,都是无序的。
如果都是私有变量,最终结果是正确的。
例:
一段代码是这样的:
1.去超市买东西
2.去食堂用20分钟吃饭
3.去超市取快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次超市。这种叫做指令重排序。
但在多线程场景下就有问题了,可能快递是在你吃饭的20分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。

二、解决线程不安全问题

1.原子性

JDK中有atomic包提供给我们实现原⼦性操作。
多线程【三】:线程安全与不安全
特殊的原子性代码(分解执行存在编译为class文件时,也可能存在CPU执行命令):
(1)n++、n–、++n、–n都不是原子性:
需要分解为三条指令:从内存读取变量到CPU,修改变量,写回内存。
(2)对象的new操作:Object obj = new Object();
分解为三条指令:分配对象的内存、初始化对象、将对象赋值给变量。

2.可见性&有序性

使用volatile关键字:volatile是⼀种轻量级的同步机制
volatile仅仅⽤来保证该变量对所有线程的可⻅性,但不保证原⼦性
1.使⽤了volatile修饰的变量保证了三点:
(1)⼀旦你完成写⼊,任何访问这个字段的线程将会得到最新的值。
(2)在你写⼊前,会保证所有之前发⽣的事已经发⽣,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写⼊值都刷新到缓存。
(3)volatile可以防⽌重排序。如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方。
2.满⾜下⾯的条件才应该使⽤volatile修饰变量:
(1)修改变量时不依赖变量的当前值(因为volatile是不保证原⼦性的)
(2)该变量不会纳⼊到不变性条件中(该变量是可变的)
(3)在访问变量的时候不需要加锁(加锁就没必要使⽤volatile这种轻量级同步机制了)。volatile可以结合线程加锁的一些手段,提高线程效率。

3.线程封闭

在多线程的环境下,只要我们不使⽤成员变量(不共享数据),那么就不会出现线程安全的问题了。即每个线程都拥有⾃⼰的变量,互不⼲扰。

4.不变性

不可变对象⼀定线程安全的。
要想将对象设计成不可变对象,那么要满⾜下⾯三个条件:
(1)对象创建后状态就不能修改
(2)对象所有的域都是final修饰的
(3)对象是正确创建的(没有this引⽤逸出)