并发编程思维导图分享(包含详细知识点)
思维导图地址:
并发编程思维导图,点此跳转
思维导图内容如下:
并发专题
- 并发理论知识
- 并发与并行
- 并行
- 指在同一时刻,有多条指令在多个处理器上同时执行。(多通路同时执行)
- 并发
- 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行
- 线程基础
- 线程&进程
- 进程:操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。(网易云音乐、腾讯视频)
- 线程:线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。
- 进程与线程的区别
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信同一台计算机的进程通信称为 IPC,不同计算机之间的进程通信,需要网协议,例如 HTTP线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
- 进程间通信的方式
- 信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
- 管道(pipe)及有名管道(named pipe)
- 管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
- 信号(signal)
- 信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
- 消息队列(message queue)
- 消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
- 共享内存(shared memory)
- 可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
- 信号量(semaphore)
- 主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
- 套接字(socket)
- 这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
- 线程的同步互斥
- 线程同步:一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
- 线程互斥:对于共享的进程系统资源,在各单个线程访问时的排它性。
- 线程同步互斥的控制方法
- 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
- 互斥量:为协调共同对一个共享资源的单独访问而设计的。
- 信号量:为控制一个具有有限数量用户资源而设计。
- 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
- Java线程
- Java线程的实现方式
- 继承Thread类
- 实现 Runnable 接口
- 使用有返回值的 Callable
- 线程池
- 1.newCachedThreadPool创建一个可缓存线程池程
- 2.newFixedThreadPool 创建一个定长线程池
- 3.newScheduledThreadPool 创建一个定长线程池
- 4.newSingleThreadExecutor 创建一个单线程化的线程池
- Java线程实现原理
- 基于操作系统原生线程模型来实现。都使用与操作系统一对一的线程模型实现
- Java线程的调度机制
- 抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定
- Java线程的生命周期(6种状态)
- NEW(初始化状态)
- RUNNABLE(可运行状态+运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待)
- TIMED_WAITING(有时限等待)
- TERMINATED(终止状态)
- Thread常用方法
- start方法:创建线程,等得到cpu的时间段后则会执行所对应的run方法体的代码
- sleep方法(不会释放对象锁)
- 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
- sleep当传入参数为0时,和yield相同
- yield方法(不会释放对象锁)
- yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
- 具体的实现依赖于操作系统的任务调度器
- join方法:等待调用join方法的线程结束之后,程序再继续执行
- Java线程的中断机制
- 注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况
- sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
- wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位
- interrupt(): 将线程的中断标志位设置为true,不会停止线程
- isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
- Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
- Java线程间通信
- volatile
- volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
- 等待唤醒机制
- cas+park/unpark
- LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”
- sychronized+wait/notify/notifyAll
- reentrantLock+Condition(await/singal/singalAll)
- 管道输入输出流
- Thread.join
- join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。
- Java线程的实现方式
- Java线程的生命周期(6种状态)
- start,sleep,join,yield等方法详解
- Java线程的中断机制
- 线程&进程
- Java内存模型(JMM)
- 一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
- JMM是围绕原子性、有序性、可见性展开的
- 这块如何学?
- 这部分理解并发的三大特性,JMM工作内存和主内存关系,知道多线程之间如何通信的,掌握volatile能保证可见性和有序性,CAS就可以了,后续JVM层面和硬件层面的分析,基础比较薄弱的同学听不懂可以先跳过,从后面的Java锁机制课程听起,掌握常用的并发工具类,并发容器之后再来看JMM这块。
- 并发三大特性
- 可见性
- 当一个线程修改了共享变量的值,其他线程能够看到修改的值。
- Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
- 如何保证可见性
- volatile关键字
- 内存屏障
- synchronized关键字
- Lock
- final关键字
- 有序性
- 程序执行的顺序按照代码的先后顺序执行。
- JVM 存在指令重排,所以存在有序性问题。
- 如何保证有序性
- volatile关键字
- 内存屏障
- synchronized关键字
- Lock
- 指令重排序
- 只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序
- 意义:使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能
- 原子性
- 一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
- 如何保证原子性
- synchronized关键字
- Lock
- CAS
- JMM与硬件内存架构的关系
- Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系:
- 主存和工作内存交互8大原子操作
- 规则:
- 要求右侧操作必须按顺序执行,而没有保证必须是连续执行。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
- JMM的内存可见性保证
- 单线程程序
- 不会出现内存可见性问题
- 正确同步的多线程程序
- 正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。
- JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
- 未同步/未正确同步的多线程程序
- JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。
- JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。
- volatile
- volatile的特性
- 可见性
- 对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 实现原理
- JMM内存交互层面实现
- volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程的可见性。
- 硬件层面实现
- 通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
- 实现原理
- 原子性
- 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(基于这点,我们通过会认为volatile不具备原子性)。
- 锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。
- 有序性
- 对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性。
- volatile内存语义
- 读:JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量
- 写:JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
- volatile的特性
- JMM内存屏障
- 插入策略
- 1. 在每个volatile写操作的前面插入一个StoreStore屏障
- 2. 在每个volatile写操作的后面插入一个StoreLoad屏障
- 3. 在每个volatile读操作的后面插入一个LoadLoad屏障
- 4. 在每个volatile读操作的后面插入一个LoadStore屏障
- JVM层面的内存屏障
- LoadLoad屏障:(指令Load1; LoadLoad; Load2)
- 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- LoadStore屏障:(指令Load1; LoadStore; Store2)
- 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:(指令Store1; StoreStore; Store2)
- 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- ⭐StoreLoad屏障:(指令Store1; StoreLoad; Load2)
- 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
- 它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
- 由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令,其他屏障对应空操作
- 硬件层内存屏障
- lfence:是一种Load Barrier 读屏障
- sfence,:是一种Store Barrier 写屏障
- mfence: 是一种全能型的屏障,具备lfence和sfence的能力
- ⭐Lock前缀:Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
- 内存屏障功能
- 对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
- Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。
- 阻止屏障两边的指令重排序
- 刷新处理器缓存/冲刷处理器缓存
- JVM层与硬件层内存屏障关系:Java内存模型屏蔽了底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
- 并发三大特性
- 并发与并行
- 线程安全问题
- 分类
- 运行结果错误
- 例如:两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果不是 0
- 原因: Java 中对静态变量的自增,自减并不是原子操作
- 活跃性问题(死锁,饥饿,活锁)
- 对象发布和初始化
- 线程安全问题解决方案
- 无锁
- 局部变量
- 不可变对象(final关键字)
- cas+自旋(Atomic原子操作类)
- CAS
- (Compare And Swap,比较并交换),通常指的是这样一种原子操作CAS缺陷
- CAS缺陷
- 自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销
- 只能保证一个共享变量原子操作
- ABA 问题
- 当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改成功。
- ABA问题的解决方案
- 1.数据库有个锁称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加。
- 2.Java也提供了相应的原子引用类AtomicStampedReference<V>,reference即我们实际存储的变量,stamp是版本,每次修改可以通过+1保证版本唯一性。这样就可以保证每次修改后的版本也会往上递增。
- CAS缺陷
- Atomic原子操作类
- 在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。
- 基本类型:AtomicInteger、AtomicLong、AtomicBoolean;
- 引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;
- 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
- 原子类型累加器(jdk1.8增加的类):DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64
- 有锁
- synchronized
- synchronized的使用:synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用
- synchronized底层原理
- synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。
- Monitor(管程)
- 管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发
- Java语言的内置管程synchronized
- Java语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。
- synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。
- notify()和notifyAll()分别何时使用
- 满足以下三个条件时,可以使用notify(),其余情况使用notifyAll():
- 1.所有等待线程拥有相同的等待条件;
- 2.所有等待线程被唤醒后,执行相同的操作;
- 3.只需要唤醒一个线程。
- Hotspot对象的内存布局
- 对象头
- Mark Word
- 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- 64位JVM下的对象结构描述
- hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
- age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
- biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
- lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
- JavaThread*: 保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
- epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
- Klass Pointer
- 对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。
- 数组长度(只有数组对象有,4字节)
- 实例数据
- 存放类的属性数据信息,包括父类的属性信息
- 对齐填充
- 由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
- 对象头
- 偏向锁
- 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。
- 偏向锁延迟偏向(4s)
- 调用对象HashCode
- 1.当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁;
- 2.当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。
- 原因:mark Word中偏向锁是没有地方保存hashcode
- 当对象可偏向时,MarkWord未锁定状态,升级成轻量锁
- 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁
- 调用wait/notify
- 偏向锁obj.notify() 升级为轻量级锁
- 偏向锁obj.wait(timeout) 升级为重量级锁
- 轻量级锁
- 轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。
- 偏向锁升级轻量级锁:多个持有锁的线程交替执行
- 轻量级锁膨胀为重量级锁:多个持有锁的线程同时执行
- synchronized锁优化
- 批量重偏向(bulk rebias)
- 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID
- 应用场景
- 一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。
- 总结
- 1.批量重偏向和批量撤销是针对类的优化,和对象无关。
- 2.偏向锁重偏向一次之后不可再次重偏向。
- 3.当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
- 批量撤销(bulk revoke)
- 当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
- 注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时
- 应用场景
- 在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
- 自旋优化
- 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
- 锁粗化
- 假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
- 锁消除
- 锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
- 逃逸分析(Escape Analysis)
- 逃逸分析后对象的作用域在方法/线程内,并且可以进行标量替换,Java Hotspot编译器(JIT)能够将这个对象分配到CPU高速缓存上,而不是堆中。
- 方法逃逸(对象逃出当前方法)
- 线程逃逸((对象逃出当前线程)
- 锁膨胀流程图
- 偏向锁轻量级锁重量级误区
- 无锁——>偏向锁——>轻量级锁——>重量级2锁 (不存在无锁——>偏向锁)轻量级锁自旋获取锁失败,会膨胀升级为重量级锁 (轻量级锁不存在自旋)重量级锁不存在自旋 (重量级锁存在自旋 )
- 偏向锁轻量级锁重量级误区
- Lock
- AQS(AbstractQueuedSynchronizer)
- AQS原理分析
- AQS:是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
- AQS特性
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
- AQS两种资源共享方式
- Exclusive-独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
- AQS两种队列
- 同步等待队列
- 使用场景:主要用于维护获取锁失败时入队的线程
- 数据结构:一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列(阻塞)
- 原理:
- 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
- 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
- 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)
- 同步等待/条件队列原理图
- 同步等待队列
- AQS原理分析
- 2、条件等待队列
- 使用场景:调用await()的时候会释放锁,然后线程加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁
- 数据结构:条件队列是使用单向列表保存的,用nextWaiter来连接:
- 原理
- 调用await方法阻塞线程;
- 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)
- Condition接口方法
- 1、调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。2、调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。
- AQS(AbstractQueuedSynchronizer)
- AQS5个队列中节点状态
- 1、值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。2、CANCELLED,值为1,表示当前的线程被取消;3、SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;4、CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;5、PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
- 自定义同步器实现时主要实现方法
- 不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
- 1、isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。2、tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。3、tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。4、tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。5、tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- synchronized
- ReentrantLock
- 定义:ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。默认是非公平锁
- 特点
- 可中断 可以设置超时时间可以设置为公平锁 支持多个条件变量支持可重入
- synchronized和ReentrantLock的区别
- 1、synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;2、synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;3、synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;4、synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;5、在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显示释放锁;6、ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;7、synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;
- ReentrantLock原理分析
- 关注点
- 1. ReentrantLock加锁解锁的逻辑
- 2. 公平和非公平,可重入锁的实现
- 3. 线程竞争锁失败入队阻塞逻辑和获取锁的线程释放锁唤醒阻塞线程竞争锁的逻辑实现 ( 设计的精髓:并发场景下入队和出队操作)
- 原理图
- 关注点
- Semaphore(信号量)
- PV操作
- P操作
- ①S减1;
- ②若S减1后仍大于或等于0,则进程继续执行;
- ③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。
- V操作
- ①S加1;
- ②若相加后结果大于0,则进程继续执行;
- ③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。
- P操作
- Semaphore介绍:它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的
- Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量。
- Semaphore使用
- 构造器参数
- permits 表示许可证的数量(资源数)fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程。
- 常用方法
- acquire() 表示阻塞并获取许可tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞release() 表示释放许可int availablePermits():返回此信号量中当前可用的许可证数。int getQueueLength():返回正在等待获取许可证的线程数。boolean hasQueuedThreads():是否有线程正在等待获取许可证。void reducePermit(int reduction):减少 reduction 个许可证Collection getQueuedThreads():返回所有等待获取许可证的线程集合
- 主要前三个
- 构造器参数
- 应用场景
- 限流
- Semaphore原理分析
- 流程图
- PV操作
- 链接 CountDownLatch
- CountDownLatch介绍:CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。
- CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置。如果你需要一个重置count的版本,那么请考虑使用CyclicBarrier。
- 图例
- CountDownLatch的使用
- 构造器
- 常用方法
- // 调用 await() 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
- public void await() throws InterruptedException { };
- // 和 await() 类似,若等待 timeout 时长后,count 值还是没有变为 0,不再等待,继续执行
- public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
- // 会将 count 减 1,直至为 0
- CountDownLatch应用场景
- 用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成。
- CountDownLatch实现原理
- 底层基于 AbstractQueuedSynchronizer 实现,CountDownLatch 构造函数中指定的count直接赋给AQS的state;每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;这一步是由最后一个执行countdown方法的线程执行的。
- 而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。
- CountDownLatch与Thread.join的区别
- 其提供了比 join() 更加灵活的API。
- CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作Join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待
- CountDownLatch与CyclicBarrier的区别
- CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
- CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、isBroken(用来知道阻塞的线程是否被中断)等方法。
- CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
- CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执行。CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。
- CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果。
- CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现
- CyclicBarrier
- CyclicBarrier介绍:字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
- 图例
- CyclicBarrier的使用
- 构造方法
- // parties表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
- public CyclicBarrier(int parties)
- // 用于在线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景(该线程的执行时机是在到达屏障之后再执行)
- 重要方法
- //屏障 指定数量的线程全部调用await()方法时,这些线程不再阻塞
- // BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
- public int await() throws InterruptedException, BrokenBarrierException
- public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
- //循环 通过reset()方法可以进行重置
- 构造方法
- CyclicBarrier应用场景
- 可以用于多线程计算数据,最后合并计算结果的场景。
- 利用CyclicBarrier的计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景
- CyclicBarrier原理分析
- 关注点:
- 1.一组线程在触发屏障之前互相等待,最后一个线程到达屏障后唤醒逻辑是如何实现的
- 2.删栏循环使用是如何实现的
- 3.条件队列到同步队列的转换实现逻辑
- 原理图
- 关注点:
- CyclicBarrier介绍:字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
- 链接 ReentrantReadWriteLock(读写锁)
- 读写锁介绍
- 读写,写读,写写互斥,其他不互斥适用于读多写少
- 线程进入读锁的前提条件
- 没有其他线程的写锁
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
- 线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
- 读写锁特性
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。可重入:读锁和写锁都支持线程重入。锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。 不支持锁升级,读锁到写锁需要重新获取锁,防止死锁
- 注意事项
- 读锁不支持条件变量
- 重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
- 重入时支持降级: 持有写锁的情况下可以去获取读锁
- 锁降级:锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
- 设计的精髓
- 用一个变量如何维护多种状态:高16为表示读,低16为表示写
- 读写锁介绍
- 无锁
- 分类
- 阻塞队列
- BlockingQueue常用方法示例
- 队列满了无法添加元素,或者是队列空了无法移除元素时
- 抛出异常:add、remove、element
- 返回结果但不抛出异常:offer、poll、peek
- 阻塞:put、take
- 队列满了无法添加元素,或者是队列空了无法移除元素时
- 常见阻塞队列
- 常见阻塞队列详解 线程池对于阻塞队列的选择
- FixedThreadPool(SingleThreadExecutor 同理)选取的是 LinkedBlockingQueue
- CachedThreadPool 选取的是 SynchronousQueue
- ScheduledThreadPool(SingleThreadScheduledExecutor同理)选取的是延迟队列
- 选择策略
- 功能
- 比如是否需要阻塞队列帮我们排序,如优先级排序、延迟执行等。如果有这个需要,我们就必须选择类似于 PriorityBlockingQueue 之类的有排序能力的阻塞队列。
- 容量
- 第 2 个需要考虑的是容量,或者说是否有存储的要求,还是只需要“直接传递”。在考虑这一点的时候,我们知道前面介绍的那几种阻塞队列,有的是容量固定的,如 ArrayBlockingQueue;有的默认是容量无限的,如 LinkedBlockingQueue;而有的里面没有任何容量,如 SynchronousQueue;而对于 DelayQueue 而言,它的容量固定就是 Integer.MAX_VALUE。所以不同阻塞队列的容量是千差万别的,我们需要根据任务数量来推算出合适的容量,从而去选取合适的 BlockingQueue。
- 能否扩容
- 第 3 个需要考虑的是能否扩容。因为有时我们并不能在初始的时候很好的准确估计队列的大小,因为业务可能有高峰期、低谷期。如果一开始就固定一个容量,可能无法应对所有的情况,也是不合适的,有可能需要动态扩容。如果我们需要动态扩容的话,那么就不能选择 ArrayBlockingQueue ,因为它的容量在创建时就确定了,无法扩容。相反,PriorityBlockingQueue 即使在指定了初始容量之后,后续如果有需要,也可以自动扩容。所以我们可以根据是否需要扩容来选取合适的队列。
- 内存结构
- 第 4 个需要考虑的点就是内存结构。我们分析过 ArrayBlockingQueue 的源码,看到了它的内部结构是“数组”的形式。和它不同的是,LinkedBlockingQueue 的内部是用链表实现的,所以这里就需要我们考虑到,ArrayBlockingQueue 没有链表所需要的“节点”,空间利用率更高。所以如果我们对性能有要求可以从内存的结构角度去考虑这个问题。
- 性能
- 第 5 点就是从性能的角度去考虑。比如 LinkedBlockingQueue 由于拥有两把锁,它的操作粒度更细,在并发程度高的时候,相对于只有一把锁的 ArrayBlockingQueue 性能会更好。另外,SynchronousQueue 性能往往优于其他实现,因为它只需要“直接传递”,而不需要存储的过程。如果我们的场景需要直接传递的话,可以优先考虑 SynchronousQueue。
- BlockingQueue常用方法示例
- 并发设计模式
- 终止线程的设计模式
- Two-phase Termination(两阶段终止)模式
- 其中第一个阶段主要是线程 T1 向线程 T2发送终止指令,而第二阶段则是线程 T2响应终止指令。
- 使用场景
- 安全地终止线程,比如释放该释放的资源;
- 要确保终止处理逻辑在线程结束之前一定会执行时,可使用该方法;
- 避免共享的设计模式
- Immutability模式(不变性)
- 不变性(Immutability)模式。所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。
- Copy-on-Write模式(写时复制)
- 读多写少的场景
- Thread-Specific Storage(线程本地存储)
- 即使只有一个入口,也会在内部为每个线程分配特有的存储空间的模式。
- 多线程版本的if模式
- Guarded Suspension模式
- 允许多个线程对实例资源进行访问,但是实例资源需要对资源的分配做出管理。
- Balking模式
- 一个线程发现另一个线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。
- 多线程分工模式
- Thread-Per-Message 模式
- 为每个任务分配一个独立的线程,
- Worker Thread模式
- 线程池
- 生产者 - 消费者模式
- 终止线程的设计模式
synchronized的使用:synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用
ABA 问题
常见阻塞队列
Java语言的内置管程synchronized
图例
图例
原理图
64位JVM下的对象结构描述
锁膨胀流程图
BlockingQueue常用方法示例
同步等待/条件队列原理图
JMM与硬件内存架构的关系
流程图
插入策略
主存和工作内存交互8大原子操作
构造器
原理图
并发编程思维导图分享(包含详细知识点)相关推荐
- Java并发编程思维导图(知识点总结,含面试题整理)
我公布的所有思维导图笔记(后端技术知识点汇总) 目录链接 前言 继JVM思维导图之后又一肝作 年前刚好整理完毕,公开克隆分享. 本张思维导图优势 思维导图融入大量java并发编程知识的同时,覆盖大量 ...
- mindmaster思维导图 常用快捷键 详细使用技巧 做笔记你真的应该尝试一下这个软件
本文是众多使用技巧中其中的一篇, 全部使用技巧点击链接查看, 保证你收获满满 我主页中的思维导图中内容大多从我的笔记中整理而来,相应技巧可在笔记中查找原题, 有兴趣的可以去 我的主页 了解更多计算机学 ...
- 人力资源的必备鱼骨图|思维导图分享
任正非曾经说过:HR日常花时间最多的事务性工作,并不能发挥HR的核心价值,只是在浪费HR的时间和公司的资源. 这也就是为什么大部分HR工作很累,却仍然得不到老板重视的原因:在你的角色完全可以发挥更大的 ...
- Scratch编程思维导图_ADOPT法则_与非学堂出品
今天给学习Scratch的朋友们介绍一个Scratch编程任务时思维套路:ADOPT法则. Scratch编程思维导图ADOPT法则与非学堂出品标题 大家可以放大来看. ADOPT法则要点: A:分析 ...
- 操作系统——思维导图分享
文章目录 操作系统--思维导图分享 操作系统框架 第一章.操作系统概述 第二章.进程管理 第三章.内存管理 第四章.文件管理 第五章.输入/输出系统管理 操作系统--思维导图分享 说明: 我分享的思维 ...
- 关于主机的思维导图_「停课不停学」思维导图—初中语文全部知识点总结,高清可打印...
导读 思维导图的创始人东尼·博赞先生在读大学的时候,作为一名大一新生,在第一天上课时,好奇心就被略带傲慢的教授点燃了,因为他之前从来没见一个老师可以不用翻花名册点名,而且是第一次上课,全部是新生的情况 ...
- 初中各科思维导图汇总PDF,初中生物思维导图汇总,初中数学知识点
初中各科思维导图汇总PDF,初中生物思维导图汇总,初中数学知识点 链接:https://pan.baidu.com/s/1O0Kry18KYKy4FpBao1DnZw?pwd=k6o7 提取码:k6 ...
- Redis思维导图分享(包含详细知识点)
思维导图地址: Redis思维导图,点击跳转 思维导图内容 Redis Redis基础 Redis基本命令 遍历键 keys:全量遍历键,用来列出所有满足特定正则字符串规则的key,当redis数据量 ...
- python编程思维导图_用来梳理 Python 编程核心知识15张思维导图
原标题:用来梳理 Python 编程核心知识15张思维导图 小编这次在逛论坛的时候,无意中发现了一份python的武功秘籍,也就是一份思维导图,堪称业界经典! 思维导图可以有力地激发你的联想,通过一个 ...
最新文章
- MaxCompute助力OSS支持EB级计算力
- python类中方法的执行顺序-Python中实例化class的执行顺序示例详解
- word经常用到的技巧
- 一文读懂残差网络ResNet
- pandas(六) -- 合并、连接、去重、替换
- zzuli 2527: THE END IS COMING!!!!!(最小费用最大流)
- dm9000 driver 2
- 微服务架构 性能提升_如何通过无服务器架构提高性能
- PostgreSQL定时自动备份
- ODrive踩坑(四)AS5047P-SPI绝对值磁编码器,不需每次上电校准无刷电机,直接上电可用
- ASP.NET2.0(学习第一天)
- sqlserver一些对象的创建
- Python读取 csv文件中文乱码处理
- C语言 · 进制转换
- Linux内核memcpy的不同实现
- outlook统一签名模版设置
- 搜狗收录提交方法搜狗收录方法
- 名词解释isp_名词解释
- TexWorks中添加拼写纠察
- 显示Java国家列表
热门文章
- android sqlite数据库加密,(转)SQLite数据库的加密
- 4月2日lol服务器维护嘛,英雄联盟4月2日更新维护时间 英雄联盟4月2日维护到几点...
- Tcl与Design Compiler 04——综合库(时序库)和DC的设计对象
- mysql4种数据类型_MySQL(四)之MySQL数据类型
- java对数据结构的了解_数据结构对java有用吗
- 关于端口被占用的问题(以61440端口为例)
- 最终,还是没能成为吹牛逼的资本!(结束)
- 转---找女朋友的标准
- ¥300/天+午饭+全套周边!DevJoy 招募学生志愿者!
- 2017计算机省一试题及答案,2017年全国计算机等级考试一级练习试题及答案(一)...