第一章 高并发概述
- 高并发的基本背景
对于互联网应用而言,需要能够支撑海量用户同时在线,以及高效、快速地处理用户高并发请求流量,保证应用系统在高并发场景中依然保持高性能和高可用。如果一个系统既存在高并发场景,又具备高并发的处理能力,则该系统属于高并发系统。
三高:高性能、高可用、高并发
高并发编程就是使用诸如多线程设计、缓存加速、异步处理、分布式系统架构、集群部署等技术来实现在高并发场景中,依然可以正确、快速地处理每一个用户请求。
二并:并发与并行 - 高并发的衡量指标
- 响应时间:从请求发起到获得处理结果的时间
- 吞吐量与QPS:吞吐量指的是单位时间内完成请求的数量,QPS类似(只是单位时间具体到秒,描述系统的最大吞吐量)
- TPS:与QPS仅一字之差,指的是应用每秒完成的事务数(QPS通常在接口层面衡量某个接口每秒完成的请求数,TPS在系统层面衡量该系统每秒能够完成的事务数量)
- 并发用户数
- 应对策略
- 单机高并发:充分利用服务器的CPU、内存等硬件资源来加快每个请求的处理速度,实现技术诸如基于多线程设计来实现对CPU多个核心的利用,基于内存来进行缓存设计、减少对数据库的访问,加快数据存取操作的速度,数据压缩等。
- 分布式高并发:基于分布式系统架构和集群部署来实现对应用系统的横向拓展。基于负载均衡机制来分发并发请求流量到多台机器。
第二章 操作系统多线程基础
这一章简要回顾草做系统多线程部分。
现代操作系统都是多任务操作系统,高并发系统设计的一个核心点就是如何利用多线程来实现对请求的并发处理。
为了保证任务执行的连贯性,需要对任务的执行状态进行保存,以便后续能够接着之前的执行状态继续执行,这个状态就称之为上下文。
操作系统的多线程模型:多对一、一对一、多对多
需要结合机器的性能高低和压力测试来决定最佳的线程数
多线程的挑战:
- 数据一致性。解决通常用锁(互斥锁、共享锁)或者CAS(在一个原子操作中先完成对旧数据的检查,如果没有被其他线程修改过,则进行更新操作;否则自旋)
- 死锁
- 线程上下文切换和内存开销
第三章 Java多线程基础
Java高并发的基础就是多线程程序设计基础,因此有必要回顾一下Java多线程编程。
多线程类:
- Thread:抽象类,需要实现run方法,调用start执行线程
- Runnable:接口,需要实现run方法,作为Thread参数执行
- 获取线程名字:Thread.currentThread().getName()
- 线程等待:在主线程中可以创建多个子线程来实现多个任务的同时执行,此时一般需要在主线程中等待这些子线程的执行结果,从而进一步汇总。在主线程中等待多个子线程执行完成的做法是:在主线程中通过子线程实例调用其join方法,使得主线程阻塞等待,直到子线程执行完成后才返回。
- 线程暂停sleep和yield:线程在调用sleep休眠之后是可以被其他线程中断的(被叫起床),故需要通过try/catch捕获中断异常InterruptedException,其中sleep是Thread类的一个静态方法。yield也是让线程休眠,但两者的不同是(划重点!!):yield让出线程CPU资源,然后让同等优先级的线程去竞争获取CPU资源来执行。注意这里去竞争CPU资源的线程有可能就还包括了刚刚调用yield方法的线程本身,因此该线程有可能又获得CPU资源并继续执行。
- 线程优先级
- 线程协作:wait/notify
线程安全
一个big topic
synchronized关键字与互斥锁:
这种做法与加互斥锁Mutex来同步多个线程对共享资源的访问,保证任何时候只有一个线程可以访问共享资源,这个关键字的作用可以归纳如下:
- 确保线程互斥访问同步代码
- 保证共享变量的线程可见性
- 禁止指令重排
synchronized关键字在实现层面是结合一个监视器对象monitor来实现的,所以在使用的使用需要将某个对象作为监视器对象monitor。(1) 类的静态方法:将类对象本身作为监视器monitor;(2) 类的成员方法:使用类的对象实例作为监视器对象monitor;(3) 代码块:使用某个对象作为监视器monitor
synchronized的实现原理:
在JV层面,synchronized是基于JVM提供的monitorenter和monitorexit字节码指令、以及监视器对象monitor实现的。
- monitorenter:进入同步块,所有线程共享该同步代码和该对象关联的监视器monitor,当每个线程执行到monitorenter指令的时候,会检查对应的monitor对象的计数是否为0.如果是0,则当前线程成为该monitor对象的拥有者,递增计数为1.之后该线程每调用一次使用了该monitor对象作为监视器的同步方法时,monitor对象计增加1,这是synchronized可重入的实现。而对于其他线程来说,检测到计数器不为0,则会阻塞,下次再竞争monitor
- monitorexit:计数器减1
每个监视器对象monitor都会关联一个等待队列和同步队列。等待队列用于存放调用了wait方法的线程,同步队列存放在等待队列中被唤醒的线程,由同步队列的线程去竞争该锁。
volatile关键字与线程可见性:
Java内存模型决定了:每个线程都有自己的工作内存,并且该线程的所有操作都是在这个工作内存中完成的,即每个线程都将主内存中的共享数据复制到自身的工作内存来进行操作,所以不同线程之间的操作是相互不可见的。在多线程环境下,需要保证一个线程对共享数据进行操作后对其他线程可见,保证其他线程可以读到这个共享数据的最新值,实现数据一致性。当某个线程在自身工作内存中修改了这个使用volatile关键字修饰的变量时,需要将这个变量的最新值同步回主内存,同时其他线程的本地内存中的该变量的副本会自动失效。
volatile关键字并不提供原子性和线程安全性。
禁止指令重排:多线程代码中可能存在由于指令重排引发的错误,用volatile关键字修饰时,Java编译器在编译这段Java代码或CPU在执行这段代码时,不会对该变量的位置与其相邻的其他变量进行调整。(实现机理:在修饰的关键字周围加上内存屏障指令)
不可变final关键字和无状态
final就不必多讲了
讲讲无状态吧
当某个对象被多个线程共享,且有多个线程同时调用该对象的某个方法时,如果该方法只包含 (1) 方法调用时传递进来的参数;(2) 方法内部定义的局部变量,而不会使用到该对象的成员变量,则该方法是无状态方法,是线程安全的。
ThreadLocal线程本地变量包装器
ThreadLocal是基于空间换时间的思路来设计,即通过使用ThreadLocal对共享变量进行包装,使得每个线程都包含这个共享变量的一个副本,从而达到线程安全。(这里包装的共享变量是不需要和其他线程合作共享的,本身就是要实现数据对其他线程不可见)
简要实现机理:
每个Thread线程对象都包含ThreadLocalMap这样一个字典集合,所以实现了每个线程都包含共享变量的一个副本。
- ThreadLocal值的初始化:重写initialValue方法
- get 获取线程绑定的值:每个线程都是获取到该线程绑定的值,即从该Thread线程所关联的线程本地变量集合threadLocals中获取。
- set 设置线程绑定的值