并发与高并发

并发与高并发的关注点

  • 并发:多个线程操作相同的资源,保证程序线程安全,合理使用资源
  • 高并发:服务能同时处理很多请求,提高程序性能

基础知识

CPU多级缓存

  • 为什么需要CPU缓存:
  • 缓存存在的意义:时间一致性,空间一致性
  • 缓存一致性(MESI):用于保证多个CPU缓存之间缓存共享数据的一致性(没听懂)
  • 乱序执行优化:处理器为提高运算速度而做出违背代码原有顺序的优化

J.U.C 之 AQS 组件

CountDownLatch

计数器,能阻塞(await)某些线程直到 CountDownLatch 的值变为 0,其他线程负责将 CountDownLatch 减 1(countDown)。

计数器只能使用一次。

Semaphore

信号量,能控制一定数量线程的并发执行。

在执行业务代码前,调用acquire()获取一个许可,执行完之后,调用release()释放一个许可。

还允许尝试获取许可,尝试获取多个许可以及它们的超时版。

CyclicBarrier

允许多个线程互相等待,到达全部准备好的状态。就像赛跑时所有人都要在起跑线上准备好,然后再一起开跑。

可以重置。

synchronized

可重入锁,一个线程进去过一次后还可以再进去一次。

其实 synchronized 也是可重入锁。

自从 synchronized 引入了偏向锁,自旋锁之后,性能与 synchronized 差不多了。

StampedLock

StampedLock 是 Java8 引入的一种新的所机制,简单的理解,可以认为它是读写锁的一个改进版本。
读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,
它使用的依然是悲观的锁策略。如果有大量的读线程,他也有可能引起写线程的饥饿。
而StampedLock 则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。

Fork/Join 框架

并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。并行流的底层其实就是ForkJoin框架的一个实现。

Fork/Join框架:在必要的情况下,将一个大任务,进行拆分(fork) 成若干个子任务(拆到不能再拆,这里就是指我们制定的拆分的临界值),再将一个个小任务的结果进行join汇总。

Fork/Join采用“工作窃取模式”,当执行新的任务时他可以将其拆分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随即线程中偷一个并把它加入自己的队列中。

就比如两个CPU上有不同的任务,这时候A已经执行完,B还有任务等待执行,这时候A就会将B队尾的任务偷过来,加入自己的队列中,对于传统的线程,ForkJoin更有效的利用的CPU资源!

BlockingQueue

BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。一个线程往里边放,另外一个线程从里边取。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。
如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。
负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,
这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。

  • 直接提交队列:SynchronousQueue,没有容量,所以提交的任务不能保存,总是将任务交给空闲线程,如果没有空闲线程,就创建线程,一旦达到maximumPoolSize就执行拒绝策略
  • 有界任务队列:ArrayBlockingQueue,当线程池的数量小于corePoolSize时,当有新的任务时,创建线程,达到corePoolSize后,则将任务存到ArrayBlockingQueue中,直到有界队列容量已满时,才可能会将线程数提升到corePoolSize之上。
  • 无界队列:LinkedBlockingQueue,除非系统资源耗尽,否则不存在任务队列入队失败的情况,因此当线程数达到corePoolSize之后,就不会增加,有新的任务到来时,都会放到无界队列中。
  • 优先任务队列:PriorityBlockingQueue是带有优先级的队列,特殊的无界队列,理论上来说不是先入先出的,是根据任务的优先级来确定执行顺序
  • DelayQueue:执行定时任务,将任务按延迟时间长短放入队列中,延迟时间最短的最先被执行,存放在队列头部的是延迟期满后保存时间最长的任务
  • LinkedTransferQueue:其实和SynchronousQueue类似,当生产者生产出产品后,当先去找是否有消费者,如果有消费者在等待资源,则直接调用transfer()方法将资源给消费者消费,而不会放入队列中。如果没有消费者等待,则当生产者调用transfer()方法时会阻塞,而调用其他的方法,如aput()则不会阻塞,会把资源放到队列中,因为put()方法只有在队列满的时候才会阻塞。适用于游戏服务器中,可以是并发时消息传递的效率更高

多线程并发最佳实践

  • 使用本地变量
  • 使用不可变类
  • 最小化锁的作用域范围:S=1/(1-a + a/n)
  • 使用线程池的 Executor,而不是直接 new Thread 执行
  • 宁可使用同步也不要使用线程 wait 和 notify
  • 使用 BlockingQueue 实现生产消费模式
  • 使用并发集合而不是加了锁的同步集合
  • 使用 Semaphore 创建有界的访问
  • 宁可使用同步代码块,也不使用同步的方法
  • 避免使用静态变量

高并发处理的思路及手段

  • 扩容:水平扩容、垂直扩容
  • 缓存:Redis、Memcache、Guava Cache等的介绍与使用
  • 队列:kafka、RabbitMQ、RocketMQ等
  • 应用拆分:服务化Dubbo与微服务Spring Cloud
  • 限流:Guava RateLimiter,常用限流算法
  • 服务降级与服务熔断:Hystrix
  • 数据库切库、分库、分表
  • 高可用:任务调度分布式elastic-job、主备curator的实现、监控报警机制

缓存

浏览器 -> 网络转发 -> 服务 -> 数据库

其实缓存可以出现在上述的各个环节中。

特征

  • 命中率:命中数 /(命中数 + 未命中数)
  • 最大元素(空间)
  • 清空策略:FIFO、LFU、LRU、过期时间、随机等

缓存命中率影响因素

  • 业务场景和业务需求
  • 缓存的设计(粒度和策略)
  • 缓存容量和基础设施

缓存分类和应用场景

  • 本地缓存:编程实现(成员变量、局部变量、静态变量)、Guava Cache
  • 分布式缓存:MemCache、Redis
Guava Cache

灵感来源于ConcurrentHashMap

MemCache

客户端:采用一致性哈希算法,把某个key的操作映射到固定机器上。

Redis

远程内存数据库,支持数据持久化。