JAVA并发编程中的安全性、活跃性和性能问题

松花皮蛋me 2019-03-17 11:40
文章首发于公众号 松花皮蛋的黑板报松花皮蛋的黑板报,作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解

保证线程安全本质上是保证线程同步,实际上就是线程间通信问题,线程通信常见方式有信号量、管道、共享内存、消息队列、socket,java线程通信主要使用共享内存,那么首先需要关注的是可见性、有序性、原子性问题,然后就是活跃性问题和性能问题。

一、原子性、可见性和有序性

1、基本概念

  • 可见性:jvm对象变量的修改存在从Heap加载到Heap更新的过程,一个线程对共享变量的修改,另外一个线程能够立即看到,也就是保证CPU缓存和内存一致
  • 原子性:线程切换时,保证一个或多个操作在cpu执行的过程中不被中断的特性
  • 有序性: jvm指令重排和多线程代码交替串行执行中,保证程序执行的顺序按照代码的先后顺序执行,否则可能会导致线程获取到一个未初始化的对象

2、方法:

  • 保证可见性:按需禁用缓存,包括volatile、synchronized、final
  • 保证原子性:互斥锁,比如synchronized修饰的临界区是互斥的
  • 保证有序性:按需禁用编译优化,比如synchronized等,编译器通过在操作前后各插入一些内存屏障禁止重排序优化
特性volatile关键字synchronized关键字Lock接口Atomic变量
原子性无法保障可以保障可以保障可以保障
可见性可以保障可以保障可以保障可以保障
有序性一定程度保障可以保障可以保障无法保障

3、Happens-Before规则

前面一个操作的结果对后续操作是可见的。

  • 程序的顺序性规则
  • volatile变量规则:对一个volatile变量的写操作先行于后面对这个变量的读操作
  • 传递性
  • 管种中锁的规则:一个unlock操作先行于后面对同一个锁的lock操作
  • 线程start规则:主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B前的操作
  • 线程join规则:主线程A等待子线程B执行完成,当子线程B完成后,主线程A能看到子线程B的操作
  • 线程中断规则:对线程的中断操作先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成先行于它的finalize方法的执行

二、活跃性问题,包括死锁、活锁和饥饿性

1、死锁

争夺资源而造成互相等待的情况。可以通过资源一次性分配、可剥夺资源、资源有序分配法进行预防,银行家算法进行避免,在分配资源前先看清楚,分配后不会造成死锁就分配,否则不分配

2、活锁

线程虽然没有阻塞但是仍然会存在执行不下去的情况,比如相互谦让,处理办法是相撞后各自等待一个随机值。

3、饥饿性

线程无法访问到所需资源而无法继续执行下去,可以通过保证资源充足或者公平地分配资源(先来后到的公平锁),防止持有锁的线程长时间执行。

三、性能问题

锁的过度使用可能导致串行化的范围变大,造成吞吐量、延迟、并发量变差,可以使用下面的两种方案解决

1、无锁,比如写入时复制、线程本地存储等

Copy-on-write最常见的就是iterator的修改,使用了CopyOnWriteArrayList,在操作新元素时不直接操作原容器,而是先复制一个快照,对这个快照进行操作,在操作结束后再将原容器的引用指向新引用

2、减少锁持有时间,增加并行度,比如concurrentHashMap的分段锁,读写锁ReentrantReadWriteLock

concurrentHashMap将内部分成多个segment数组,segment通过继承ReentrantLock加锁。segment里面则是hashEntry数组,hash值相同的条目以链表的形式存放,当数目达到一定时采用红黑树存放。另外hashEntry内部使用volatile的value字段来保证可见性


阅读 146 次