导图社区 Java并发
这是一篇关于Java并发的思维导图,主要内容包括:锁,线程池,并发容器,基本概念。
编辑于2024-04-22 19:41:17Java并发
基本概念
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
进程
进程是程序的一次执行过程,是系统运行程序和作系统资源分配基本单位进程
堆
方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
线程
线程是处理器任务调度和执行的基本单位
程序计数器
1、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 2、在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
虚拟机栈
每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
并发
两个及两个以上的作业在同一 时间段内执行。
并行
两个及两个以上的作业在同一 时刻 执行。
容易导致内存泄漏、死锁、线程不安全
同步
发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步
调用在发出之后,不用等待返回结果,该调用直接返回。
线程安全
1、线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。 2、线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
创建线程
继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture
线程的生命周期和状态

新建(NEW)
new Thread() 该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。
就绪(Runnable)
调用了 start()方法之后,该线程处于就绪状态。 Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
运行(Running)
处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞(Blocked)
同步阻塞(Blocked)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
等待阻塞(WAITING)
1.Object#wait() 且不加超时参数 2. Thread#join() 且不加超时参数 3. LockSupport#park(), JVM 会把该线程放入等待队列(waitting queue)中。
超时等待阻塞(TMME-WAITING)
1、Thread#sleep() Object#wait() Thread#join() 并加了超时参数 2、 LockSupport#parkNanos() 5. LockSupport#parkUntil() 3、发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。 当 sleep()状态超时、 join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
死亡(Dead/TERMINATED)
1、正常结束run()或 call()方法执行完成。 2、异常结束 线程抛出一个未捕获的 Exception 或 Error。 3、调用 stop直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
线程上下文切换
上下文:线程在执行过程运行条件和状态(程序计数器,栈信息) 发生情况: 1、主动让出 CPU --- 调用了 sleep(), wait() 2、时间片用完、 3、调用了阻塞类型的系统中断(请求 IO,线程被阻塞) 会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。
线程死锁
死锁四个必要条件
1、互斥条件:该资源任意一个时刻只由一个线程占用。 2、请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 3、不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 4、循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
如何预防死锁
1、破坏互斥条件 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问) 2、破坏请求与保持条件 一次性申请所有的资源。 3、破坏不剥夺条件 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 4、破坏循环等待条件 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 5、锁排序法:(必须回答出来的点) 指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,在多线程条件下,如何避免死锁? 通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。 6、使用显式锁中的ReentrantLock.try(long,TimeUnit)来申请锁
如何排查
图形化:jconsole
jps -l 查出程序进程号,jstack 进程编号
JMM
为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。
定义
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。 不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
背景
Java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
CPU和缓存一致性
当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
处理器优化
处理器可能会对输入代码进行乱序执行处理
指令并行重排
编译器优化重排
内存系统重排
并发理论
as-if-serial规则
不管怎么重排序(编译器和处理器),单线程执行结果不能被改变。
happens-before规则
1、如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 2、两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM允许这种重排序。
程序顺序规则
一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则
对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则
对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性规则
如果A happens-before B,且B happens-before C,那么A happens-before C。
start()规则
如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
join()规则
如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
三大特性
原子性
原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。
可见性
可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。
有序性
即程序执行的顺序按照代码的先后顺序执行
如何解决并发问题
限制处理器优化和使用内存屏障
三大关键字
volatile
可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
可见性
对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
happens before原则
原理
生成汇编指令时会比普通的变量多出一个Lock指令,这个Lock指令就是volatile关键字可以保证内存可见性的关键
写的内存语义
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存。
读的内存语义
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
有序性
编译器在生成字节码时会通过插入内存屏障来禁止指令重排序 当第一个操作是volatile读时,无论第二个操作是什么都不能进行重排序。 当第二个操作是volatile写时,无论第一个操作是什么都不能进行重排序。 当第一个操作是volatile写,第二个操作为volatile读时,不能进行重排序。
内存语义的实现
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
synchronized
作用
主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
原子性
通过monitorenter和monitorexit这两个字节码指令来实现的。
有序性
synchronized修饰的代码,同一时间只能被同一线程访问,根据as-if-serial规则,保证单线程执行结果不会发生改变。
可见性
当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中。
写的内存语义
当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中。
读的内存语义
当线程获取锁时,JMM 会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。
使用方式
修饰实例方法
修饰静态方法
修饰代码块
原理

同步代码块
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 通过moniterenter、moniterexit 关联到到一个monitor对象,进入时设置Owner为当前线程,计数+1、退出-1。除了正常出口的 monitorexit,还在异常处理代码里插入了 monitorexit。
同步方法
同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。
java对象模型
对象头
Mark Word

