thanq的日志

一篇文章看明白java并发编程

多线程环境会遇到什么问题

  • 内存可见性问题: 由于cpu缓存机制, 一个线程修改了变量, 另一个线程可能看不到该变量的修改
  • 原子性问题:
    • 看起来是单个的操作, 实际执行时可能是多步操作(cpu实际执行了多条指令), 其它线程可能会访问到操作了一半的数据
    • 有些代码块会被要求作为一个整体执行, 执行过程中不能发生线程切换, 除非当前线程执行完, 别的线程不能执行该代码块的内容
  • 有序性问题: 为了让程序有更好的性能,编译器和处理器会对指令做重排序来优化执行, 虽然单线程执行时重排序后的执行结果会保证和原程序一致, 但在多线程协作场景中, 如果协作细节对代码执行顺序有要求, 就会产生问题

现代cpu为解决这些问题提供了哪些机制

  • cas(compare and swap)操作指令:
    • 指令形式为: cas(内存地址, 比较值, 新值); 当内存中的当前值等于比较值时, 将新值赋给该内存地址, 如果不一致, 返回失败
    • cup针对该操作有专门指令, 由cpu来保证原子性
    • 优点: cas能检测到来自其他线程的干扰, 可以不使用锁来实现原子的读-改-写操作序列, 且通常cas操作会比加锁操作效率高很多, 可以用来实现一些不使用锁的高性能非阻塞组件
    • 缺点:
      • 需要处理者自己来处理竞争失败问题(重试or回退or放弃), 而锁会自动处理竞争问题(线程在拿到锁前会一直阻塞)
      • 在高度竞争场景, cas性能会比锁差, 但几乎没有业务场景是除了竞争操作什么都不做的
  • 内存屏障指令:
    • 一些用来对内存操作进行顺序限制的指令, 可以确保屏障指令两边的指令以正确的顺序执行, 并让相关数据对所有处理器可见(解决有序性和可见性)
    • 有哪些内存屏障指令:
      • LoadLoad 屏障: 保证后续的读操作会读到该指令前读操作的结果, 使得读操作有序
      • StoreStore 屏障: 保证该指令前写操作的结果先对所有cpu可见(如把新数据及时刷到内存)再被该指令后的写操作使用
      • LoadStore 屏障: 保证该指令前的读操作在该指令后的写操作前完成
      • StoreLoad 屏障: 保证该指令前的写操作在被之后的读操作读取前对其他处理器可见, 该指令可同时获得其它三种屏障的效果, 但常会使得一个缓存区域强刷到内存, 开销也最大

java为并发编程提供了哪几种机制

volatile关键字:

  • 底层实现: volatile关键字修饰的变量在读写操作前后会加上合适的内存屏障指令, 来让读写操作强制走内存, 从而保证了该变量的全局可见性
  • 优点: 以较轻量的开销提供了全局可见性
  • 缺点: 无法构建复合操作
  • 使用场景: 状态标识, 计数器 (jdk封装的原子变量是更好的volatile, 如非必要, 优先使用原子变量)
  • 典型使用模式:
    • 先读取值volatile变量a, 然后根据a计算b, 再然后通过cas以原子的方式将volatile变量a的值改成b

内部锁(synchronized):

  • 底层实现: 每个对象都有一个锁, 也就是监视器(monitor), 如果monitor=0, 表明该对象没有被锁定, monitor=1, 表明该对象正在被一个线程锁定, monitor>1, 表明该对象正在被一个线程多次锁定; 通过锁可以界定一个临界区, 该临界区同时只允许一个线程进入, 其中的代码同时最多只会有1个线程来执行, 临界区内容执行时会加入内存屏障, 以保证临界区内操作的可见性
  • 线程是持有锁的单位
  • 优点: 使用相对简单
  • 缺点: 可能会产生死锁, 并且出现一旦死锁就只能重启了
  • 防止死锁的方法:
    • 避免有一步操作hold多个锁的情况
    • 如果非要在一步操作内hold多个锁, 需要保证操作hold锁的顺序一致以确保不会出现相互等待对方解锁的状况

Lock:

  • 底层实现: cas + volatile + 线程队列 + 线程中断机制, 可以和内部锁一样, 让一个临界区最多只有1个线程来执行
  • ReentrantLock的好处: 提供了更灵活使用锁操作的机制
  • ReentrantLock的问题: 如果忘记在finally块中调用unlock就会导致灾难, 用起来会比内部锁复杂
  • 什么时候使用ReentrantLock: 需要使用可定时/轮询/可中断的方式来获取锁操作的场景, 以及需要使用公平锁的场景
  • 读操作更多的场景可以考虑使用ReadWriteLock (比起ReentrantLock, ReadWriteLock可以允许一个资源被多个读操作访问或一个写操作访问, 但两者不能同时进行, 在读操作更多时, 会有更好的性能)

final关键字:

  • 底层实现: java内存模型会在final修饰的变量赋值后加入StoreStore内存屏障来保证可见性, 同时由于final关键字修饰的变量不会被再次赋值, 所以后续的程序使用该变量时也就不会有线程安全问题; 使用final关键字还可以帮助jvm更好的优化性能

threadlocal机制:

  • threadlocal机制可以将变量绑定在线程自身携带的map里, 通过threadlocal的api可以使得该变量只能被同一线程操作, 这样就避免了线程安全问题

最佳实践:

  • 不要同时使用多种同步机制, 同时使用多种同步机制会带来混乱, 但不会在性能和安全性上带来好处
  • 不要在无法快速完成的操作上加锁
  • 尽量使用同步块, 而不是同步方法(spring容器中加了事务注解的同步方法还是同步方法么?)
  • 除非某个域是可变的, 否则将其声明为final
  • 优先使用jdk封装的并发工具, 并确保正确使用它们
  • 复杂交互场景下用BlockingQueue解耦程序模块
  • 需要高性能的场景可以尝试使用非阻塞数据结构(如Disruptor等)和原子变量
  • 需要高伸缩性的场景可以尝试提高可并行组件在所有组件中的比重
thanq wechat
扫一扫订阅我的微信公众号