Java 并发编程
2024-03-18 11:52:35 0 举报
AI智能生成
Java并发编程是一种编程范式,允许多个任务同时执行,以提高程序的运行速度和性能。
作者其他创作
大纲/内容
线程是进程划分成的更小的运行单位。
JDK 1.2 之前,一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。
绿色线程
由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
用户线程
由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。
内核线程
用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。
区别
线程分类
Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程。
Java 在 Windows 和 Linux 是 1:N。
线程模型:1:1、1:N、N:N
程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务。
本地方法栈则为虚拟机使用到的 Native 方法服务。
保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
程序计数器、虚拟机栈和本地方法栈
堆存放新创建的对象。
方法区存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆和方法区
并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
并发与并行
同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步:调用在发出之后,不用等待返回结果,该调用直接返回。
同步和异步
问题:内存泄漏、死锁、线程不安全等
为了能提高程序的执行效率提高程序运行速度
在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。
线程安全
CPU 密集型和 IO 密集型。
线程类型
NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
BLOCKED:阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
线程生命周期和状态
切换过程占用CPU。
线程切换需要保存以及恢复相应线程的上下文。
线程上下文切换
多线程中一个或多个等待某个资源释放,而被无限期阻塞,导致程序不能正常终止。
互斥条件;请求与保持条件;不剥夺条件;循环等待条件;
条件
线程死锁
sleep() 方法没有释放锁,wait() 方法释放了锁 。
sleep()方法操作线程,wait()方法操作对象(对象锁)。
sleep() 方法和 wait() 方法
真正多线程
new一个Thread并调用start(),分到时间片后,start()准备后自动调用run()。
非多线程
直接调用run()就是main线程下的普通方法。
start() 方法和 run() 方法
多线程
进程 vs 线程
主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。
CPU 可以通过制定缓存一致协议(比如 MESI 协议)来解决内存缓存不一致性问题。
为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。
编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。
Java源代码重排过程:编译器优化重排 —> 指令并行重排 —> 内存系统重排
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,多线程下可能引发问题。
指令重排序
Memory Barrier,或有时叫做内存栅栏(Memory Fence)
一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。
内存屏障
主内存:所有线程创建的实例对象都存放在主内存中。
本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。
同步操作:锁定(lock);解锁(unlock);read(读取);load(载入);use(使用);assign(赋值);store(存储);write(写入);
同步规则:8种,了解即可。
通过逻辑时钟来区分事件发生的前后顺序。
前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。
程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作。
解锁规则:解锁 happens-before 于加锁。
volatile 变量规则:对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。
传递规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
线程启动规则:Thread 对象的 start()方法 happens-before 于此线程的每一个动作。
如果两个操作不满足上述任意一个 happens-before 规则,JVM 就可以对这两个操作进行重排序。
常见规则
happens-before 原则
借助synchronized、各种 Lock 以及各种原子类实现原子性。
原子性
借助synchronized、volatile 以及各种 Lock 实现可见性。
可见性
volatile 关键字可以禁止指令进行重排序优化。
有序性
并发编程重要特性
JMM(Java 内存模型)Java Memory Model
标记JVM每次都要到主存中进行读取。
保证变量的可见性
插入特定内存屏障
防止 JVM 的指令重排序
双重检验锁方式实现单例模式
保证变量的可见性,但不能保证对变量的操作是原子性的。
volatile 关键字
假设最好,提交时验证修改(CAS算法)
java.util.concurrent.atomic包下面的原子变量类
大量失败重试,导致CPU飙升。
多用于写比较少的情况(多读场景,竞争较少)
乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些。
Compare And Swap(比较与交换)
原子操作,底层依赖于一条 CPU 的原子指令。
V:要更新的变量值(Var)
E:预期值(Expected)
N:拟写入的新值(New)
三个操作数
CAS算法
ABA 问题;循环时间长开销大;只能保证一个共享变量的原子操作(AtomicReference对象原子性);
乐观锁
假设最坏,使用就会上锁
synchronized 和 ReentrantLock 等独占锁
大量阻塞,频繁切换上下文,死锁等
多用于写比较多的情况(多写场景,竞争激烈)
早起属于重量级锁,Java 6 后轻量级锁
自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁
偏向锁增加了 JVM 的复杂性,JDK15默认关闭,JDK18彻底废弃
修饰实例方法;修饰静态方法;修饰代码块;
通过获取对象监视器 monitor 实现。
只能是非公平锁。
synchronized 关键字
实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
内部类 Sync 继承 AQS(AbstractQueuedSynchronizer)
内部类 Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类,默认非公平锁。
公平锁 : 锁被释放之后,先申请的线程先得到锁。
非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。
ReentrantLock
synchronized 和 ReentrantLock都是可重入锁。
可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。
等待可中断;可实现公平锁;可实现选择性通知(锁可以绑定多个条件);
ReentrantLock 比 synchronized 多的高级功能
Condition接口:JDK1.5,实现多路通知进行线程调度,可选择性通知。
使用少,JDK1.8后使用性能更好的读写锁 StampedLock。
ReentrantReadWriteLock
悲观锁
依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
锁状态
可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理,ReentrantLock。
不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理,synchronized。
可中断锁和不可中断锁
共享锁:一把锁可以被多个线程同时获得。
独占锁:一把锁只能被一个线程获得。
共享锁和独占锁
写锁可以降级为读锁,但是读锁却不能升级为写锁。
JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 Conditon。
提供了三种模式的读写控制模式:读锁、写锁和乐观读。一定条件下进行相互转换。
适合读多写少的业务场景。性能虽好,但使用麻烦,不建议使用高级性能。
基于 CLH 锁 实现的(与AQS一样),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。
StampedLock
乐观锁和悲观锁
提供线程局部变量,每个线程 Thread 拥有一份自己的副本变量(ThreadLocalMap),多个线程互不干扰。
类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
ThreadLocal<?> k ,继承自WeakReference,弱引用
Key
代码中放入的值,强引用
Value
Key被回收,Value仍然存在,容易出现内存泄漏。
HASH_INCREMENT = 0x61c88647
斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
Hash 算法
HashMap通过链表结构或红黑树解决。
ThreadLocalMap没有链表结构,采用。。。
Hash 冲突
探测式清理(expungeStaleEntry()):以当前Entry 往后清理,遇到值为null则结束清理,属于线性探测清理。
启发式清理(cleanSomeSlots()):Heuristically scan some cells looking for stale entries.
过期Key数据清理方式
先探测式清理再扩容
扩容机制
在异步场景下给子线程共享父线程中创建的线程副本数据。
有缺陷,推荐阿里开源的 TransmittableThreadLocal 组件。
InheritableThreadLocal
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
内存泄露
ThreadLocalMap
ThreadLocal
为了减少每次获取资源的消耗,提高对资源的利用率。
通过 ThreadPoolExecutor 构造函数来创建(推荐)。
FixedThreadPool:返回一个固定线程数量的线程池。
SingleThreadExecutor: 返回一个只有一个线程的线程池。
CachedThreadPool: 返回一个可调整线程数量的线程池。
ScheduledThreadPool:返回一个延迟后运行或者定期执行任务的线程池。
弊端:最大Integer.MAX_VALUE,导致OOM。
通过 Executor 框架来创建(不推荐)。
创建线程池
抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.AbortPolicy
任务回退给调用者,使用调用者的线程来执行任务。
ThreadPoolExecutor.CallerRunsPolicy
不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardPolicy
将丢弃最早的未处理的任务请求。
ThreadPoolExecutor.DiscardOldestPolicy
线程池的饱和策略
LinkedBlockingQueue(无界队列):FixedThreadPool 和 SingleThreadExector 。
SynchronousQueue(同步队列):CachedThreadPool 。
DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPool 和 SingleThreadScheduledExecutor 。
阻塞队列
CPU 密集型任务(N+1)
I/O 密集型任务(2N)
最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))
设定线程池的大小
美团:自定义队列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
Hippo4j:异步线程池框架
Dynamic TP:轻量级动态线程池
动态修改线程池的参数
使用 PriorityBlockingQueue (优先级阻塞队列)作为任务队列(workQueue 参数)
优先级任务线程池
SpringBoot 中的 Actuator 组件。
利用 ThreadPoolExecutor 的相关 API 做监控。
监测线程池运行状态
重复创建线程池的坑
Spring 内部线程池的坑,一定要手动自定义线程池,配置合理的参数
解决:TransmittableThreadLocal(TTL)
线程池和 ThreadLocal 共用的坑,可能会导致线程从ThreadLocal获取到的是旧值/脏数据。
坑儿
线程池
Executor 框架是 Java5 之后引进来管理线程的框架,比Thread更好,易管理,效率好,避免this逃逸问题。
任务(Runnable /Callable)
任务的执行(Executor)
异步计算的结果(Future)
构成
Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。
Runnable vs Callable
execute()无返回值,submit()有返回值。
execute() vs submit()
shutdown()关闭线程池,线程池的状态变为 SHUTDOWN。
shutdownNow()关闭线程池,线程池的状态变为 STOP。
shutdown() vs shutdownNow()
isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true。
isTerminated() vs isShutdown()
对比
Executor
异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。
取消任务;判断任务是否被取消;判断任务是否已经执行完成;获取任务执行结果;
功能
Future 接口的基本实现,用来封装 Callable 和 Runnable。
FutureTask 相当于对 Callable 进行了封装,管理着任务执行的情况,存储了 Callable 的 call 方法的任务执行结果。
FutureTask
Future 存在一些缺陷,比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用。
Java 8 引入 CompletableFuture 类解决 Future 的缺陷。
CompletableFuture 同时实现了 Future 和 CompletionStage 接口。
CompletionStage 接口描述了一个异步计算的阶段。
通过 new 关键字。
基于 CompletableFuture 自带的静态工厂方法:runAsync()、supplyAsync() 。
创建
thenApply()、thenAccept()、thenRun()、whenComplete()
处理异步结算的结果
handle()、exceptionally()、completeExceptionally()
异常处理
thenCompose()、thenCombine()
组合
allOf()、anyOf()
并行
常用方法
建议使用自定义的线程池,而非默认的 ForkJoinPool.commonPool()
如果必须要使用的话,需要添加超时时间
尽量避免使用 get()
正确进行异常处理
合理组合多个异步任务
建议
CompletableFuture
Future
抽象队列同步器(AbstractQueuedSynchronizer):主要用来构建锁和同步器。
原理:用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)
资源共享方式
使用了模板方法模式,自定义同步器时需要重写 AQS 提供的钩子方法
钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰。
自定义同步器
构造(信号量)可以用来控制同时访问特定资源的线程数量。
原理:共享锁的一种实现,默认构造 AQS 的 state 值为 permits。
场景:限流,仅限单机,实际推荐Redis + Lua 来做。
Semaphore
一次性。
允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
原理:共享锁的一种实现,默认构造 AQS 的 state 值为 count。
场景:多个无顺序依赖任务,需要统一返回结果。-- 阻塞,条件,释放
替代:CompletableFuture,Java 8
CountDownLatch
与 CountDownLatch 非常类似,但更加复杂和强大。
原理:基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的实现。
CyclicBarrier
AQS
Java 21 重量级更新
由 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。
平台线程(Platform Thread):在引入虚拟线程之前,JVM 调度程序通过平台线程(载体线程)来管理虚拟线程。
主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统除外。
非常轻量级;简化异步编程;减少资源开销;
不适用计算密集型任务;依赖于语言或库支持;
优缺点
Thread.startVirtualThread()、Thread.ofVirtual()、ThreadFactory、Executors.newVirtualThreadPerTaskExecutor()
在密集 IO 的场景下,比平台线程性能更好。
虚拟线程(Virtual Thread)
线程安全的 HashMap
另一种线程安全的 HashMap实现:Collections.synchronizedMap()
ConcurrentHashMap
程安全的 List,在读多写少的场合性能非常好,远远好于 Vector。
在 JDK1.5 之前,如果想要使用并发安全的 List 只能选择 Vector。
CopyOnWriteArrayList
高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
ConcurrentLinkedQueue
这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
BlockingQueue 接口的有界队列实现类,底层采用数组来实现。
ArrayBlockingQueue
底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用。
LinkedBlockingQueue
支持优先级的无界阻塞队列。
PriorityBlockingQueue
BlockingQueue
跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
跳表是一种可以用来快速查找的数据结构,有点类似于平衡树,是一种利用空间换时间的算法。
ConcurrentSkipListMap
并发容器总结
具有原子/原子操作特征的类,都存放在java.util.concurrent.atomic下。
AtomicInteger:整型原子类
AtomicLong:长整型原子类
AtomicBoolean:布尔型原子类
基本类型
AtomicIntegerArray:整型数组原子类
AtomicLongArray:长整型数组原子类
AtomicReferenceArray:引用类型数组原子类
数组类型
AtomicReference:引用类型原子类
AtomicMarkableReference:原子更新带有标记的引用类型。
AtomicStampedReference:原子更新带有版本号的引用类型。
引用类型
AtomicIntegerFieldUpdater:原子更新整型字段的更新器
AtomicLongFieldUpdater:原子更新长整型字段的更新器
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
对象的属性修改类型
Atomic 原子类
Java 并发编程
0 条评论
回复 删除
下一页