Klass Point
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
字节对齐
锁升级

无锁
锁状态
001
偏向锁
锁状态
101
升级时机
只有单线程竞争锁
具体操作
线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。
好处
后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
偏向线程ID与当前线程ID不一致
发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID
竞争成功
表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁。
竞争失败
这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。
技术实现
一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID,无需再进入Monitor去竞争对象了。
轻量级锁
锁状态
00
升级时机
1、当关闭偏向锁功能 2、多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。
作用
有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质就是自旋锁CAS,在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。
自旋
而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
和偏向锁区别
争夺轻量级锁失败时,自旋尝试抢占锁 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
重量级锁
锁状态
10
升级时机
有大量的线程参与锁的竞争,冲突性很高
原理
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。
jdk1.6优化
自旋锁
Java6之前:默认启用,默认情况下自旋的次数是10次或者自旋线程数超过cpu核数一半
适应性自旋锁
自适应意味着自旋的次数不是固定不变的。而是根据:同一个锁上一次自旋的时间;拥有锁线程的状态来决定
锁消除
锁粗化
偏向锁
轻量级锁
final
作用
在引用变量为任意线程可见之前,该引用变量指向的对象的 final 域已经在构造函数中被正确初始化过了
重排序规则
1、在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 2、初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。
写 final 域的重排序规则
1、JMM 禁止编译器把 final 域的写重排序到构造函数之外。 2、编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。
读 final 域的重排序规则
1、在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作 2、在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
内存屏障
待补充
并发容器
List
CopyOnWriteArrayList
写时复制
Vector
Set
CopyOnWriteArraySet
底层使用CopyOnWriteArrayList
Map
ConcurrentHashMap
ConcurrentSkipListMap
跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找
HashTable
queue
ArrayBlockingQueue
底层是数组,一旦创建,容量不能改变。默认情况下不能保证线程访问队列的公平性,其并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。
LinkedBlockingQueue
底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE 。
PriorityBlockingQueue
是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。 并发控制采用的是可重入锁 ReentrantLock,队列为无界队列
SynchronousQueue
SynchronousQueue没有容量。与其他BlockingQueue(阻塞队列)不同,SynchronousQueue是一个不存储元素的BlockingQueue。只是它维护一组线程,这些线程在等待着把元素加入或移出队列。
线程池
定义
管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
优势
降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
创建方式
Executors
FixedThreadPool
固定线程数量的线程池
SingleThreadExecutor
只有一个线程的线程池
概使用的是无界的 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM
CachedThreadPool
可根据实际情况调整线程数量的线程池。
使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE
ScheduledThreadPool
给定的延迟后运行任务或者定期执行任务的线程池。
使用的无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
ThreadPoolExecutor
常见参数
corePoolSize
任务队列未达到队列容量时,最大可以同时运行的线程数量。
maximumPoolSize
任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue
新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
keepAliveTime
线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。
unit
keepAliveTime 参数的时间单位。
threadFactory
executor 创建新线程的时候会用到。
handler
饱和策略
AbortPolicy
抛出 RejectedExecutionException来拒绝新任务的处理。
CallerRunsPolicy
调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。
DiscardPolicy
不处理新任务,直接丢弃掉。
DiscardOldestPolicy
此策略将丢弃最早的未处理的任务请求。
处理任务的流程

如何设定线程池的大小
理论算法
Ncpu * Ucpu * (1 + W / C)
经验法
CPU 密集型任务(N+1)
I/O 密集型任务(2N)
Future 类
我有一个任务,提交给了 Future 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 Future 那里直接取出任务执行结果。
取消任务
判断任务是否被取消
判断任务是否已经执行完成
获取任务执行结果
缺点
不支持异步任务的编排组合 获取计算结果的 get() 方法为阻塞调用
CompletableFuture

