并发学习(六) — 线程安全的三个方面

一、线程安全性:

         当多个线程访问某个类,不管运行环境采用何种调度方式或者这些进程将如何调用,并且在主调代码中不需要额外的同步或协同,都能表现出正确的行为,这就是线程安全的。

主要体现在三个方面:

  • 原子性:互斥访问,同一个时刻只能有一个线程来对它进行操作,  如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(规则)
  1. 线程解锁前,必须把共享变量的最新值刷新到主内存
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁,解锁是同一把锁)

可见性 — volatile

通过内存屏障和禁止重排序来优化实现

  1. 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
  2. 对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


猜你喜欢

转载自blog.csdn.net/jae_wang/article/details/80344486