
并发
多线程
上下文切换
在一个CPU 上,多个线程共享 CPU 时间片,当一个线程的时间片用完后,需要切换到另一个线程运行。此时需要保存当前线程的状态信息,包括程序计数器、寄存器、栈指针等,以便下次继续执行该线程时能够恢复到正确的执行状态。同时,需要将切换到的线程的状态信息恢复,以便于该线程能够正确运行。
如何减少上下文切换?
- 减少线程数:通过合理的线程池管理,避免过多线程导致的上下文切换。
- 使用无锁并发编程:避免线程因等待锁而阻塞
- 使用CAS算法:避免线程阻塞和唤醒
- 使用协程:协程是用户态线程,切换不需要操作系统参与,避免了操作系统级别的上下文切换。
- 合理使用锁:缩小同步块或同步方法的范围,减少线程等待时间
线程状态
如何创建线程
- 继承Thread类
- 实现Runnable接口
- 通过Callable和FutureTask创建线程
- 线程池
Java内存模型 (JMM)
控制多线程环境下线程如何访问共享内存,确保线程间的可见性、原子性和有序性。通过主内存和工作内存的交互机制,来保证多线程程序的正确性。
as-if-serial 原则
单线程,编译器和处理器按这个原则,就可以认为是按顺序执行的
happens-before原则
多线程
- 如果一个操作happens-before另一个操作,那么第一个操作的结果对第二个操作是可见的,即第一个操作在内存中的改变对第二个操作是可见的。
- 所有的同步操作(如synchronized、volatile变量的读写等)都遵循happens-before原则
异常
- try-catch中只能捕获本线程的异常,不能捕获子线程的异常
- 每个线程是独立的,并发执行,一个线程出现异常,JVM进程不会退出
如何捕获?
- 使用Callable和Future:通过实现Callable接口并在call方法中抛出异常,可以使用Future.get()方法来获取子线程的执行结果和异常。
- 异常处理器接口:可以定义一个异常处理器接口,并在子线程中调用该接口的方法来处理异常。
- CompletableFuture:CompletableFuture.supplyAsync() 来执行任务,并使用handle()方法捕获异常。
并发安全
概念:
并发环境中,多个线程同时访问共享变量,得到正确的结果。
- 并发:一个CPU,为多个用户进程分配时间片,时间片轮转。并行:多个CPU同时进行
- 共享变量:保存在堆、方法区
- 成员变量:类内,方法外
- 类变量(静态变量):方法区
- 实例变量:堆
- 局部变量:类内,方法内。栈
- 成员变量:类内,方法外
- 正确:原子性 有序性 可见性
实现线程安全的方案
- 单线程 redis
- 互斥锁 Synchronized、ReentrantLock等
- 读写分离 COW
- 原子操作 一般依赖CAS
- 不可变模式 string
- 数据不共享 ThreadLocal
线程同步的方式
- Synchronized 方法、代码块,同一时间只有一个线程执行。
- ReentrantLock 更灵活
- Semaphore 多个线程同时访问资源,限制线程个数
- CountDownLatch 计数器,允许一个或多个线程等待其他线程完成操作后再继续执行。
- CyclicBarrier 同步屏障,允许多个线程相互等待,直到到达某个公共屏障点后再一起继续执行。
Synchronized
原理
- 底层:监视锁Monitor
- 每个对象都有自己的Monitor
- Monitor 结构:
- _header:对象头(存储 Mark Word,用于锁状态)。
- _count:锁计数器(记录重入次数)。
- _owner:当前持有锁的线程。
- _WaitSet:等待锁的线程集合(调用 wait() 的线程)。
- _EntryList:竞争锁的线程集合(未获取锁的线程)。
同步方法
- 同步方法的常量池中会有一个ACC_SYNCHRONIZED 标志,当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED
- 如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
- 如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
同步代码块
- 使用 monitorenter和monitorexit两个指令实现。可以把执行monitorenter 指令理解为加锁,执行monitorexit理解为释放锁。
- 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。 当计数器为0的时候,锁将被释放,其他线程便可以获得锁。
锁升级
- 无锁:对象未被锁定。
- 偏向锁:
- 触发条件:首次进入sychronized块的线程
- 锁对象会记录偏向线程 ID,后续该线程访问时直接获取锁。
- 轻量级锁:
- 触发条件:另一个线程尝试获取已被偏向的锁
- 通过 CAS 操作竞争锁,失败则升级为重量级锁。
- 重量级锁:
- Monitor结构
- 线程想获取锁,需要先进入等待队列
锁优化
- 锁升级
- 自旋锁
- 线程在获取锁失败时,不立即阻塞,而是自旋(循环尝试)一段时间。(不放弃CPU)
- 如果自旋期间锁被释放,线程可以直接获取锁。
- 锁消除
- JIT 编译器通过逃逸分析判断锁对象是否不会逃逸出当前线程(即不会被其他线程访问)。
- 如果锁对象是线程私有的,JIT 会移除同步代码。
- 锁粗化
- JIT 编译器将相邻的多个同步块合并(减少锁竞争次数)。
ReentrantLock
和synchronized都是可重入锁
volatile
修饰变量。无锁
- 可见性:变量修改后,刷新到内存
- 有序性:通过内存屏障 禁止指令重排
- 不能保证原子性
CAS
Compare And Swap,先比较再替换
原理
- 包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。并发修改时,先比较A和V中取出的值是否相等,如果相等,则会把值替换成B,否则就不做任何操作。
- 当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
主要应用:实现乐观锁和锁自旋。
操作系统层面
- 原子性:基于硬件提供的cmpxchg指令,是原子的;
- 可见性:cmpxchg指令基于CPU缓存一致性协议
AQS
抽象队列同步器。reentrantlock等就是基于AQS
实现:
- 一个FIFO队列 + 一个volatile的int类型的state变量。在state=1的时候表示当前对象锁已经被占有了,state变量的值修改的动作通过CAS来完成。
ThreadLocal
为每一个线程创建一份共享变量的副本,来保证各个线程之间的变量的访问和修改互相不影响
方法
- initialValue
- get
- set
- remove
实现原理
- Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组。
内存泄漏
- 栈上的ThreadLocal Ref引用不在使用了,即方法结束后这个对象引用就不再用了,那么,ThreadLocal对象因为还有一条引用链在,所以就会导致他无法被回收
- 弱引用。那么ThreadLocal对象就可以在下次GC的时候被回收掉了。
- Thread对象如果一直在被使用,比如在线程池中被重复使用,那么从这条引用链就一直在,那么就会导致ThreadLocalMap无法被回收。
- 在一个ThreadLocal用完之后,手动调用一下remove,就可以在下一次GC的时候,把Entry清理掉。
InheritableThreadLocal:用于主子线程之间参数传递
线程池
提前创建好一批线程,保存在线程池中。当有任务需要执行时,从线程池中选一个线程来执行。
参数
- corePoolSize: 核心线程数量。正式员工数量,常驻线程数量。
- maximumPoolSize: 最大的线程数量。公司最多雇佣员工数量,常驻+临时线程数量。
- workQueue: 多余任务等待队列。再多的人都处理不过来了,需要等着,在这个地方等。
- keepAliveTime: 非核心线程空闲时间。外包人员等了多久,如果还没有活干,解雇了。
- threadFactory: 创建线程的工厂。在这个地方可以统一处理创建的线程的属性。每个公司对员工的要求不一样,在这里设置员工的属性。
- handler: 线程池拒绝策略。当任务实在是太多,人也不够,需求池也排满了,还有任务咋办?
- AbortPolicy(默认):当线程池无法接受新任务时,会抛出RejectedExecutionException异常。新任务会被立即拒绝,不会加入到任务队列中,也不会执行。
- DiscardPolicy:丢弃新的任务而且不会抛出异常。新任务提交后会被默默地丢弃,不会有任何提示或执行。一般用于日志记录、统计等不是非常关键的任务。
- DiscardOldestPolicy:丢弃任务,但它会先尝试将任务队列中最早的任务删除,然后再尝试提交新任务。如果任务队列已满,且线程池中的线程都在工作,可能会导致一些任务被丢弃。实时性要求较高的场景。
- CallerRunsPolicy:将任务回退给调用线程,而不会抛出异常。调用线程会尝试执行任务。降低任务提交速度,适用于任务提交者能够承受任务执行的压力,但希望有一种缓冲机制的情况。
实现
- ThreadPoolExecutor:使用共享阻塞队列(如 LinkedBlockingQueue),所有线程从同一个队列获取任务。如果队列为空,线程会阻塞等待新任务。
- ForkJoinPool:每个线程有自己的双端队列(Deque),优先执行自己的任务。如果自己的任务队列为空,线程会窃取其他线程的任务(从队列尾部偷取)。自动创建销毁线程,可以递归地将大任务拆分为小任务。
场景
三个线程如何顺序执行?
- 直接调用start方法:不行
- 使用join方法:Thread类中的join方法可以让主线程等待一个子线程完成后再继续执行。通过在T2中调用thread1.join(),在T3中调用thread2.join(),可以确保T1完成后T2执行,T2完成后T3执行。
- 使用CountDownLatch:CountDownLatch是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。通过创建三个CountDownLatch对象,可以在每个线程执行完毕后调用countDown,而主线程则通过await方法等待。
- 使用CyclicBarrier:CyclicBarrier用于等待所有线程到达一个公共屏障点。通过创建一个CyclicBarrier对象,并在每个线程中调用await方法,可以确保所有线程按顺序执行。
- 使用Semaphore:Semaphore用于控制同时访问某个特定资源的线程数量。通过创建一个许可数为1的Semaphore,可以确保线程按顺序执行。
- 使用线程池:通过创建一个单线程的线程池,可以按照任务提交的顺序执行线程,因为线程池内部使用队列存储任务。
- 使用CompletableFuture:CompletableFuture可以用来实现异步编程,通过链式调用thenRun或thenAccept等方法,可以在前一个任务完成后执行下一个任务。
- 底层实现:主要依赖于ForkJoinPool线程池。当创建一个CompletableFuture时,它会将任务提交到ForkJoinPool中,由池中的线程异步执行。任务完成后,会触发链式调用的下一个任务。
三个线程分别顺序打印0-100
- synchronized + wait()/notifyAll()
- 使用一个共享对象作为锁,并维护一个 currentThread 变量标记当前应该执行的线程。
- 每个线程检查是否轮到自己执行,如果是则打印并唤醒下一个线程,否则等待。
- ReentrantLock + Condition
- 使用 ReentrantLock 和多个 Condition 实现更精细的线程唤醒控制。
- 每个线程对应一个 Condition,轮到自己时才唤醒。
1万+

被折叠的 条评论
为什么被折叠?



