JUC:Java 并发编程学习笔记
并发压测工具jcstress概念进程Process线程Thread本质程序的一次执行实例Execution Instance进程内的一个执行流Execution Flow是什么操作系统分配资源的基本单位操作系统进行调度的基本单位Java 底层会调用 pthread_create 来创建线程所以本质上 java 程序创建的线程就是和操作系统线程是一样的是 1 对 1 的线程模型。并发和并行创建新线程Context Switch主动让出 CPU比如调用了sleep(),wait()等。时间片用完调用了阻塞类型的系统中断比如请求 IO线程被阻塞。被终止或结束运行常用方法Sleep 让线程从running 到timed wating 阻塞状态释放 CPU但不会释放持有的锁 醒来过不一定立即执行yield方法 hint调度器当前线程愿意放弃其当前的 CPU 时间片。状态从RUNNING(运行中) -RUNNABLE(就绪) 。调度器完全可以忽略这个提示让该线程立即再次运行设置线程优先级 cpu忙的时候优先级高的时间片分的多 闲的时候优先级没什么用 最终 都是取决于os的任务调度器join/join(long) 等待线程运行结束 可用于实现同步等待中断线程通过标志位判断需要正确使用volatile关键字对目标线程调用interrupt()方法 如果该线程正在执行低级别的可中断方法如Thread.sleep()、Thread.join()或Object.wait()则会解除阻塞并抛出InterruptedException异常 →抛异常 清标志非阻塞状态运行、锁竞争 →不抛异常 保留打断标志相当于“未被打断依旧正常执行”设计模式——两阶段终止模式方案一利用interrupt方法第一阶段通知线程准备停止设置停止标志或发送中断信号。第二阶段线程感知到终止请求后完成剩余任务并释放资源最终安全退出。 方案二volatile变量替代打断标记Park方法但打断标记true的时候park失效守护线程线程状态状态转换BLOCKED和WAITING两个状态最大的区别有两个BLOCKED是锁竞争失败后的被动结果WAITING是主动触发的状态BLCKED的唤醒时jvm自动触发的而WAITING状态是必须要通过特定的方法来主动唤醒BLOCKED和WAITING都是用放弃 CPU的方式来换取不消耗资源但代价是高昂的切换成本。共享模式之管程monitor临界区对共享资源有读有写的竞态条件互斥 是解决“竞态条件”的手段上下文切换是trigger它通过保证“原子性”来确保并发安全 同步 是一个比“互斥”更宽泛的概念。它的目的就是协调多个线程的执行顺序或时机线程安全成员变量和静态变量 如果被共享了且有写操作 非线程安全 局部变量 是线程安全的 但如果是引用的对象需要判断是否发生引用逃逸局部变量的引用本身是线程私有的但被引用对象是否线程安全要看对象是否逃逸出方法作用域成员变量实例字段存放在堆Heap中静态变量类字段存放在方法区 / 元空间MetaSpace逻辑上也是共享的局部变量存在每个线程自己的栈帧Stack Frame中设计线程安全类时禁止子类通过继承修改关键同步逻辑通过final和privateString类就是final的常见线程安全类组合调用不一定安全Integer 和String 都是不可变类 就是read only 因此线程安全即不改变只新建monitor lock操作系统提供的 把锁对象的markword置为指向monitor的指针 把owner设置为该线程对象头4字节markword 4字节kcalss word 数组对象还多一个4字节的数组长度normal状态下 mark word的内容 是hashcodeGCagebiaslocktwo bits的标识synchronized优化wait notifywait/notify等方法依赖于monitor对象这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法Owner线程发现条件不足调用wait方法进入waitset变成waiting 状态BLOCKED和WAITING的线程都处于阻塞状态不占用CPU时间片唤醒 BLOCKED线程 会在Owner线程释放锁时被jvm全部唤醒 WAITING线程在Owner线程调用notify才被唤醒 被唤醒后竞争是非公平的可能导致虚假唤醒问题和sleep区别设计模式-同步模式保护性暂停用在一个线程等待另一个线程的结果 使用notifyAll()并配合while循环来检查等待条件是 Java 中实现线程间等待/通知的标准范式join的原理就是应用了保护性暂停 只不过等待的是另一个线程的结束解耦异步模式park unpark解决了suspend/resume的死锁问题原理通过_counter计数 当作“通行证”每个线程都有且仅有一个关联的“通行证”。这个通行证的“数量”非 0 即 1unpark(Thread t)的作用是给目标线程t“发放”一个通行证调用LockSupport.park()的作用是检查当前线程是否持有通行证没有会进入WAITING状态Condition(await/signal) 和synchronized(wait/notify) 都是“监视器”模型而park/unpark是“许可证”模型。park/unpark的设计专门解决了wait/notify的一个核心痛点。痛点wait/notify必须先wait后notify。如果一个线程notify发生在了wait之前这个通知信号就会丢失导致wait的线程永久等待。多锁细粒度的锁 增强程序并发性死锁死锁是指两个或多个线程或进程在执行过程中因互相持有对方所需要的资源而导致的一种永久的阻塞状态。在这种状态下所有线程都在等待一个永远不会被释放的资源导致程序无法继续执行。哲学家就餐问题 tryLock定位死锁jconsel 或者jsp定位id 再jstack分析活锁问题双方同时改变对方的结束条件 解决方案引入随机性如随机睡眠时间饥饿现象 可以使用公平锁ReentrantLock可重入 可打断 锁超时 公平锁 条件变量多个waitlist一定要在finally块里unlock可打断 必须调用.lockInterruptibly(); “可中断”特性只在阻塞状态的阶段响应中断支持超时ReentrantLock提供了tryLock(timeout)的方法可以指定等待获取锁的最长等待时间如果超过了等待时间就会获取锁失败不会一直等待。条件变量 借助于newCondition()方法和await()/signal()——实现等待/通知机制注意Condition必须在锁内使用同步模式——顺序控制使用reentrantLock的awaitsignal机制 实现交替输出的时候 如果没有像wait/notify 一样使用状态变量 就会有死锁风险信号丢失可见性犹豫/balking模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事那么本线程就无需再做了直接结束返回 经典应用线程安全的单例线程安全单例实现共享模式之无锁CASCompare-And-Swap基于乐观锁的思想 CAS配合volatile因为你比较肯定比最新的值嘛可以实现无锁并发无阻塞并发上下文切换相对少线程数最好不超过cpu核心数怎么实现的 Java 中 CAS 是 C 内联汇编的形式实现的通过 JNI调用。因此CAS 的具体实现与操作系统以及 CPU 密切相关原子类原子类的组合操作不是线程安全的 解法就是标准的 CAS 自旋循环while(true)原子操作类Atomic类依赖于 CAS乐观锁volatile 来保证其方法的原子性基本类型AtomicInteger AtomicLong AtomicBoolean引用类型AtomicReference在多线程环境下修改一个对象的引用例如User user new User(...)本身是原子的但是“检查后执行”Check-Then-Act的操作不是原子的。例如if (user expected) user newOne;这段逻辑如果不加锁在并发下是不安全的。 而AtomicReference利用 CPU 提供的 CAS指令保证原子性而无需使用重量级的synchronized锁。AtomicStampedReference带版本号/时间戳的原子引用解决了ABA问题数组类型使用原子的方式更新数组里的某个元素对象的属性修改类型原子类/字段更新器它只是一个工具/视窗。它本身不存储数据FieldUpdater在底层利用了Unsafe.compareAndSwapObject指令。CAS 操作要想安全前提是多线程必须能看到变量的“最新值”即内存可见性。 由于FieldUpdater无法改变字段原本的定义它只能强制要求你在定义字段时就必须加上volatile原子累加器LongAdderLongAdder的思路是既然一个变量base大家抢着更新太慢那我就把这个变量拆成很多份cell[]让大家分头去更新最后再加起来缓存行伪共享 为防止缓存行失效导致效率降低CAS可能的问题ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。循环时间长开销大 CAS 经常会用到自旋操作来进行重试也就是不成功就一直循环执行直到成功。如果长时间不成功会给 CPU 带来非常大的执行开销Unsafe类共享模式之不可变不可变类设计天生线程安全 可以作为 HashMap 的 Key 原子性保障类声明为final防止被继承子类可能会重写方法来破坏不可变性。所有成员变量为private finalprivate隐藏内部实现。final保证引用一旦赋值不可变更且保证了多线程下的初始化安全性JMM 保证。不提供 Setter 方法堵死修改的入口。构造函数初始化所有变量在创建时一次性赋值。关键点保护性拷贝如果成员变量是引用类型如Date,List,Person且该对象本身是可变的你必须在构造函数和 Getter 方法中进行深拷贝。享元模式最小化内存使用体现例子包装类、String 串池、连接池final 原理final通过禁止构造函数内的指令重排序写屏障保证了对象在多线程环境下的初始化安全性线程池线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗。线程池处理流程线程池状态用一个AtomicInteger ctl 描述 前三bits是状态 后29bits是线程数量构造方法7个参数corePoolSize线程池核心线程数量。默认情况下线程池中线程的数量如果 corePoolSize那么即使这些线程处于空闲状态那也不会被销毁。maximumPoolSize限制了线程池能创建的最大线程总数包括核心线程和非核心线程当corePoolSize已满 并且 尝试将新任务加入阻塞队列失败即队列已满并且 当前线程数 maximumPoolSize就会创建新线程执行此任务但是当corePoolSize满 并且 队列满 并且 线程数已达maximumPoolSize并且 又有新任务提交时就会触发拒绝策略。keepAliveTime当线程池中线程的数量大于corePoolSize并且某个线程的空闲时间超过了keepAliveTime那么这个线程就会被销毁。unit就是keepAliveTime时间的单位。workQueue工作队列。当没有空闲的线程执行新任务时该任务就会被放入工作队列中等待执行。threadFactory线程工厂。可以用来给线程取名字等等handler拒绝策略。当一个新任务交给线程池如果此时线程池中有空闲的线程就会直接执行如果没有空闲的线程就会将该任务加入到阻塞队列中如果阻塞队列满了就会创建一个新线程从阻塞队列头部取出一个任务来执行并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize就不会创建新线程就会去执行拒绝策略四种预置的拒绝策略CallerRunsPolicy使用线程池的调用者所在的线程去执行被拒绝的任务除非线程池被停止或者线程池的任务队列已有空缺。AbortPolicy直接抛出一个任务被线程池拒绝的异常。DiscardPolicy不做任何处理静默拒绝提交的任务。DiscardOldestPolicy抛弃最老的任务然后执行该任务。自定义拒绝策略通过实现接口可以自定义任务拒绝策略提交任务方法所属接口参数 / 返回值阻塞性异常处理机制 (核心)适用场景executeExecutorRunnablevoid|否|直接抛出❌ 堆栈可见⚠️ Worker可能重建 |发后即忘(日志、通知) ||submit|Ex...Service|Run/CallableFuture|否|内部捕获如果是runnable就需要用trycatch块future 需调用get()才会抛出 |异步求值(需要结果/后续处理) ||invokeAll|Ex...Service|CollCallListFuture|是(等全部) |封装于 Future⏳ 超时未完会被自动取消|并发聚合 (全量)(比价、多源汇总) ||invokeAny|Ex...Service|CollCallT(结果) |是(等最快) |丢弃异常️ 除非全失败才抛出 |竞争获取 (赛马)(多备份查询取最快) |Shutdownshutdown使用了以后会置状态为SHUTDOWN正在执行的任务会继续执行下去没有被执行的则中断。此时则不能再往线程池中添加任何任务否则将会抛出 RejectedExecutionException 异常而 shutdownNow 为STOP并试图停止所有正在执行的线程不再处理还在池队列中等待的任务当然它会返回那些未执行的任务。 它试图终止线程的方法是通过调用 Thread.interrupt() 方法来实现的但是这种方法的作用有限如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以ShutdownNow()并不代表线程池就一定立即就能退出它可能必须要等待所有正在执行的任务都执行完成了才能退出。异步模式之工作线程池化死锁根本原因当提交到线程池的任务父任务依赖于同一个线程池中的子任务的执行结果时就可能发生死锁。 解决方案 隔离原则准备多个线程池有线程池参数设置的经验吗核心线程数corePoolSize设置的经验CPU密集型corePoolSize CPU核数 1避免过多线程竞争CPUIO密集型corePoolSize CPU核数 x 2或更高具体看IO等待时间fork/join 池原文地址JUC本文持续更新最新内容及相关资料请访问原文。