java并发编程的任务

并发编程的挑战

上下文切换

  • 任务从保存到再加载的过程就是一次上下文切换
  • 单核处理器通过给每个线程分配CPU时间片来实现多线程
  • 减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。 (协程不是被操作系统内核所管理,而完全是由程序所控制;一个线程也可以拥有多个协程)

死锁

避免死锁

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

资源限制的挑战

概念

资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。

硬件资源限制

带宽的上传/下载速度、硬盘读写速度和CPU的处理速度

软件资源限制

数据库的连接数和socket连接数等

资源限制引发的问题

  • 在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行
  • 并行会增加上下文切换和资源调度的时间

解决资源限制问题

对于硬件资源限制

采用集群并行执行程序

对于软件资源限制

可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接

Java并发编程的底层实现原理

volatile的应用

  • 如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存

  • 在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议 。每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是否过期,如果过期,将当前缓存行设置成无效状态。后续修改该数据时,重新从系统中把数据读到缓存中。

volatile两条实现原则

  • Lock前缀指令会引起处理器缓存回写到内存

在多处理器环境中,LOCK#信 确保在声言该信 期间,处理器可以独占任何共享内存 ;LOCK#信 一般不锁总线,而是锁缓存

  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致 ;

volatile使用优化

  • 追加字节性能优化(队列集合类LinkedTransferQueue )

追加64字节能够提高并发编程的效率 :因为对于英特尔酷睿i7、酷睿、Atom和NetBurst,以及Core Solo和Pentium M处理器的L1、L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行,这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中 。而填充到64字节,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定。

synchronized的实现原理与应用

Java中锁的三种形式

  1. 对于普通同步方法,锁是当前实例对象。
  2. 对于静态同步方法,锁是当前类的Class对象。
  3. 对于同步方法块,锁是Synchonized括 里配置的对象

JVM实现Synchonized原理

  • JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样

  • 代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的

  • monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

java对象头

  • synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头
  • Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位
  • 锁标志位:00 – 轻量级锁;10 – 重量级锁;11 – GC 标记; 101 – 偏向锁; 001 – 无锁

锁升级与对比

  • 锁的四种状态

级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态 ,锁可以升级但不能降级 。

偏向锁

  • 为了让线程获得锁的代价更低而引入了偏向锁。

  • 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

  • 偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程

  • 关闭偏向锁

    偏向锁在应用程序启动后几秒钟后激活,jvm关闭延迟:-XX:BiasedLockingStartupDelay=0

    JVM关闭偏向锁:-XX:-UseBiasedLocking=false

轻量级锁

  • 轻量级锁加锁

线程在执行同步块之前,jvm会在当前线程的栈帧中创建存储锁记录的空间,并将对象头中的mark word 复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

  • 轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

锁的优点比较

指令并行重排序和内存系统重排序属于处理器重排序,可能会导致多线程程序出现内存可见性问题

  • 处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序

  • 内存屏障类型

  • as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义

  • 程序顺序规则

  • volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升

  • 由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势

  • 锁的内存语义

    • 锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义
    • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息
    • 在常见的intel X86处理器中,CAS是如何同时具有volatile读和volatile写的内存语义的
      • 如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg)
      • 如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)
    • 公平锁和非公平锁的内存语义总结
      • 公平锁和非公平锁释放时,最后都要写一个volatile变量state
      • 公平锁获取时,首先会去读volatile变量
      • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义
    • Java线程之间的通信现在有了下面4种方式
      • A线程写volatile变量,随后B线程读这个volatile变量
      • A线程写volatile变量,随后B线程用CAS更新这个volatile变量
      • A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
      • A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量
    • concurrent包通用化的实现模式
      • 首先,声明共享变量为volatile
      • 然后,使用CAS的原子条件更新来实现线程之间的同步
      • 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信

    final域的内存语义

    • 于final域,编译器和处理器要遵守两个重排序

      • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 (写final域的重排序规则禁止把final域的写重排序到构造函数之外 )

        • JMM禁止编译器把final域的写重排序到构造函数之外
        • 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外
      • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

        • 读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障
        • 初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系
          java并发编程的任务

    writer()函数的执行包含两步:1)构造一个FinalExample类型的对象 ;2)把这个对象的引用赋值给引用变量obj。规则1可以保证先执行第一步,然后执行第二步

    读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经被A线程初始化过了

    • final域为引用类型
      • 在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

    双重检查锁

    单例案例

    • 单线程案例
    • 多线程中标准答案

      • 加粗锁(性能开销大)
      • 双重检查锁定(错误优化

      问题原因:instance = new Instance(); 代码可以分解为下面三行

      当线程A调用getInstance()方法时,到达步骤4,条件成立,进入并获取锁,但是当执行步骤7的时候,发生了指令重排序:

      并且 当A线程执行到 instance = memory; 没有执行ctorInstance(memory);时,线程B也访问getInstance() 方法,这时候线程B获取的instance 是未成功 初始化的。

      • 基于volatile的双重检查锁定
    • 基于类初始化的解决方案

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2021年4月27日
    下一篇 2021年4月27日

    相关推荐