并发的三种场景
分工
分工是多线程并发最基本的场景,各司其职,完成各自的工作。分工,就是线程各司其职,完成不同的工作。分工,也是有很多模式的。比如有:
- 生产者-消费者模式;
- MapReduce模式,把工作拆分成多份,多个线程共同完成后,再组合结果,Java8中的stream与Fork/Join就是这种模式的体现;
- Thread-Per-Message模式,服务端就是这种模式,收到消息给不同的Thread进行处理
同步
有分工就要有同步,不同工人之间要协作,不同线程也是。一个线程的执行条件往往依赖于另一线程的执行结果。
线程之间最基本的通信机制是管程模式与wait/notify,除此外还有多个工具类,如:
- Future及其衍生的工具类FutureTask/CompletableFuture等,可以完成异步编程;
- CountDownLatch/CyclicBarrier可以实现特定场景的协作;
- Semaphore提供了经典的PV同步原语,还可以作为限流器使用;
- ReentrantLock与Condtion,对管程同步的扩展;
互斥
多线程访问相同的共享变量,就需要做互斥处理。分工与协作强调的是性能,互斥问题强调的是正确,即线程安全问题。Java解决互斥问题提供了很多思路与工具。
- 避免共享,没有共享,没有竞态,就没有伤害,如ThreadLocal;
- 没有改变,如果大家都不做改变,都是只读的,一起也没有错;
- Copy-on-write,你变你的,我变我的,每变一次都生成新的副本,只要不冲突就可以并行;
- CAS,写入前要看一看,有没有物逝人非(变量和自己读取时一样),没有再写入,否则再做一变;
- Lock,最终手段,但也不想做得太绝,够用就行,ReadWriteLock/StampedLock,够用就行
并发问题产生的原因
缓存导致的可见性问题
在运行时,同一份数据就出现了两份,一个在内存,一个在CPU缓存。每个CPU中有各自的数据缓存(JMM内存模型)。
线程切换带来的原子性问题
计算机看起来可以同时运行多于自身核数的线程,是因为现代操作系统的分时切换机制。分时机制提高了CPU的使用率,也可以保证多线程可以相对公平地获取CPU。但分时机制导致了一个不可避免的问题,就是线程切换。发生线程切换时,被休眠的线程会暂存现场,包括PC(程序计数器)与栈等。等到此线程再次被唤醒,可能发现这个世界已经物是人非了,因为一条高级语言指令可能对应多条CPU指令。
编译优化带来的有序性问题
JAVA为了优化性能,可能对指令进行重排,这些重排在大部分时候是无害的。但是有些时候,可能导致意想不到的Bug。由重排引起的一个经典问题是双重量检查创建单例。
并发的三种问题
安全性问题
并发程序因为可见性、原子性及有序性问题等导致的正确性问题
活跃性问题
指的是某个操作无法执行下去,如死锁等导致的问题
性能问题
一般都是由锁的滥用引起的。
性能方面有三个主要的指标:吞吐、时延及并发量。
- 吞吐,指单位时间处理的请求数;
- 时延,指单次处理的平均耗时;
- 并发,同一时刻可以接入的请求数