java中线程安全,volatile,synchronized,锁,线程同步,锁的状态和锁升级,CAS ABA,happens-before

在说线程之前,首先必须要说的一个概念是进程,任何线程不能独立存在,进程只操作系统结构的基础,是代码在数据集上的一次运行活动,是系统进行资源分配和调度的一个独立单位,线程可以理解为是进程的子任务,是进程的一个执行路径,进程中的多个线程共享进程的资源。我们知道CPU执行是给每个线程分配CPU时间片来执行线程业务,时间片是CPU份额配给各个线程的时间,非常短,CPU通过不停切换线程执行,充分利用CPU和发挥CPU多核。这里就存在一个问题,CPU在线程之间来回切换执行,切换到之前执行的线程,如何继续执行下去,所以每次切换之前会给当前线程保存一个状态,一般说线程上下文,当线程再次获得CPU时间片执行的时候加载线程上下文继续之前的执行逻辑,线程上下文的切换会影响多线程的执行速度
在Java中一般要想实现自定义的线程有两种方式,一种是继承Thread类,另一种是实现 Runnable接口。
在java中,与线程相关的一个重要话题就是线程安全问题,在说线程安全问题之前,我们先需要了解一下java中的内存模型(Java Mermory Model,JMM)
JMM
在java中线程运行的时候,当需要读取某个变量的时候,由JMM将变量读取到本地线程副本,当对变量修改完后,由JMM负责写回到主内存(不定时,无法确定详细时间)。现代处理器一般都是将数据写入到临时写缓冲区,写缓冲区保证指令流水线持续运行,避免CPU等待向内存写入数据的延迟,以批处理方式刷新写缓冲区、合并写缓冲区中对同一地址的多次写,减少对内存总线的占用。
需要理解的是,CPU并不直接和主内存打交道,而是速度更快的L1,L2,L3 缓存行,当CPU需要对内存中的数据进行运算的时候,先在缓存行查找是否有这个数据,如果没有在从主内存中将数据加载到缓存行中进行运算
由于各个平台,系统以及硬件的不同,硬件级别和系统级别提供的内存操作语义可能不尽相同,为了对开发者能够有一个统一的内存操作模型,JMM为开发者提供了跨平台、跨硬件、跨系统的统一内存操作模型,提供一致的内存可见性。

何谓线程安全,当多个线程同时读写一个共享资源且没有任何同步措施的时候,导致出现脏数据或其他不预见的结果的问题。
指令重排:实际程序运行的时候,为了提高性能,编译器和处理器常常会对指令做重排序,在不影响结果的前提下,提升并行度和程序效率,主要有三种情况:
(1)编译器优化的重排序,在单线程内不改变程序语义的前提的时,指令重排
(2)指令级并行的重排序。在多核CPU下,采用指令级并行技术,将多条指令并行处理,如果不存在数据依赖,处理器可以改变语句对应机器指令的执行程序
(3)内存系统的重排序。由于缓存和读写缓冲区,使得加载和存储操作看上去可能是在乱序执行。

Java中使用happens-before来描述操作之间的内存可见性,如果一个操作执行的结果需要对另一个操作可见,这两个操作之间必须存在happens-before关系,happens-before指定了两个操作之间的执行顺序,可以在不同线程之间
happens-before描述如下:

  • 如果A操作happens-before B 操作,那么A操作的执行结果必须对B操作可见,且A操作执行顺序在B操作之前

  • 如果A,B两个操作存在happens-before规则,并不意味着Java平台必须严格按照happens-before关系顺序执行,如果重排序之后的执行结果与按照happens-before执行结果易一致,那么这种重排序是合法的。
    一般常见happends-before规则如下:

  • 程序顺序规则,一个线程中的每个操作,happens-before于该线程中任意后续操作

  • 锁规则,对一个锁的解锁,happens-before于随后这个锁的加锁

  • volatile规则,对volatile变量的写happens-before于后续对这个变量的读

  • 传递性,如果 A happens-before B,B happens before C,则 A happens before C

  • start规则,如果ThreadA执行 ThreadB.start,则ThreadA的ThreadB.start操作happens-before线程B中的任意操作

  • join规则,如果线程A执行ThreadB.join,则线程B中任意操作happens-before线程A的ThreadB.join操作

在以下几种情况下,不会重排序:

  • 数据存在依赖情况下,如果两个操作访问同一个变量,且其中有一个为写操作,不会重排序
  • 单线程执行结果不能被改变,使单线程不会被重排序干扰到,能够按照预期结果输出

