尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

一文掌握JUC常见八股

一文掌握JUC常见八股 线程线程的状态及流转Java中在Thread类里有一个枚举应Enum明确规定了Java线程的6种状态。创建线程对象新建状态new调用start方法就绪状态Runable就绪状态如果抢到CPU的执行去哪就变成运行阻塞状态Blocked无法获取锁对象。等待状态waiting遇到wait方法时计时等待状态TIME_WAITING遇到 sleep 方法时。结束状态Terminated本身的run方法执行完毕后。创建线程的方式继承Thread类实现Runnable接⼝实现Callable接⼝结合FutureTask适⽤于执⾏有返回值的任务通过线程池创建线程多线程的应用软件中耗时操作拷贝迁移文件加载大文件所有聊天软件所有后台服务器线程和协程的区别可以把县程想象成公司的正式员工成本高每个人都要占工位内存大约占 1 MB。入职离职手续麻烦创建和销毁慢。归老板管操作系统就是老板。老板让你干你就干让你停就得停抢占式调度切换任务时还要开交接会上下文切换而协程就像是外包临时工成本极低自带小板凳就能干活(栈内存极小几KB)招之即来挥之即去。归项目组管程序自己就是组长用户态调度。组长发现A临时工在等快递IO阻塞立马让他去旁边休息换B临时工上来干活根本不需要惊动大老板(OS)。线程问题线程A里启动一个线程BA线程需要等待B线程执行完后才能执行。都有哪些方法可以实现这种场景Thread.join方法并发工具类CountDownLatchCompletableFutureimportjava.util.concurrent.CompletableFuture;publicclassCompletableFutureDemo{publicstaticvoidmain(String[]args){System.out.println(A 线程启动 B 线程);// 启动 B 线程并等待它完成CompletableFuture.runAsync(()-{System.out.println(B 线程执⾏);try{Thread.sleep(2000);}catch(InterruptedExceptione){}System.out.println(B 线程执⾏完毕);}).join();// 阻塞 A 线程等待 B 完成 (注意这⾥)System.out.println(A 线程继续执⾏);}}线程池使用线程池的好处通过使用线程池可以起到资源管理、线程复用的效果避免频繁地创建和销毁线程带来的性能开销。具体解释线程创建和销毁是一项开销较大的操作通过使用线程池可以事先创建一些线程并将其放入池中。需要执行任务时直接从池中复用线程从而避免频繁的创建和销毁。线程池的七个参数核心线程数量最大线程数量临时线程最大存活时间临时线程最大存活时间单位任务阻塞队列线程工厂用于创建线程的工厂类任务的拒绝策略线程池提交任务的流程提交任务时会先让核心线程去执行任务。如果核心线程都在工作则先将任务放到阻塞队列。如果阻塞队列满了创建临时线程去执行任务核心线程加临时线程不能超过最大线程数。如果达到最大线程数则会触发拒绝策略。任务拒绝策略怎么选择线程池的拒绝策略是在任务队列满且线程数达到最大值时触发的兜底机制。JDK内置了4种策略AbortPolicy直接抛RejectedExecutionException这是默认策略。调用方能立即感知任务被拒适合核心业务场景比如订单支付必须让上游知道这单没处理。2)CallerRunsPolicy谁提交的任务谁执行。调用者线程被拉去干活自然就提交不了新任务相当于一个反压机制。适合允许短暂阻塞调用线程的场景比如后台日志异步写入。3)DiscardOldestPolicy踢掉队列里排队最久的那个任务把当前任务塞进去。用的时候要小心老任务可能比新任务更重要。DiscardPolicy静默丢弃不抛异常也不执行。只适合那些丢了也无所谓的场景比如埋点上报。如何设置线程池的线程数线程数怎么设置关键看是 CPU 密集型还是 IO 密集型。对于 CPU 密集型任务比如解密、压缩、复杂计算这时候线程开太多没有意义线程切换反而浪费时间。经验是设置为 CPU 核心数 1多出来的一个是为了应对偶发的页缺失等中断。IO密集型任务比如读数据库线程大部分时间在等IO返回CPU闲着。这时候可以多开线程让CPU在等待期间去处理其他任务经验是CPU核心数乘2。上线需要关注那些线程指标运行时状态指标核心线程数活跃线程数队列大小任务处理效率指标任务提交速率完成速率任务平均耗时任务等待时间资源占用指标CPU使用率内存占用异常情况指标拒绝任务数线程创建/销毁数线程安全线程安全的核心是在多线程并发访问共享资源时程序的执行结果始终符合预期且不会出现数据错乱或逻辑错误。其根源在于下面三个方面原子性操作是否不可分割可见性线程对共享变量的修改是否对其他线程可见有序性代码执行顺序是否可能被编译器和 CPU 重排序Thread local.ThreadLocal提供了一种线程级别的数据存储机制每个线程都拥有自己的独立ThreadLocal意味着每个线程都会独立安全地操作变量而不会影响其他线程。典型使用场景用户身份信息存储在请求拦截器或过滤器中鉴权校验用户身份后将用户信息存入 ThreadLocal 中。在请求的后续链路中如果需要获取用户信息直接从 ThreadLocal 中获取。线程安全ThreadLocal 可以用来存储一些需要并发安全处理的成员变量。日志上下文存储在常见的日志框架中经常使用 ThreadLocal 来存储与当前线程相关的日志上下文。主要的两个作用4. 在线程中传递数据在同一个线程执行过程中ThreadLocal 的数据一直在。所以可以在前面把数据放到 ThreadLocal 中在后面需要时再取出来这就避免了数据通过参数在多层方法中传递。5. 解决并发问题实现原理Thread 类对象中维护了 ThreadLocalMap 成员变量。ThreadLocalMap 类对象中维护了 Entry 数组Entry 数组中的每一个元素都是一个 Entry 对象(a) 每个 Entry 对象中存储了一个 ThreadLocal 对象与其要存入的数据 value。(b) 每个 Entry 对象在 Entry 数组中的位置是通过 ThreadLocal 对象的 threadLocalHashCode 计算出来的以此快速定位 Entry 对象在 Entry 数组中的位置。所以在 Thread 中可以存储多个 ThreadLocal 对象。内存泄露内存泄露存在无法回收的对象假设我们单独开启一个线程并且将数据存储到 ThreadLocal 中。当线程执行任务结束退出时线程对象被销毁那么线程与 ThreadLocalMap 实例对象之间的关系就不存在了。在 GC 时ThreadLocalMap 对象、ThreadLocal 对象以及之前存储的数据都会被回收掉所以其实不存在内存泄漏。Synchronized每个Java对象头部都有一个mark word存储着对象的运行时数据如哈希码、锁状态信息。锁状态信息标志了synchronized的状态。原理Java的同步机制是分层抽象的在语言层面是一个关键字在JVM字节码层面对应的是monitorenter和monitorexit两台哦字节码指令表示进入临界区告诉JVM在此处需要所和解锁在JVM内部通过Monitor监视器来管理线程的竞争和等待每个锁对象都关联一个监视器内部通过三个逻辑区域管理线程竞争owner当前持有锁的线程。entryList等待锁的阻塞线程队列。流程如下当线程执行 monitorenter 时JVM 会检查锁对象的 monitor(a) 若 owner 为空线程成为 owner。(b) 若 owner 被占用线程进入 entrylist 阻塞。当线程执行 monitor exit 时JVM 释放锁并唤醒 entrylist 中的线程让它们去竞争锁。在操作系统层面需要从用户态切换到内核态向操作系统底层申请互斥量性能开销很大因此称其为重量锁。锁升级过程synchronized由于涉及到操作系统层面申请互斥量还涉及到线程的阻塞和唤醒都需要从用户态切换到内核态性能开销很大因此称其为重量级锁。在 JDK 1.6 中考虑到不同竞争场景和不同竞争强度的场景对 synchronized 进行了优化经历了从无锁到偏向锁再到轻量级锁最后到重量级锁的过程。无锁-偏向锁最初是无锁的状态。当一个 synchronized 代码块被线程首次进入时JVM 会将锁标记为偏向锁并在锁对象头中记录下该线程的 ID。如果该线程再次请求同一把锁即重入锁通过比较线程 ID它会直接获得锁。这主要是为了没有锁竞争且需要重入的场景而引入的。偏向锁-轻量级锁如果有其他线程来请求获取锁此时偏向锁就会被撤销。两个线程会通过 CAS 方式自旋尝试获取锁。获取锁后JVM 会将锁标记为轻量级锁。获取锁的流程将锁对象头中的 mark word 复制到线程栈中的锁记录结构中。尝试通过 CAS 操作将锁对象头的 mark word 的内容更新为指向锁记录的指针。如果这个更新操作成功那么这个线程就成功获取了这个对象的轻量级锁。轻量级锁-重量级锁当有较多线程短时间内请求获取锁意味着当前竞争比较激烈。而轻量级锁会使用 CAS 方式会占用 CPU因此此时会发生锁膨胀JVM 将锁标记为重量级锁。重量级锁中竞争失败的线程会陷入阻塞不会占用 CPU。总的来说所升级的过程受益思路其实是为了适配不同竞争强度下的场景。ReentrantLockLock是接口ReentrantLock是其实现类底层实现是AQS。AQSAQS 是阻塞式锁和相关同步器工具的框架主要由两部分组成state 属性用于表示共享资源的状态分为独占模式和共享模式。不同的子类实现中对于 state 值有不同的含义。(a) 独占模式只有一个线程能够访问资源。(b) 共享模式允许多个线程使用。此外使用 CAS 机制更新 state 属性保证对 state 值修改的线程安全。FIFO 等待队列双向队列用于存储等待获取锁的线程。因为有些线程可以抢占到锁而有些线程无法抢占到锁此时没有竞争到锁的线程就会被打包成一个 Node 节点按照顺序组成一个双向链表。当释放锁时会唤醒双向链表中的第一个 Node 节点。独占模式ReentrantLock只有一个线程能够访问资源共享模式CountDownLatch允许多个线程同时访问资源。ReentrantLockReentrantLock 是 Java 并发包中的一类。它提供了比 synchronized 关键字更灵活的锁机制支持公平锁和非公平锁并且可以中断、超时等高级特性。ReentrantLock 如何实现公平锁和⾮公平锁先解释公平和非公平的概念公平竞争锁资源的线程严格按照请求的顺序来分。非公平竞争锁资源的线程允许插队来尝试抢占锁资源。ReentrantLock 默认是非公平锁。其底层 AQS 的实现是当线程没有抢占到锁时会将其打包成 Node 节点加入到 FIFO 双向链表。在上述背景的情况下公平锁的实现当线程在竞争锁资源时会判断双向链表中是否有线程在等待。如果有就加入到链表的尾部进行等待。非公平锁的实现当线程在竞争锁资源时不管双向链表中是否有线程在等待都会去尝试抢占锁资源。如果抢占不到再加入双向链表。公平和非公平其实是相对于在双向链表中等待的线程而言的。CASCS 原理Compare and Swap比较再交换在更新前检查数据是否被修改若未被修改则更新否则自旋重试具体解释如下主存中有一个共享变量。当线程想要修改这个变量的值时具体流程如下初始计算线程需要先把该变量的值拷贝到自己的工作内存中进行计算操作得到一个新的值。比较与写入线程带着旧值和新值再次找到主存中的共享变量比较旧值和当前主存中的共享变量是否相同(a) 若相同说明这期间没有其他线程修改过主存中的共享变量是线程安全的。因此把新值写入主存的共享变量中。(b) 若不同说明期间有其他线程修改过主存中的共享变量不是线程安全的。此时需要自旋重试。自旋重试再次取得主存中共享变量的值回到线程的工作内存进行计算操作得到新值。然后再次比较旧值和主存中的共享变量的值是否相同重复上述逻辑直至自旋成功。CAS 的应用场景AQS 中大量使用 CAS 来实现基本操作。juc包下的原子类中大量使用 CAS volatile来保证线程安全。CAS 的优劣点优点不加锁无锁并发保证线程安全。缺点自旋占用 CPU。乐观锁和悲观锁乐观锁(a) 核心态度持乐观态度认为并发冲突的概率较低。(b) 工作机制在提交时检查数据是否被修改若未被修改则提交否则重试或抛出异常。© 实现方式例如版本号、CAS 及乐观锁实现。(d) 应用场景适合读多写少可以降低所带来的性能开销。(e) 特点并发性能高系统能够容忍或处理失败的情况因为乐观锁可能会因为并发冲突导致执行失败。悲观锁(a) 核心态度持悲观态度认为并发冲突的概率高。(b) 工作机制每次操作数据时都加锁确保线程安全。© 实现方式例如 synchronized 及悲观锁实现。(d) 应用场景适合写多读少避免频繁冲突导致的多次重试数据一致性要求极高如金融系统。(e) 特点并发性能低。
返回列表