并发学习(六) — 线程安全的三个方面
一、线程安全性:
当多个线程访问某个类,不管运行环境采用何种调度方式或者这些进程将如何调用,并且在主调代码中不需要额外的同步或协同,都能表现出正确的行为,这就是线程安全的。
主要体现在三个方面:
- 原子性:互斥访问,同一个时刻只能有一个线程来对它进行操作, 如Atomic包,锁
- 可见性:一个线程对主内存的修改可以及时的被其他线程观察的到
- 有序性:一个线程观察其他线程中指令执行顺序,由于指令重排序存在,观察结果一般杂乱无序
二、原子性
Atomic包:
- Atomic包:核心就是CAS,使用的是Unsafe类用的compareAndSafeInt方法(CAS),其实就是将当前值和期望值(底层值)相同,才赋值,否则就一直循环。CAS缺点就是在一个死循环中尝试着修改值,竞争不激烈修改很高,当竞争高,效率下降。对于普通long,double类型,JVM允许64位读操作或写操作拆成2个32位的操作
- JDK8增加了LongAdder类,在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base直接更新很好保障和AtomicLong一样,高并发通过分散提高了性能,缺点是统计时可能会有误差。(优先用)
- compareAndSet常用于AtomicBoolean,希望某件事执行一次。
- AtomicReference和AtomicIntegerFieldUpdater原子性去更新某个类的实例的某一个字段(volatile,不能是static)
- AtomicStampReference:解决的是CAS的ABA问题,加了个版本号来区别。
锁:
- Synchronized : 依赖JVM,作用对象的作用范围内,都是同一时刻只有一个线程操作的。
- Lock:依赖特殊的CPU指令,代码实现,ReentrantLock。
Synchronized:
- 修饰代码块:大括号括起来的代码,作用于调用的对象
- 修饰方法:整个方法,作用于调用的对象
- 修饰静态方法:整个静态方法,作用于所用对象
- 修饰类:括号括起来的部分,作用于所有对象
前两个锁的是当前对象,当多个对象则互不影响,当方法内部是一个完整的代码块和一个synchronized的方法是等同的。
重要一点,当前类是父类,子类继承父类的synchronized方法,是不带synchronized关键字,必须自己带上关键字。
原子性对比:
- synchronized:不可中断锁,适合竞争不激烈,可读性好
- Lock:可中断锁,多样化同步,竞争激烈时能维持常态
- Atomic:竞争激烈时能维持常态,比Lock性能好,只能同步一个值
三、可见性:
- 导致共享变量在线程间不可见的原因:
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主存间及时更新
可见性 — synchronized(规则)
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁,解锁是同一把锁)
可见性 — volatile
通过内存屏障和禁止重排序来优化实现
- 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
- 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
总之:每次对被线程访问时都强迫从主内存中读取该变量的值,而该变量发生变化时,又强迫线程将最新的值刷新回主内存,任何时候线程都能看到最新的值。
volatile执行count操作是不是安全的,不适用计数场景。没有原子性。
常用于状态标记量。
四、有序性:
Java内存模型中,允许编译器和处理器对指令进行重排序优化,但是重排序不影响单线程的执行,只会影响多线程并发执行。
仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦。
Happens before 天生的有序性:
先行发生原则:8个,如果不满足,则虚拟机可以随意对指令重排序
程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
传递性 A先于B ,B先于C 那么A必然先于C
线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
对象终结规则 对象的构造函数执行,结束先于finalize()方法
五、总结:
原子性: 互斥访问,Atomic包,CAS算法,Synchronized,Lock
可见性:synchronized,volatile
顺序性:happends-before