volatile的语义,由之前JMM模型我们知道当我们要读取一个变量时,首先是从主内存中读取到本地局部缓存,修改变量值后,是由JMM择机写回主内存,并不是立即写回,而volatile变量则是每次读取都是从主内存中读取,写入时立马写回主内存,可以理解为对一个volatile的读能够读取到任意线程对该变量的最后写入,但是需要注意的是对单个volatile变量的读写具有原子性,但是volatile++并不是原子性的操作,实际上是分成了先 读取volatile变量在执行+1在写回,这个过程并不是原子性的操作。
的语义,在锁对应的代码块中,共享变量不是从本地局部变量读取,而是直接从主内存中读取,当锁释放的时候,共享变量直接写回主内存,从这里可以看出,锁的语义与volatile的语义基本上相同。
synchronized语义,java中一般可以用synchronized来实现锁的效果,具体形式为:

  • 普通的同步方法,锁是当前对象
  • 静态同步方法,所示当前Class对象
  • 同步代码块,所示synchronized括号里面的对象

JVM中实现synchronized主要是通过进入和退出Monitor对象,基于monitorentermonitorexit指令来实现,在java对象头中MarkWord会有当前对象锁的相关信息。
java中锁一般有4中状态,无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态

JVM 对象头Mark Word
锁升级过程:
         当程A访问执行代码并请求获取对象锁时,首先会比较当前线程ID与对象头中偏向锁的线程ID是否一致,如果一致,则表明当前线程A已经获取到了锁,继续操作。如果不一致,则判断对象头中线程ID对应线程B是否存活,如果对应线程B不在存活,设置对象头为无锁状态,当前线程A获取偏向锁,并通过CAS设置对象头偏向锁线程ID为当前线程A ID,如果线程B存活,判断线程B是否需要继续使用对象锁,如果不需要,设置对象头为无锁状态,当前线程通过CAS设置对象头偏向锁线程ID,如果CAS设置失败,表示有其他线程获取锁,升级为偏向锁。如果线程B不在需要当前锁,则会将对象头锁设置为无锁状态,线程A在执行获取偏向锁操作。
需要注意的是,偏向锁不会主动释放锁,每次都是等另外线程来获取锁,才会检察之前的线程的偏向锁是否需要释放
当线程A请求获取偏向锁时,线程B已经获取了偏向锁,且线程B存活并在使用锁,这时候jvm暂停线程B,将对象头锁设置为偏向锁,同时在线程A,B栈帧中创建存储锁记录的空间,并将对象头中Mark Word复制到A,B锁记录中,并将对象头中轻量级锁指向线程B栈中锁记录指针,线程A自旋尝试使用CAS将对象头中的Mark Word中锁记录指针指向线程A的栈中锁记录指针,此时线程B已经获取,失败,线程A尝试自旋来获取锁
但是不能一直自旋,自旋是在消耗CPU的,当自旋到一定程度,仍未获取到锁的时候,这时候就会升级为重量级锁,这时候线程B已经获取了轻量级锁,jvm会将对象头中锁标志改为重量级锁,同时会阻塞当前在请求锁但是没有获取到锁的线程。这个时候尝试获取锁的线程都会被阻塞,当持有锁的线程释放锁之后会唤醒这些线程,开启新的锁的竞争。
正如开始提到的,线程上下文切换会消耗资源影响速度,重量级锁锁被阻塞和获取锁之后实际上发生了线程上下文切换,但是如果轻量级锁一直自旋,CPU会一直自旋浪费。

悲观锁,乐观锁,公平锁非公平锁,独占锁,共享锁,可重入锁,自旋锁

在同步或者锁的过程中,基于阻塞方式会产生线程上线文切换的开销,随着现代计算机的发展和硬件的进步,在硬件和系统层面增加了CAS(Compare And Swap)支持,解决读-改-写等原子性问题,提供原子性操作。
CAS操作比较著名的是ABA问题:线程1首先获取变量X的值(A),这时候线程2通过CAS修改X的值为B,然后又使用CAS修改X的值为A,这时候线程1使用CAS修改X的值(A)为M,这时候的A与刚开始获取的值A不是同一个概念了。JDK中AtomicStampedReference给没给变量的状态值都增加了一个时间戳,这样每次操作除了值还会比较时间戳变量是否一致

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章