# javaconcurrency **Repository Path**: interview_44/javaconcurrency ## Basic Information - **Project Name**: javaconcurrency - **Description**: 介绍java并发编程 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-04-20 - **Last Updated**: 2023-03-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 并发编程学习 介绍:针对java并发编程,学习,复习,面试使用。 [1、线程生命周期](#1线程生命周期) [2、可见性](#2可见性) [3、有序性](#3有序性) [4、原子性](#4原子性) [5、volatile](#5volatile) [6、synchronize](#6synchronize) [7、CAS](#7CAS) [8、lock](#8lock) [9、lock](#9线程池) [10、java多线程工具](#10java多线程工具) [11、MESI协议](#11MESI协议) ### 1、线程生命周期 下图展示线程生命周期 ![](./pictrue/1、线程的生命周期.png) Java 语言中线程共有六种状态,分别是: NEW(初始化状态) RUNNABLE(可运行 / 运行状态) BLOCKED(阻塞状态) WAITING(无时限等待)TIMED_WAITING(有时限等待)TERMINATED(终止状态) ![](./pictrue/2、java线程生命状态.png) ### 2、可见性 ![](./pictrue/3、可见性.png) 在一段范围时间段内,方法A的执行结果对方法B不可见,导致方法B错误执行。 具备可见性,方法A的结果,强制方法B可见。 一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。 ### 3、有序性 ![](./pictrue/4、有序性.png) 多核cpu,jdk编译代码时,或者寄存器执行指令时,导致指令重排序,造成有序性问题。 ### 4、原子性 在一段时间范围内,方法A对变量Z操作,方法B不允许操作变量Z。 保护变量Z,叫有原子性。 ![](./pictrue/5、原子性.png) ### 4-5、并发规则简介 synchronized atomic lock voratile happens-beforez ![](./pictrue/3-并发包工具粗讲.drawio.png) ### 5、volatile volatile 解决可见性和有序性,专门解决可见性 基于happen-before规则 可见性:当有线程修改了volatile的字段被修改,变量从寄存器放回主存,会强制刷新其他线程改变量值,关键是用cpu的flush与reflush指令。 有序性:禁止volatile修饰的变量的读写进行重排,利用内存屏障。 特殊场景原子性:例如32位的long与duble,加上volatile就可以保证值原子性。 ![](./pictrue/6、volatile.png) 多线程执行共享变量,每个线程有自己的上下文,在高速缓存中保存有该变量,为了节省时间,拿来就用。 共享变量加上volatile修饰之后,变量每一次写完都会强制刷新到内存,总线,会感知变量修改,从主存中拉取最新值到工作缓存中。 ### 6、synchronize 保证原子性,有序性,可见性,指令重排序 synchroniz(myObject){ synchroniz(myObject){ } } sychronize破坏不可强占条件,所以需要使用lock。 1、能够响应中断 2、支持超时 3、非阻塞地获取锁 ![](./pictrue/7、synchronize简略.png) Synchronized 在1.6 之前只是重量级锁。 因为会有线程的阻塞和唤醒,这个操作是借助操作系统的系统调用来实现的,常见的 Linux 下就是利用 pthread 的 mutex 来实现的。 而涉及到系统调用就会有上下文的切换,即用户态和内核态的切换,我们知道这种切换的开销还是挺大的。 所以称为重量级锁,也因为这样才会有上面提到的自适应自旋操作,因为不希望走到这一步呀! 在 Java 中,对象结构分为对象头、实例数据和对齐填充。 而对象头又分为:MarkWord 、 klass pointer、数组长度(只有数组才有),我们的重点是锁,所以关注点只放在 MarkWord 上。 ![](./pictrue/11、MarkWord.png) MarkWord 结构之所以搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。 在重量级锁时,对象头的锁标记位为 10,并且会有一个指针指向这个 monitor 对象,所以锁对象和 monitor 两者就是这样关联的。 而这个 monitor 在 HotSpot 中是 c++ 实现的,叫 ObjectMonitor,它是管程的实现,也有叫监视器的。 这里有个概念叫临界区。 我们知道,之所以会有竞争是因为有共享资源的存在,多个线程都想要得到那个共享资源,所以就划分了一个区域,操作共享资源资源的代码就在区域内。 可以理解为想要进入到这个区域就必须持有锁,不然就无法进入,这个区域叫临界区。 当用 Synchronized 修饰代码块时 ![](./pictrue/9、修饰代码块时.png) 此时编译得到的字节码会有 monitorenter 和 monitorexit 指令,我习惯按照临界区来理解,enter 就是要进入临界区了,exit 就是要退出临界 区了,与之对应的就是获得锁和解锁。 实际上这两个指令还是和修饰代码块的那个对象相关的,也就是上文代码中的lockObject。 每个对象都有一个 monitor 对象于之关联,执行 monitorenter 指令的线程就是试图去获取 monitor 的所有权,抢到了就是成功获取锁了。 从生成的字节码我们也可以得知,为什么 synchronized 不需要手动解锁? 是有人在替我们负重前行啊!编译器生成的字节码都帮咱们做好了,异常的情况也考虑到了。 当用 synchronized 修饰方法时 ![](./pictrue/10、synchronize修饰方法.png) 原理就是修饰方法的时候在 flag 上标记 ACC_SYNCHRONIZED,在运行时常量池中通过 ACC_SYNCHRONIZED 标志来区分,这样 JVM 就知道这个方法 是被 synchronized 标记的,于是在进入方法的时候就会进行执行争锁的操作,一样只有拿到锁才能继续执行。 运行原理: ![](./pictrue/8、sychronize实现原理.png) 1、可以看到重点就是通过 CAS 把 ObjectMonitor 中的 _owner 设置为当前线程,设置成功就表示获取锁成功。 2、如果 CAS 失败的话,会执行下面的一个循环:先再尝试一下获取锁,不行的话就自适应自旋,还不行就包装成 ObjectWaiter 对象加入到 _cxq 这个单向链表之中,挣扎一下还是没抢到锁的话,那么就要阻塞了。 3、wait 操作:就是将当前线程加入到 _waitSet 这个双向链表中,然后再执行 ObjectMonitor::exit 方法来释放锁。 4、notify 操作:就是从 _waitSet 头部拿节点,然后根据策略选择是放在 cxq 还是 EntryList 的头部或者尾部,并且进行唤醒。 为什么会有_cxq 和 _EntryList 两个列表来放线程? 因为会有多个线程会同时竞争锁,所以搞了个 _cxq 这个单向链表基于 CAS 来 hold 住这些并发,然后另外搞一个 _EntryList 这个双向链表, 来在每次唤醒的时候搬迁一些线程节点,降低 _cxq 的尾部竞争。 引入自旋:自旋其实就是空转 CPU,执行一些无意义的指令,目的就是不让出 CPU 等待锁的释放。 正常情况下锁获取失败就应该阻塞入队,但是有时候可能刚一阻塞,别的线程就释放锁了,然后再唤醒刚刚阻塞的线程,这就没必要了。 引入轻量级锁:多个线程都是在不同的时间段来请求同一把锁,此时根本就用不需要阻塞线程,连 monitor 对象都不需要,所以就引入了轻量级锁这个 概念,避免了系统调用,减少了开销。 还有非常复杂的锁升级逻辑,没法总结,有点难~-_-~ ![](./pictrue/13、锁升级过程.png) ### 7、CAS 一、存在并发问题 int i = 0; public int add(){ i ++; } 二、不存在并发问题 public class myObject(){ AtomicInteger i = 0; public int add(){ i.incrementAndGet(); } } ![](./pictrue/14、CAS.png) 执行逻辑就是,在存值的时候,先进行CAS比对,是否原来的值,如果不为原来的值,则再次读取,再次执行,存值的时候又去比对。 ConcurrentHashMap map里面有多个元素,各个线程操作不同的元素,不需要串行执行,所以推出ConcurrentHashMap。 jdk7:实现的思路是分段加锁。 jdk8:put时如果2线程都是put相同数组,就采取cas策略,同一个时间只有一个线程能执行cas。(底层保证cas同时只有一个线程能成功执行), 只有一个线程put之前拿出来是null,直接插入数据,刚才已经有人放值进去该数组,就需在该位置基于链表+红黑树进行处理,该hash地址转为数组, synchronized(数组【5】)锁上,基于链表或者是红黑树在这个位置插进去自己的数据。 ### 8、lock lock比sychronize多了3个方法 // 支持中断的API void lockInterruptibly() // 支持超时的API boolean tryLock(long time, TimeUnit unit) // 支持非阻塞获取锁的API boolean tryLock(); AQS:Abstract Queued Synchronize,基于AQS实现lock ReentrantLock lock = new ReentrantLock(); lock.lock(); lock.unlock(); lock默认使用非公平锁,线程1执行完了,队列里面有线程2,线程3;线程2先进入队列,但是有可能会线程3先拿到锁。 可以加参数,使线程变为公平锁,等待队列里的线程不会无限等待 ![](./pictrue/15、lock的AQS.png) 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再 tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重 复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。 再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执 行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数 返回,继续后余动作。 各种锁: (1)锁消除 锁消除是JIT编译器对synchronized锁做的优化,在编译的时候,JIT会通过逃逸分析技术,来分析synchronized锁对象,是不是只可能被一个线程来加锁,没有其他的线程来竞争加锁,这个时候编译就不用加入monitorenter和monitorexit的指令 这就是,仅仅一个线程争用锁的时候,就可以消除这个锁了,提升这段代码的执行的效率,因为可能就只有一个线程会来加锁,不涉及到多个线程竞争锁 (2)锁粗化 synchronized(this) { } synchronized(this) { } synchronized(this) { } 这个意思就是,JIT编译器如果发现有代码里连续多次加锁释放锁的代码,会给合并为一个锁,就是锁粗化,把一个锁给搞粗了,避免频繁多次加锁释放锁 (3)偏向锁 这个意思就是说,monitorenter和monitorexit是要使用CAS操作加锁和释放锁的,开销较大,因此如果发现大概率只有一个线程会主要竞争一个锁, 那么会给这个锁维护一个偏好(Bias),后面他加锁和释放锁,基于Bias来执行,不需要通过CAS 性能会提升很多 但是如果有偏好之外的线程来竞争锁,就要收回之前分配的偏好 可能只有一个线程会来竞争一个锁,但是也有可能会有其他的线程来竞争这个锁,但是其他线程唉竞争锁的概率很小 如果有其他的线程来竞争这个锁,此时就会收回之前那个线程分配的那个Bias偏好 (4)轻量级锁 如果偏向锁没能成功实现,就是因为不同线程竞争锁太频繁了,此时就会尝试采用轻量级锁的方式来加锁,就是将对象头的Mark Word里有一个轻量级 锁指针,尝试指向持有锁的线程,然后判断一下是不是自己加的锁 如果是自己加的锁,那就执行代码就好了 如果不是自己加的锁,那就是加锁失败,说明有其他人加了锁,这个时候就是升级为重量级锁 (5)适应性锁 这是JIT编译器对锁做的另外一个优化,如果各个线程持有锁的时间很短,那么一个线程竞争锁不到,就会暂停,发生上下文切换,让其他线程来执行。 但是其他线程很快释放锁了,然后暂停的线程再次被唤醒 也就是说在这种情况下,线程会频繁的上下文切换,导致开销过大。 ### 9、线程池 为了让系统不无限制的创建线程,构造一个线程池,执行任务后不销毁,下次任务来了可以直接使用,不用创建线程销毁线程。 ExecutorService threadPool = Executors.newFixedThreadPool(3) -> 3:corePoolSize threadPool.submit(new Callable(){ public void run (){ } }); ![](./pictrue/16、线程池工作原理.png) 线程池参数 corePoolSize:3 maximumPoolSize:Integer.MAX_VALUE(当线程池用光了,最多可创建的线程) keepAliveTime:60s(额外线程空闲时间->60s->销毁) new ArrayBlockingQueue(200) 线程使用完,队列满了,只能reject掉,他有几种reject策略,可以传入RejectedExecutionHandler (1)AbortPolicy (2)DiscardPolicy (3)DiscardOldestPolicy (4)CallerRunsPolicy (5)自定义 问题一: 任务使用无界阻塞队列,队列不会满,但是否会内存飙升。如果任务很慢很慢,任务非常多,内存使用会飙升,最后出现OOM。 问题二: 任务使用有界队列,队列满了之后,可以自定义reject处理,可以把任务写入磁盘中,等资源降低了,再拿出来处理。 问题三: 服务器宕机,线程池阻塞队列里面的任务会怎么样? 线程池阻塞队列里面的任务都会丢失。 解决:要提交任务到线程池阻塞队列里,在提交前先插入一条任务到数据库(未提交,已提交,已完成),如果机器宕机了,可以捞数据库里的任务。 问题四: Executors线程池里面的线程不会退出,join()失效? 应用计时器:CountDownLatch或者CyclicBarrier ![](./pictrue/17、Executors线程池里面的线程不会退出.png) ### 10、java多线程工具 1、Semaphore 信号量,允许一定数量的线程进入临界区,一定数量的并行 init():设置计数器的初始值。 down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。 up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。 其他组合工具: ![](./pictrue/18、java并发工具.png) ### 11、MESI协议 ![](./pictrue/19、MESI.png) 可见性如何实现?:store屏障+loader屏障 如果加了store屏障之后就会要求你对一个写操作必输阻塞等待其他的处理器返回invalidateAck之后, 对数据加锁,然后修改数据到高速缓存中,必须在写数据之后,强制执行flush操作。 加了load屏障之后,在告诉缓存中读取数据时,如果发现无效队列里有一个invalidate消息, 把本地高速缓存数据设置为I(过期),然后就可以强制从其他处理器的高速缓存中加载最新值。 有序性如何实现?: 内存屏障,Acquire屏障,Release屏障,但是都是由基础的StoreStore屏障,StoreLoad屏障,可以避免指令重排序的效果。 StoreStore屏障,会强制让写数据的操作全部按照顺序写入写缓冲器里,他不会让你第一个写到写缓冲器里去,第二个写直接修改高速缓存了。 # java并发工具 [12、lock](#12、lock) ### 12/lock的出现 synchronized在获取锁的时候,如果获取不到,线程会出现阻塞,啥也不干,也不会释放原有的资源。 #### ReentrantLock |--Lock&Condition |--ReentrantLock ReentrantLock内部持有一个volitale的成员变量state,同一个线程可在锁内state+1。 #### ReadWriteLock 两种锁:读锁、写锁 1、读-并发 2、写-只能一个线程写 3、写得时候不允许读/写 支持锁降级,不支持锁升级 #### StampedLock 高性能读写锁 性能较高 三种锁:写锁、悲观读锁、乐观读锁 支持锁升级,降级。 #### CountDownLatch线程同步工具 主要用来解决一个线程等待多个线程。主线程等待几十个子线程。 latch.await() 来实现对计数器等于 0 的等待。 #### CyclicBarrier 主要用于各线程之间互相等待。 创建 CyclicBarrier 的时候,我们还传入了一个回调函数,当计数器减到 0 的时候,会调用这个回调函数。 #### CopyOnWriteArrayList 安全的list 读无锁,写会把整个数组复制到另外一个数组。迭代器只能遍历,不能增删改 #### ConcurrentHashMap key不能为空,安全。1.8以后,细粒度锁,锁key的hashcode的之后的位置 #### ConcurrentSkipListMap 跳表,不理解 #### linkedBlockingQueue 单端阻塞队列,Excutor内部队列 #### Executor 线程池 不建议使用 Executors 的最重要的原因是: Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。 #### ThreadPoolExecutor 线程池 handler: 通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列), 那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。 #### FutureTask 异步计算任务 |-- Future |---FutureTask 支持获得任务执行结果 #### CompletableFuture 多线的任务并行后 - 程聚合,等待工具 |--CompletionStage |--CompletableFuture 1、无需手工维护线程,没有繁琐的手工维护线程的工作,给任务分配线程的工作也不需要我们关注; 2、语义更清晰,例如 f3 = f1.thenCombine(f2, ()->{}) 能够清晰地表述“任务 3 要等待任务 1 和任务 2 都完成后才能开始”; 3、代码更简练并且专注于业务逻辑,几乎所有代码都是业务逻辑相关的。 4、指定线程池 #### CompletionService 批量提交 - 线程操作工具 多个线程一起提交 -> 执行结果 -> 放入阻塞队列 -> 那个线程先执行完,谁先往下走。 #### ForkJoinPool 分治任务 #### MapReduce 大数据处理模型