本文内容为读书笔记,强烈推荐《java高并发核心编程-卷2》这本书
书作者博客地址:https://www.cnblogs.com/crazymakercircle/p
查看进程小工具:ProcessExplorer
了解更多JAVA后台知识整理:JAVA后台系列目录
1. 进程/线程
进程:一般由程序段、数据段和进程控制块三部分组成,进程是程序执行的最小单位
其中程序控制块包含:进程的描述信息、进程的调度信息、进程的资源信息、进程上下文
线程:线程描述信息、程序计数器(PC)、和栈内存组成,线程是 CPU 调度的最小单位
线程的基本信息包含了:线程 ID、线程名称、线程优先级、线程状态、其他的线程信息
进程与线程的区别
线程是“进程代码段”的一次的顺序执行流程。一个进程由一个或多个线程组成;一个进程至少有一个线程。
线程是 CPU 调度的最小单位;进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程程序的并发性高。
线程是出于高并发的调度诉求,从进程内部演进而来。线程的出现,既充分发挥 CPU的计算性能,弥补进程调度的过于笨重。
进程之间是相互独立的;但进程内部各个线程之间,并不完全独立。各个线程之间共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等等)。
切换速度不同:线程上下文切换比进程上下文切换要快得多。所以,有的时候,线程也称之为轻量级进程。
2. 并发/并行
并发:指的是CPU切换很快的执行任务
并行:指的是同时执行
3. 创建线程的四种方法
Thread类中的方法详解
创建线程方式1:(1)需要继承 Thread 类,创建一个新的线程类。
(2)同时重写 run()方法,将需要并发执行的业务代码编写在 run 方法中
创建线程方式2: 实现 Runnable 接口创建线程目标类
创建线程方式3: 使用 Callable 和 FutureTask 创建线程
创建线程方式4: 通过线程池创建线程
4. 线程原理理解
4.1 线程如何调度
基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,
等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”
分时调度模型:系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。
抢占式调度模型:系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;
如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。
优先级高线程的只是概率大的先执行并不是绝对执行
4.2 线程的生命周期状态
NEW:新建,创建成功但是没有调用 start()方法启动,
RUNNABLE:可执行:包含操作系统的就绪、运行两种状态
BLOCKED:阻塞
WAITING:等待
TIMED_WAITING:计时等待
Thread.sleep(int n):使得当前线程进入限时等待状态,等待时间为 n 毫秒。
Object.wait():带时限的抢占对象的 monitor 锁。
Thread.join():带时限的线程合并。
LockSupport.parkNanos():让线程等待,时间以纳秒为单位。
LockSupport.parkUntil():让线程等待,时间可以灵活设置。
TERMINATED:终止
5. 线程的基本操作
5.1 设置线程名称要点
1)线程的名称一般在启动线程前设置,但也允许为运行的线程设置名称。
2)允许两个 Thread 对象有相同名称,但是应该避免。
3)如果程序没有为线程指定名称,系统会自动为线程设置名称。
5.2 sleep操作: sleep的作用是让目前正在执行的线程休眠,让CPU去执行其他的任务
5.3 线程的 interrupt 操作:
1) 如果线程被 Object.wait, Thread.join 和 Thread.sleep 三种方法之一阻塞,此时调用该线程的 interrupt()方法,
那么该线程将抛出一个 InterruptedException 中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。
2) 如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为 true。
所以,程序可以在适当的位置,通过调用 isInterrupted()方法来查看自己是否被中断,并做退出操作。
5.4 线程的 join 操作
1)三个重载方法:区别在被合并线程执行 时间。
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
public final synchroinzed void join(long millis, int nanos) throws InterruptedException
调用 join()方法的语句可以理解为合并点,合并的本质:
线程 A 需要在合并点等待,一直等线程 B 执行完成,或者等待超时
2)线程的 WAITING“等待”状态表示线程在等待被唤醒。
处于 WAITING 状态的线程,不会被分配 CPU 时间片。执行以下两个操作,当前线程将处于 WAITING“等待”状态
两种情况
第一种:执行没有时限(timeout)参数的 thread.join()调用:在线程合并场景中,如果线程 A 调用 B.join()去合入 B 线程,
则在 B 执行期间线程 A 处于 WAITING“等待”状态,一直等线程 B执行完成。
第二种:执行没有时限(timeout)参数的 object.wait()调用:指一个拥有 object 对象锁的线程,
进入到相应的代码临界区后,调用相应的 object 的 wait()方法去等待其“对象锁”(object monitor) 上的信号, 如果由于“对象锁”上没有信号,则当前线程处于 WAITING“等待”状态,
3)线程的 TIMED_WAITING“限时等待”状态,表示在等待唤醒。处于 TIMED_WAITING 状态的线程,也不会被分配 CPU 时间片,它们要等待被唤醒,或者知道等待的时限到期。
5.5 线程的yield操作
线程的 yield(让步)操作的作用:是让目前正在执行的线程放弃当前的执行,让出 CPU 的执行权限,使得 CPU 去执行其他的线程。处于让步状态的 JVM 层面的线程状态,仍然是RUNNABLE 可执行状态
5.6 线程的 daemon 操作
守护线程:Java 中的线程为分为两类:守护线程与用户线程。守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。
GC线程就是守护线程
守护线程提供服务,是守护者。用户线程享受服务,是被守护者。只有全部的用户线程终止了,相当于没有了被守护者,守护线程也就没有工作可做了,也可全部终止了
5.7 线程状态说明
状态类型 | 说明 | 场景 |
NEW 状态 | 通过 new Thread(…)已经创建线程,但尚未调用 start()启动线程,该线程处于 NEW(新建) 状态。 | |
RUNNABLE 状态 | Java 把就绪(Ready)和执行(Running)两种状态合并为一种状态:可执行(RUNNABLE) 状态(或者可运行状态) | 调用线程的 start()方法,此线程进入就绪状态。 等待用户输入结束。 |
执行状态 | 线程调度程序从就绪状态的线程中选择一个线程,作为当前线程时线程所处的状态。这也是 线程进入执行状态的唯一方式 | |
BLOCKED 状态 | 处于阻塞(BLOCKED)状态的线程并不会占用 CPU 资源 | 线程等待获取锁、IO 阻塞 |
WAITING 状态 | 处于 WAITING(无限期等待)状态的线程不会被分配 CPU 时间片需要被其他线程显式地唤醒,才会进入就绪状态 | Object.wait() 方法,对应的唤醒方式为:Object.notify() / Object.notifyAll()。 Thread.join() 方法,对应的唤醒方式为:被合入的线程执行完毕。 LockSupport.park() 方法,对应的唤醒方式为:LockSupport.unpark(Thread)。 |
TIMED_WAITING 状态 | 处于 TIMED_WAITING(限时等待)状态的线程不会被分配 CPU 时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态 | hread.sleep(time) 方法,对应的唤醒方式为:sleep 睡眠时间结束。 LockSupport.parkNanos(time)/parkUntil(time) 方法,对应的唤醒方式为:线程调 |
TERMINATED 状态 | 线程结束任务之后,将会正常进入 TERMINATED(死亡)状态;或者说在线程执行过程中 发生了异常(而没有被处理),也会导致线程进入死亡状态 |
6. 线程池
java创建线程是一种非常昂贵的资源,系统要为线程堆栈分配和初始化大量内存块,其中包含至少 1M 的栈内存,还要进行系统调用,以便在 OS(操作系统)中创建和注册本地线程
线程池的架构
下面做简要说明,感兴趣可以查看JDK文档或者相关网络资源
Executor:它是 Java 异步目标任务的“执行者”接口,其目标是来执行目标任务。
void execute(Runnable command)
ExecutorService:ExecutorService 继承于 Executor。
//向线程池提交单个异步任务 <T> Future<T> submit(Callable<T> task);
//向线程池提交批量异步任务 <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)throws InterruptedException;
AbstractExecutorService:AbstractExecutorService 存在的目的是为 ExecutorService 中的接口提供了默认实现
ThreadPoolExecutor:ThreadPoolExecutor 是 JUC 线程池的核心实现类
ScheduledExecutorService:它是一个可以完成“延时”“周期性”任务的调度线程池接口,其功能和 Timer/TimerTask 类似
ScheduledThreadPoolExecutor:类 似 于 Timer , 但 是 在 高 并 发 程 序 中 ,ScheduledThreadPoolExecutor 的性能要优于 Timer
Executors:静态工厂类
四个快捷线程池说明
方法名 | 功能介绍 | 场景 |
newSingleThreadExecutor() | 创建只有一个线程的线程池
| 任务按照提交次序,一个任务一个任务逐个执行的场景。 |
newFixedThreadPool(int nThreads) | 创建固定大小的线程池 | 需要任务长期执行的场景。“固定数量的线程池”的线程 数能够比较稳定保证一个数,能够避免频繁回收线程和创建线程,故适用于处理 CPU 密集型的任务,在 CPU 被工作线程长时间使用的情况下,能确保尽可能少的分配线程,多出来的需要队列存储,大量线程可能导致资源耗尽风险 |
newCachedThreadPool() | 创建一个不限制线程数量的线程池,任何提交的任务都将立即执 行,但是空闲线程会得到及时回收 | 需要快速处理突发性强、耗时较短的任务场景,如 Netty 的 NIO 处理场景、REST API 接口的瞬时削峰场景。大量线程任务可能导致资源耗尽 |
newScheduledThreadPool() | 创建一个可定期或者延时执行任务的线程池 | 周期性执行任务的场景。SpringBoot 中的任务调度器,底层 借助了 JUC 的 ScheduleExecutorService |
线程池创建规范:使用ThreadPoolExecutor去创建,这个很重要每个参数有时间都需要去理解,这里只是列出来
int corePoolSize, // 核心线程数,即使线程空闲(Idle),也不会回收;
int maximumPoolSize, // 线程数的上限;
long keepAliveTime, TimeUnit unit, // 线程最大空闲(Idle)时长
BlockingQueue<Runnable> workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler) // 拒绝策略
线程池调用流程
7. 线程数如何设定
任务类型: IO 密集型任务:CPU 核数的 2 倍
CPU 密集型任务: CPU 的核数
混合型任务:最佳线程数目 =(线程等待时间与线程 CPU 时间之比 + 1)* CPU 核数
8. ThreadLocal
ThreadLocal、线程本地化,使得每个线程都会拥有一个独立的、自己的本地值 、
使用场景线程隔离 、跨函数传递数据
前者在数据库连接中提现,
后者用来传递请求过程中的用户 ID、用来传递请求过程中的用户会话(Session)、用来传递 HTTP 的用户请求实例 HttpRequest、其他需要在函数之间频繁传递的数据
ThreadLocal 源码分析:后续补充
ThreadLocalMap 源码分析:后续补充
9. 什么是线程安全
当多个线程并发访问某个 Java 对象(Object)时,不管系统如何调度这些线程,也不管这些线程将如何交替操作,这个 Object 都能表现出一致的、正确的行为,
那么对这个 Object 的操作是线程安全的。
自增运算不是线程安全,可以通过CountDownLatch实现线程安全,CountDownLatch(倒数闩)是一个非常实用的等待多线程并发的工具
10. synchronized相关知识点
Java 中,线程同步使用最多的方法是——使用 synchronized 关键字。每个 Java 对象都隐含有一把锁,
这里称之为 Java 内置锁(或者对象锁、隐式锁)。使用 synchronized(syncObject)调用 相当于获取 syncObject 的内置锁,
所以,可以使用内置锁对 Critical Section 进行排他性保护
11. 线程中的锁
Java对象结构和内置锁需要单独去写一篇:后续补充
12. 线程间通讯
13. CAS问题
CAS 的英文全称为 Compare and Swap,翻译成中文为“比较并交换”
CAS 是一种无锁算法,该算法关键依赖两个值——期望值(就值)和新值,底层 CPU 利用原子操作,判断内存原值与期望值是否相等,
如果相等则给内存地址赋新值,否则不做任何操作
步骤:(1)获得字段的期望值(oldValue)(2)计算出需要替换的新值(newValue)。
(3)通过 CAS 将新值(newValue)放在字段的内存地址上,如果 CAS 失败则重复第 1 步到第 2 步,一直到 CAS 成功,这种重复俗称 CAS 自旋
Java 应用层的 CAS 操作
1)获取 Unsafe 实例 2)调用 Unsafe 提供的 CAS 方法,这些方法主要封装了底层 CPU 的 CAS 原子操作
3)调用 Unsafe 提供的字段偏移量方法,这些方法用于获取对象中的字段(属性)偏移量,此偏移量值需要作为参数提供给 CAS 操作
原子类
java.util.concurrent.atomic 类路径下类,原子性类,包含基本原子类、数数组原子类、原子引用类型、字段更新原子类
在多线程环境下,如果涉及到基本数据类型的并发操作,不建议采用 synchronized 重量级锁去进行线程同步
而是建议优先使用基础原子类去保障并发操作的线程安全性其实现原理为:
基础原子类(以 AtomicInteger 为例 )主要通过 CAS 自旋 + volatile 相结合的方案实现,既保障了变量操作的线程安全性,
又避免了 synchronized 重量级锁的高开销,使得 Java 程序的执行效率大为提升。
ABA问题
引用一个通俗的例子,你和女朋友分手后,经过一段时间,后来你们又和好了,这段时间内不保证你和你的女朋友是否在和其他人交往。
很多乐观锁的实现版本,都是使用版本号(version)方式来解决 ABA 问题,也就是记录标签,
使用 AtomicStampedReference 解决 ABA 问题, Stamp 印戳(或标记)
使用 AtomicMarkableReference 解决 ABA 问题,因此其标记属性 mark 是 boolean 类型
高并发下CAS操作
会导致大量的自旋,以空间换时间:LongAdder,将一个数分离成多个,下面是一个测试结果实验
内部原理与currenthashMap相近,分散热点,降低冲突概率
CAS产生的三个问题:ABA 问题、只能保证一个共享变量之间的原子性操作、开销问题
JDK中的应用
在java.util.concurrent.atomic 包中的原子类、Java AQS 以及显示锁、CurrentHashMap 等
在java.util.concurrent.atomic 包的原子类如 AtomicXXX 中,都使用了 CAS 保障对数字成员进行操作的原子性
在java.util.concurrent 的大多数类(包括显示锁、并发容器)都基于 AQS 和 AtomicXXX 实现,而 AQS 通过 CAS 保障其内部双向队列头部、尾部操作的原子性。
14. MESI协议是什么?
CPU 缓存分为三个级别 L1、L2、L3,越靠近 CPU 内核(含寄存器)的缓存速度越快、容量越小,反之则速度越慢、容量越大 L1最快
并发编程的三大问题:原子性问题、可见性问题、有序性问题
硬件层的 MESI 协议,是用于解决内存的可见性问题的一种手段,接下来为大家介绍 MESI
CPU 主要提供了两种解决办法:总线锁、缓存锁。
一个缓存一致性协议的基础版本为 MSI 协议,也叫做写入失效协议
M、E、S 和 I(全名是 Modified、Exclusive、 Share、Invalid)代表使用缓存行所处的 4 种状态,可用 2 个 bit 表示
M:处于 Modified 状态的缓存行数据,只有在本 CPU 中有缓存,且其数据与内存中的数据不一致,数据被修改过。
E: 处于 Exclusive 状态的缓存行数据,只有在本 CPU 中有缓存,且其数据与内存中一致,没有被修改过。
S:处于 Shared 状态的缓存行的数据在多个 CPU 中都有缓存,且与内存一致
I: 该缓存行是无效的,可能有其他 CPU 修改了该缓存行
15. 内存屏障是什么?
内存屏障(Memory Barrier),又称内存栅栏(Memory Fences),是一系列的 CPU 指令,其作用主要是保证特定操作的执行顺序,保障并发执行的有序性。
三类重排:编译器重排序、处理器重新排序(指令级重排序、内存系统重排序)
As-if-Serial 规则的具体内容为:不管如何重排序,都必须保证代码在单线程下的运行正确
硬件层常用的内存屏障分为三种:读屏障(Load Barrier)、写屏障(Store Barrier)、全屏障 (Full Barrier)
作用
1)阻止屏障两侧的指令重排序 2)强制让高速缓存的数据失效
16. java内存模型
Java 内存模型的定义的两个概念:主内存、工作内存
JMM的8个操作图
17. happens-before
Happens-Before(先行发生)规则,并且确保只要两个 Java语句之间必须存在 Happens-Before 关系,JMM 尽量确保这两个 Java 语句之间的内存可见性和指令有序性。
程序顺序执行规则(as-if-serial 规则)、volatile 变量规则、传递性规则、监视锁规则(Monitor Lock Rule)、start 规则、join 规则、
18. volatile
Java 的 volatile 关键字则可以保证共享变量的内存可见性,即将共享变量的改动值立即刷新回主存
volatile 关键字除了保障内存可见性,还能确保执行的有序性,但 volatile 不能完全保证数据的原子性
19. AQS
20. ConcurrentHashMap
分段式锁、jdk1.7和jdk1.8区别
21. 高并发设计模式
22.CompletableFuture
了解更多JAVA后台知识整理:JAVA后台系列目录