解决Future的缺点
优点
提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力
CompletionStage
接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。
锁
基本概念
悲观锁
每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
优点
悲观锁的开销是固定的
缺点
激烈的锁竞争会造成线程阻塞
大量阻塞线程会导致系统的上下文切换 增加系统的性能开销
悲观锁还可能会存在死锁问题
适用场景
多写场景,竞争激烈(避免频繁失败和重试影响性能)
乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了
优点
不存在锁竞争造成线程阻塞
不会有死锁的问题
缺点
如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试
适用场景
多读场景,竞争较少(可以避免频繁加锁影响性能)
实现方案
版本号机制
CAS 算法
Unsafe类提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong
ABA 问题
循环时间长开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
ThreadLocal
作用
ThreadLocal类主要解决的就是让每个线程绑定自己的值
原理
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
Hash 算法
每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647 。这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
Hash 冲突
内存泄露问题
原因
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
解决方案
使用完 ThreadLocal方法后最好手动调用remove()方法
AQS
AQS 就是一个抽象类,主要用来构建锁和同步器。
核心思想
1、AQS 开放出 state 字段,让子类可以根据 state 字段来决定是否能够获得锁,对于获取不到锁的线程 AQS 会自动进行管理; 2、AQS 底层是由同步队列 + 条件队列联手组成,同步队列管理着获取不到锁的线程的排队和释放
state
AQS中 维护了一个volatile int state(代表共享资源)volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有
CLH队列

结构
类似一个链表队列,所有请求获取锁的线程会排列在链表队列中,自旋访问队列中前一个节点的状态。当一个节点释放锁时,只有它的后一个节点才可以得到锁。CLH 锁本身有一个队尾指针 Tail,它是一个原子变量,指向队列最末端的 CLH 节点。每一个 CLH 节点有两个属性:所代表的线程和标识是否持有锁的状态变量。
原理
当一个线程要获取锁时,它会对 Tail 进行一个 getAndSet 的原子操作。该操作会返回 Tail 当前指向的节点,也就是当前队尾节点,然后使 Tail 指向这个线程对应的 CLH 节点,成为新的队尾节点。入队成功后,该线程会轮询上一个队尾节点的状态变量,当上一个节点释放锁后,它将得到这个锁。
优点
性能优异,获取和释放锁开销小。
CLH 的锁状态不再是单一的原子变量,而是分散在每个节点的状态中,降低了自旋锁在竞争激烈时频繁同步的开销。在释放锁的开销也因为不需要使用 CAS 指令而降低了。
公平锁
先入队的线程会先得到锁。
实现简单,易于理解
扩展性强
缺点
自旋操作,当锁持有时间长时会带来较大的 CPU 开销
功能单一,不能支持复杂的功能
CLH队列变体

AQS 将自旋操作改为阻塞线程操作
锁数据结构的改进
扩展每个节点的状态
SIGNAL
-1 表示该节点正常等待
PROPAGATE
-3 应将 releaseShared 传播到其他节点
CONDITION
-2 该节点位于条件队列,不能用于同步队列节点
CANCELLED
1 由于超时、中断或其他原因,该节点被取消
显式的维护前驱节点和后继节点
释放锁的节点会显式通知下一个节点解除阻塞
出队节点显式设为 null 等辅助 GC 的优化
原理图

共享锁
一把锁可以被多个线程同时获得。
独占锁
一把锁只能被一个线程获得。
ReentrantLock
是什么
实现了 Lock 接口,是一个可重入且独占式的锁,和synchronized相比 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
原理
基于AQS实现
非公平加锁流程

线程一加锁成功时

AQS内部数据

线程二加锁失败
CLH队列情况

线程三加锁失败

公平锁加锁过程
和非公平锁区别 1、不会直接CAS state尝试直接获取锁 2、state==0 先判断CLH队列中没有等待获取锁的节点,有的话会进入队列排队
解锁过程
释放锁的过程

等待队列数据

最终队列数据

Condition
实现原理

ReentrantReadWriteLock
ReentrantReadWriteLock 实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。
适用场景
读多写少的情况下,使用 ReentrantReadWriteLock 能够明显提升系统性能
线程持有读锁还能获取写锁吗
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
读锁为不能升级为写锁
写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。 另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。
Semaphore
可以用来控制同时访问特定资源的线程数量。
两种模式
公平模式
调用 acquire() 方法的顺序就是获取许可证的顺序,遵循 FIFO;
非公平模式
抢占式的。
原理
Semaphore 是共享锁的一种实现,它默认构造 AQS 的 state 值为 permits,你可以将 permits 的值理解为许可证的数量,只有拿到许可证的线程才能执行。调用semaphore.acquire() ,线程尝试获取许可证,如果 state >= 0 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 state 的值 state=state-1。如果 state<0 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。
CountDownLatch
作用
允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
原理
CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。直到count 个线程调用了countDown()使 state 值被减为 0,或者调用await()的线程被中断,该线程才会从阻塞中被唤醒,await() 方法之后的语句得到执行。
一次性
CyclicBarrier
让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
Atomic 原子类
基本类型
AtomicInteger
AtomicLong
AtomicBoolean
数组类型
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
引用类型
AtomicReference
AtomicStampedReference
对象的属性修改类型
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater