java中的并发_AQS, CAS, synchronized, 锁

·  阅读 246

概述

  • 隐式锁: synchronized
  • 现式锁: ReentrantLock, 相关接口Lock, ReadWriteLock, 均基于AQS(AbstractQueuedSynchronizer)实现

使用

  • synchronized
    • synchronized作用于普通方法是,锁对象是this, 其他线程不能访问该对象的所有synchronized方法. 但是可以访问非synchronized方法(前提是不同的对象)
    • synchronized作用于静态方法是,锁对象是当前类的Class对象, 其他线程无法访问该class对象上的static synchronized方法. 但可以访问对象的非static [synchronized可选]方法
    • synchronized作用于代码块时,锁对象是synchronized(obj)中的这个obj
    • synchronized 锁住的是对象而非代码
    • 非公平锁
    • 支持重入
    • 通常情况建议使用
  • ReentrantLock
    • 可重入锁: 同一个线程可以同时多次请求同一把锁
    • 可超时等待: tryLock(long timeout, TimeUnit unit)
    • 可中断: lockInterruptibly(). 在等待锁或重新唤醒进入方法前如果线程被中断, 优先处理中断. 并抛出异常
    • 条件队列(condition queue): 获取锁之后,有时候还需要等待某个条件满足才能做事。这些条件被称作条件谓词,线程需要先获取锁,然后判断条件谓词是否满足,如果不满足就不往下执行,相应的线程就会放弃执行权并自动释放锁. 支持多个条件队列
    • 需要显式的申请和释放,并且释放一定要放到finally块中,否则可能会因为异常导致锁永远无法释放
    • 支持公平锁与非公平锁
    • 支持重入
    • 支持独享与共享

synchronized

  • 代码块: 使用monitorenter和monitorexit指令实现的, 底层的操作系统的Mutex Lock来实现的, Mutex Lock将当前线程挂起并从用户态切换到内核态来执行,这种切换代价昂贵
  • 修饰方法: 依靠的是方法修饰符上的ACC_SYNCHRONIZED实现, Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象
  • 原理: 基于 Java 对象头和 Monitor 机制来实现的
    • 对象头: 哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等. 根据编译环境可能是4个字节或8个字节
    • monitor: 每个对象都拥有自己的监视锁Monitor, 由c++实现
      • owner:指向持有ObjectMonitor对象的线程
      • WaitSet:存放处于wait状态的线程队列
      • EntryList:存放处于等待锁block状态的线程队列
      • recursions:锁的重入次数
      • count:用来记录该线程获取锁的次数
    • 多个线程竞争锁时, 会先进入EntryList队列. 竞争成功的线程被标记为Owner, count+1. 其他线程继续在此队列中阻塞等待
    • 如果Owner线程调用wait()方法, 则其释放对象锁并进入WaitSet中等待被唤醒。Owner被置空, count-1. EntryList中的线程再次竞争锁
    • 如果Owner线程执行完了, 便会释放锁. Owner被置空, EntryList中的线程再次竞争锁
  • 优化
    • 自旋自适应(Adaptive Spinning): 让等待线程执行一定次数的循环, 在循环中去获取锁. 这项技术称为自旋锁. 不通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定称为自适应
    • 锁粗化(Lock Coarsening): 当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部
    • 锁消除(Lock Elimination): 虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除
    • 偏向锁(Biased Locking): 同步期间大概率不会有其他线程竞争锁. 当线程请求到锁对象后, 将锁对象的状态标志位改为01即偏向模式。然后使用CAS操作将线程的ID记录在锁对象的MarkWord中. 后续该线程可以直接进入同步块. 但是一旦有第二条线程需要竞争锁. 那么偏向模式立即结束. 进入轻量级锁的状态. 线程是不会主动去释放偏向锁, 需要等待其他线程来竞争. 偏向锁的撤销需要等待全局安全点
    • 轻量级锁(Lightweight Locking): 如果同步对象锁状态为无锁状态, 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间, 然后拷贝对象头中的Mark Word复制到锁Lock Record中(官方称为Displaced Mark Word). 拷贝成功后, 虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针, 并将Lock Record里的owner指针指向对象的Mark Word. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁, 并且对象Mark Word的锁标志位设置为“00”, 表示此对象处于轻量级锁定状态. 如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁. 若当前只有一个等待线程, 则该线程通过自旋进行等待. 但是当自旋超过一定的次数, 或者一个线程在持有锁, 一个在自旋, 又有第三个来访时, 轻量级锁升级为重量级锁
    • 重量级锁(Highweight Locking): 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。通过monitor执行.

ReentrantLock

JMM

  • JVM内存区域: 方法区(method area), 堆(heap), 虚拟机栈(vm stack), 本地方法栈(native method stack), 程序计数器(program counter register)
  • 线程和主内存之间的抽象关系: 线程之间的共享变量存储在主内存中. 每个线程都有一个私有的工作内存, 本地内存存储该线程以读/写共享变量的副本.
    • CPU需要访问主存时,先读取部分主存数据到CPU缓存(如果CPU缓存中存在则直接获取),再读取CPU缓存到寄存器,当CPU写数据到主存时,先刷新寄存器中的数据到CPU缓存,然后再刷新到主内存中
    • Java线程的实现是基于一对一的线程模型,通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务
    • Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉
  • 指令重排序: 编译器和处理器通常会对指令重排序: 编译优化重排序, 指令级并行的重排序(指令级并行技术, 不存在数据依赖), 内存系统重排序
    • 编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题
    • 不同的处理器指令集对运行时重排序实现有差异
  • 顺序一致内存模型: 理想化的参考模型, 为开发者提供极强的内存可见性保证. 在顺序一致性内存模型中, 每个操作都必须原子执行且立刻对所有线程可见
    • 为提升性能, 内训模型需要为开发者提供足够强的内存可见以及对编译器处理器限制要尽量放松
    • 只要不改变程序结果, 编译器处理器可以安既定规则优化. 如果会改变程序结果则禁止重排序
  • JMM是围绕着程序执行的原子性、有序性、可见性展开的
    • 原子性: 原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响
    • 有序性: 有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的. 多线程环境, 对存在控制依赖的操作重排序保持线程之间有序
    • 可见性: 可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
  • JMM是如何确保跨平台具有相同的一致性
    • java编译器生成指令时会在适当的位置插入内存屏障指令来禁止特定类型的处理器重排序
    • happens-before: jsr-133内存模型, 如果一个操作指定的结果需要对另一个操作可见. 那么两个操作之间存在happens-before关系
      • 程序顺序规则: 同一个线程中每个操作, happens-before用于该线程中的任意后续操作
      • 监视器所规则: 对于同一个监视器的解锁, happens-before于随后这个监视器的加锁
      • volatile变量规则: 对一个volatile域的写, happens-before于任意后续对该volatile域的读
      • 线程启动规则: 对一个线程start之前, 启动该线程写happens-before于被启动线程的读.
      • 传递性: a happens-before b, 且b happens-before c. 那么a happens-before c
      • 线程终止规则: 线程的所有操作先于线程的终结, 线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见
      • 线程中断规则: 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断
      • 对象终结规则: 对象的构造函数执行,结束先于finalize()方法
      • happens-before仅要求前一个结果对后续操作可见, 并非对执行顺序的有要求
      • happends-before是JMM呈现给开发者的视图, 底层使用内存屏障技术屏蔽各处理器之间差异. 不需要额外增加同步操作

volatile

  • 特性:
    • 可见: 对一个volatile变量的读, 总能看到任意线程对该变量的最后写入
    • 原子性: 对任意单个volatile变量的读写具有原子性
  • volatile写-读的内存语义
    • 当写一个volatile变量时, JMM将该线程对本地内存中的共享变量刷新到主内存
    • 当读一个volatile变量时, JMM会把该线程对应的本地内存置为无效, 并重新从主内存读取到共享内存
  • volatile内存语义实现
    • 为实现volatile语义. 在读写之前后插入内存屏障(Memory Barrier)

CAS

  • CAS的全称是Compare And Swap 即比较交换, 是一种原子操作机制
    • unsafe.compareAndSwapInt(this, valueOffset, expect, update); 三个参数分别是内存位置 V,旧的预期值 A 和新的值 B
    • 在不阻塞其他线程的情况下避免了并发冲突
    • CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的, 在多核的情况下, 这个指令也不能保证原子性, 需要在前面加上 lock 指令.
  • 总线锁和缓存锁:
    • 总线锁就是锁住这条总线,使其他核心无法访问内存
    • 缓存锁锁定某部分内存区域。当一个CPU核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域
    • lock指令: 老版本的处理器采用总线锁, 新版本的处理器一般采用缓存锁
  • ABA问题: 一个值从A变成了B又变成了A,使用CAS操作不能发现这个值发生变化. 一般情况不需要处理, 发生概率小
    • java的解决方案: AtomicStampedReference(除了记录更新值外还继续更新时间), AtomicStampedReference(记录一个boolean值以及更新值, 减少概率, 无法完全避免ABA)

AQS

  • AbstractQueuedSynchronizer: 队列同步器。它是构建锁或者其他同步组件的基础框架, 它是JUC(java.util.concurrent)并发包中的核心基础组件
  • 核心属性volatile int state
    • state 为0表示没有任何线程持有这个锁,线程持有该锁后将 state 加1,释放时减1。多次持有释放则多次加减
    • compareAndSetState(int expect,int update), set的值也是state变量
  • 等待队列: AbstractQueuedSynchronizer.Node, 获取同步状态失败时,将当前线程构造成一个Node并将其加入到CLH同步队列, 同时会阻塞当前线程, 当同步状态释放时, 会把首节点唤醒

concurrent

  • 通用模式
    • 首先,声明共享变量为volatile
    • 然后,使用CAS的原子条件更新来实现线程之间的同步;
    • 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信
    • 整体包结构 evernotecid://437E9954-1BE5-4C61-A442-EF7A006229B0/appyinxiangcom/2048513/ENResource/p6
分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改