Java并发编程知识体系(最全最完整)持续更新
2020-11-11 22:10:57 10 举报
AI智能生成
Java 并发编程 知识体系
作者其他创作
大纲/内容
基础知识
线程基础
线程与进程区别
线程是程序的最小执行单元
进程是操作系统进行资源分配和调度的一个基本单位
关联:一个程序至少有一个进程,一个进程又至少包含一个线程。进程中的多个线程共享进程的资源
引入线程的目的:充分利用 CPU 资源,使其可以并行处理多个任务,减少时间消耗,提高效率
线程分类
用户线程 User Thread
一般是程序中创建的线程
守护线程 Daemon Thread
为用户线程服务的线程,当所有用户线程结束时才会被终止。如JVM的垃圾回收。
通过 Thread 的 setDaemon(true) 方法将一个用户线程变成守护线程
线程并发底层机制
管程
定义
所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发
实现
互斥实现
图解
使用锁
同步实现
图解
使用条件变量+等待队列
信号量
线程的生命周期
六种状态
新建状态 New
Thread 类
实现了 Runnable 接口
run 方法,无返回值
Runnable 接口
run 方法,无返回值,通过 Thread 类或线程池来使用
Callable 接口
作为 FutureTask 构造方法参数使用
call 方法,有返回值,且可以抛出异常
call方法实际是在 Runnable 的 run 方法中被执行的
就绪状态 Runnable
调用新建线程的 start() 方法
不一定会立即运行,可能需要等待 CPU 分配时间片
阻塞状态 Blocked
调用 Object 的 wait 方法后等待同步锁的状态
等待 Waiting
发生在调用以下几个方法时:
- 不带参数的 Object.wait()
- 不带参数的 Thread.join()
- LockSupport.park()
超时等待 Timed-Waiting
与 Waiting 状态不同在于不会一直等待,而是等待指定的时间
发生在调用以下几个方法时:
- Thread.sleep(long millis)
- Object.wait(long timeout)
- Thread.join(long timeout)
- LockSupport.parkNanos()
- LockSupport.parkUntil()
终结状态 Terminated
当线程运行完毕,即死亡
并发核心问题
原子性
线程对共享变量的操作是原子性操作,就是说该操作或多个操作,要么都成功要么都失败
对于复合操作而言,synchronized 关键字可以保证原子性,而 volatile 关键字不能
可见性
线程对共享变量的修改对其它线程立即可见
synchronized 和 volatile 关键字都能保证可见性
有序性
程序代码执行的结果不受JVM指令重排序的影响
由于 volatile 关键字可以禁止指令重排序,因此能保证有序性。
Lock 锁机制可以同时保证以上三个特性
并发问题根因
由于CPU,内存,I/O设备速率间的巨大差异
CPU 增加了缓存,以均衡与内存的速度差异;
可见性问题
图解
操作系统增加了进程、线程,以分时复用 CPU,
进而均衡 CPU 与 I/O 设备的速度差异;
进而均衡 CPU 与 I/O 设备的速度差异;
线程切换带来的原子性问题
图解
编译程序优化指令执行次序,
使得缓存能够得到更加合理地利用。
使得缓存能够得到更加合理地利用。
有序性问题
图解
锁基础知识
锁优化
减少锁的时间
减少锁的粒度
锁升级
公平锁、非公平锁
公平锁(Fair)
非公平锁(Nonfair)
ReentrantLock
默认是非公平锁
悲观锁、乐观锁
乐观锁
自旋锁
自旋锁尽可能的减少线程的阻塞
自旋锁的优缺点
锁的竞争不激烈
自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗
锁的竞争激烈
不适合使用自旋锁
自旋锁在获取锁前一直都是占用cpu做无用功
线程自旋的消耗大于线程阻塞挂起操作的消耗,
其它需要cup的线程又不能获取到cpu,造成cpu的浪费
其它需要cup的线程又不能获取到cpu,造成cpu的浪费
自旋锁时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理
自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能
自旋锁的开启
ABA问题
引入版本号,每次变量更新都把版本号加一
偏向锁
Java6引入的一项锁优化
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能
持有偏向锁的线程将永远不需要再进行同步
在物竞争情况下把整个同步都消除掉,连CAS操作都不做
当有另外一个线程去尝试获取这个锁的时候,偏向模式就结束了
适用场景
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用
一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作
jvm开启/关闭偏向锁
轻量级锁
由偏向所升级来的
偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的释放
悲观锁
重量级锁
Synchronized
可以把任意一个非NULL的对象当作锁
作用于方法时,锁住的是对象的实例(this)
当作用于静态方法时,锁住的是Class实例
synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块
Synchronized是非公平锁
synchronized机制对锁的释放是隐式的
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
互斥锁、读写锁
互斥锁
一次最多只能有一个线程持有的锁
synchronized
Lock
会造成线程阻塞,降低运行效率,并有可能产生死锁、优先级翻转等一系列问题
读写锁
默认情况下也是非公平锁
ReadWriteLock
实现类ReentrantReadWriteLock
读取锁(ReadLock)
允许多个reader线程同时持有
写入锁(WriteLock)
最多只能有一个writer线程持有
允许一次读取多个线程,但一次只能写入一个线程
如果一个线程已经持有了写入锁,则可以再持有读锁。相反,如果一个线程已经持有了读取锁,则在释放该读取锁之前,不能再持有写入锁
使用场合
读取数据的频率远大于修改共享数据的频率
死锁
实例
避免死锁的几个常见方法
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
互斥
阻塞同步
临界区
一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性
锁
锁的类型
公平锁/非公平锁
可重入锁
独享锁/共享锁
互斥锁/读写锁
乐观锁/悲观锁
分段锁
偏向锁/轻量级锁/重量级锁
自旋锁
数据库锁
锁的级别
锁的粒度
加锁的方式
操作方式
使用方式
问题
活锁
死锁
优先级倒置
饥饿
非阻塞同步
wait-free算法
lock-free 算法
obstruction-free 算法
可重入
监视器
等待信号
条件变量
信号灯
双检查锁
并发基础
Java内存模型
共享变量之间的可见性,顺序性,原子性
内存模型规则
重排序
定义
为了程序的性能,处理器、编译器都会对程序进行重排序处理
条件
在单线程环境下不能改变程序运行的结果
存在数据依赖关系的不允许重排序
问题
重排序在多线程环境下可能会导致数据不安全
顺序一致性
多线程环境下的理论参考模型
为程序提供了极强的内存可见性保证
特性(原子、有序、可见)
一个线程中的所有操作必须按照程序的顺序来执行
所有线程都只能看到一个单一的操作执行顺序 ,不管程序是否同步
每个操作都必须原子执行且立刻对所有线程可见
as-if-serial
所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变
happens-before
定义
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,
而且第一个操作的执行顺序排在第二个操作之前
而且第一个操作的执行顺序排在第二个操作之前
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。
如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法
如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法
JMM中最核心的理论,保证内存可见性
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
6大happens-before规则
程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
传递性规则:这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。
对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
并发底层实现
AQS
AbstractQueuedSynchronizer,同步器,实现JUC核心基础组件
解决了子类实现同步器时涉及的大量细节问题,例如获取同步状态、FIFO同步队列
采用模板方法模式,AQS实现大量通用方法,子类通过继承方式实现其抽象方法来管理同步状态
CLH同步队列
FIFO双向队列,AQS依赖它来解决同步状态的管理问题
首节点唤醒,等待队列加入到CLH同步队列的尾部
同步状态获取与释放
独占式
获取锁
获取同步状态:acquire
响应中断:acquireInterruptibly
超时获取:tryAcquireNanos
释放锁
release
共享式
获取锁
acquireShared
释放锁
releaseShared
线程阻塞和唤醒
当有线程获取锁了,其他再次获取时需要阻塞,当线程释放锁后,AQS负责唤醒线程
LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语
每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用
park()、unpark()
CAS(乐观锁)
Compare And Swap,整个JUC体系最核心、最基础理论
内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干
native中存在四个参数
缺陷
循环时间太长,CPU开销大
解决方案
让JVM支持处理器提供的pause指令
pause指令能让自旋失败时cpu睡眠一小段时间再继续自旋
CAS多与自旋结合。如果自旋CAS长时间不成功,会占用大量的CPU资源
只能保证一个共享变量原子操作,而不能保证整个代码块的原子性
解决方案1
使用JDK 1.5开始就提供的AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS操作
解决方案2
使用锁。锁内的临界区代码可以保证只有当前线程能操作
ABA问题
解决方案
版本号、标记位
AtomicMarkableReference
基于时间戳
AtomicStampedReference
如果需要解决 ABA 问题,改用传统的 synchronized 可能会比原子类更高效。
并发基础实现
synchronized
同步、重量级锁
原理
基于AQS实现,共享锁
synchronized创建一个内存屏障,保证所有结果都刷新到主存中去,保证了操作的内存可见性
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
实现机制
Java对象头
synchronized的锁就是保存在Java对象头中的
包括两部分数据
Mark Word(标记字段)
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
包括
哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳
Klass Pointer(类型指针)
monitor
Owner
初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
原理
协作
互斥执行
具体实现
同步代码块
采用monitorenter、monitorexit指令显式的实现。
每一个对象都有一个monitor,一个monitor只能被一个线程拥有
每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。
只有拥有相应对象的monitor的线程才能执行monitorexit指令。
同步方法
使用ACC_SYNCHRONIZED标记符隐式的实现。
锁对象与用法
同步代码块
通常将外界资源作为锁的对象
synchronized(obj) {
// 同步操作代码
}
// 同步操作代码
}
用于保护外界资源不被多个线程并发修改
与同步方法比较而言,使用同步代码块的好处在于其他线程仍可以访问同步代码块以外的代码
同步方法
锁的对象是当前对象this
public synchronized void test() {
// 同步操作代码
}
// 同步操作代码
}
用于保护对象属性值不会被多个线程并发修改
同步静态方法
锁的对象是类的Class对象
public static synchronized void test() {
// 同步代码
}
// 同步代码
}
用于保护类的静态属性值不会被多个线程并发修改
锁优化
自旋锁
该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁(循环方式)
自旋字数较难控制(-XX:preBlockSpin)
存在理论:线程的频繁挂起、唤醒负担较重,可以认为每个线程占有锁的时间很短,线程挂起再唤醒得不偿失
缺点
自旋次数无法确定
适应性自旋锁
自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获取锁
轻量级锁
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
通过CAS来获取锁和释放锁
性能依据
对于绝大部分的锁,在整个生命周期内都是不会存在竞争的
缺点
在多线程环境下,其运行效率比重量级锁还会慢
偏向锁
为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
主要尽可能避免不必须要的CAS操作,如果竞争锁失败,则升级为轻量级锁
锁消除
若不存在数据竞争的情况,JVM会消除锁机制
判断依据
变量逃逸
缺点分析
缺点:
1、无法知道是否成功获取到锁
2、如果是多个线程需要同时进行读操作,一个线程读操作时其它线程只有等待
1、无法知道是否成功获取到锁
2、如果是多个线程需要同时进行读操作,一个线程读操作时其它线程只有等待
synchronized缺点
阻塞线程的时候线程的资源无法得到释放
容易造成锁死
Java其他显式锁
ReentrantLock 可重入锁
包括公平锁 FairSync、非公平锁 NonfairSync 两种实现,默认是非公平
可重入锁:是指当线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞;
公平锁:是指线程按请求获取锁的先后顺序获取锁(排队);
非公平锁:则允许在线程发出请求后立即尝试获取锁(抢占),如果尝试失败才进行排队等待
公平锁:是指线程按请求获取锁的先后顺序获取锁(排队);
非公平锁:则允许在线程发出请求后立即尝试获取锁(抢占),如果尝试失败才进行排队等待
可重入实现原理:
1、其中包含一个 Thread 类型的成员变量 exclusiveOwnerThread,表示已经获取到该锁的线程,以及一个计数器;
2、当线程获取锁成功后,该成员变量即赋值为该线程,同时计数器加一;
3、下一次再获取锁的时候,会先判断已经获取到锁的线程是否是当前线程,如果是则直接获取,同时计数器加一;
4、每一次释放锁时都将计数器减一,直到减为0,将 exclusiveOwnerThread 置为null,表示当前没有线程持有该锁。
1、其中包含一个 Thread 类型的成员变量 exclusiveOwnerThread,表示已经获取到该锁的线程,以及一个计数器;
2、当线程获取锁成功后,该成员变量即赋值为该线程,同时计数器加一;
3、下一次再获取锁的时候,会先判断已经获取到锁的线程是否是当前线程,如果是则直接获取,同时计数器加一;
4、每一次释放锁时都将计数器减一,直到减为0,将 exclusiveOwnerThread 置为null,表示当前没有线程持有该锁。
加锁解锁流程
加锁
加锁失败
加锁失败,创建node加到尾部,设置前置节点唤醒后置节点的状态,最后挂起
被唤醒重新加锁
被唤醒后,尝试state=1,若成功设置自己node为head,解除原head关系,加锁成功
解锁
设置state=0,唤醒后续节点,head节点的waitStatus重设回0,加锁成功
底层依赖
底层采用AQS实现,通过内部Sync继承AQS
在AQS框架下先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁
ReadWriteLock 读写锁
允许多个线程对资源同时进行读操作,或一个线程写操作,但读和写操作二者不能同时发生
ReadLock 读锁
属于共享锁实现
WriteLock 写锁
属于排他锁实现
支持公平性、非公平性,可重入和锁降级
锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁
StampedLock
是Java 8引入的锁,用于解决原有 ReadLock 的读操作阻塞写操作的问题。
支持三种模式的锁操作。分别是: 写(Writing)、读(Reading)、乐观读 (Optimistic Reading)。
其中写模式获取的是互斥锁,而读模式不是。
其中写模式获取的是互斥锁,而读模式不是。
由于它支持多种锁模式协调使用,因此并没有直接实现 Lock 或 ReadWriteLock 接口。
主要方法
锁的获取和释放
写锁:writeLock()/tryWriteLock()/tryWriteLock(time, unit)、unlockWrite(stamp)
(悲观)读锁:readLock()/tryReadLock()/tryReadLock(time, unit)、unlockRead(stamp)
乐观读锁:tryOptimisticRead()
unlock(stamp)
锁模式切换
tryConvertToWriteLock(stamp)
tryConvertToReadLock(stamp)
tryConvertToOptimisticRead(stamp)
Java锁实现支撑
Condition
Lock 提供条件Condition,对线程的等待、唤醒操作更加详细和灵活
内部维护一个Condition队列。当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部
LockSupport工具类
用于操作线程,是锁和同步类的基础。基于 Unsafe 中的 park、unpark 实现
用来代替 wait、notity、notifyAll 。
因为 LockSupport 对 park 方法和 unpark 方法的调用没有先后的限制,而 wait 方法则必须保证在 notify/notifyAll 之前被调用。
因为 LockSupport 对 park 方法和 unpark 方法的调用没有先后的限制,而 wait 方法则必须保证在 notify/notifyAll 之前被调用。
park()、parkNanos(long timeout)、parkUntil(long deadLine) 方法表示将当前线程挂起,后两个表示挂起一段时间
unpark(Thread thread) 方法取消指定线程 thread 挂起
JDK 1.6 后增加了一个方法参数 blocker,表示锁的对象,在通过线程监控和分析工具来定位问题时很有用
volatile
特性
volatile可见性:对一个volatile的读,总可以看到对这个变量最终的写
volatile原子性:volatile对单个读/写具有原子性(32位Long、Double),但是复合操作除外,例如i++;
线程每次都从主内存中读取变量,改变后再写回到主内存。其作用如下:
1、使变量在多个线程间具有可见性(volatile 变量不会被缓存到寄存器或其他对处理器不可见的地方)
2、禁止 JVM 对该变量的操作进行指令重排序(保证有序性)
1、使变量在多个线程间具有可见性(volatile 变量不会被缓存到寄存器或其他对处理器不可见的地方)
2、禁止 JVM 对该变量的操作进行指令重排序(保证有序性)
实现机制
内存屏障
插入内存屏障指令禁止编译器和CPU对程序进行重排序
内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
操作系统语义
主存、高速缓存(线程私有)缓存一致?
解决方案
通过在总线加LOCK#锁的方式
通过缓存一致性协议(MESI协议)
使用条件
1、对变量的写操作不依赖于变量的当前值,或者保证仅有一个线程对变量进行写操作;
2、该变量不会和其他状态变量一起被纳入不变性条件中;
3、访问变量不需要加锁。
2、该变量不会和其他状态变量一起被纳入不变性条件中;
3、访问变量不需要加锁。
应用场景
一写多读,保证变量可见性
开销较低的读写锁策略
将变量使用 volatile 关键字修饰,只在多个线程同时需要进行写操作时加锁,读操作不需要
加写锁方式
synchronized 关键字
使用 Lock
volatile VS synchronized
访问 volatile 变量不需要加锁,因此不会阻塞线程,因此它是比 synchronized 更轻量级的同步机制。
但是 volatile 不能完全替代 synchronized,因为它不能保证原子性,非线程安全,而加锁机制既可以确保可见性,又可以确保原子性。
但是 volatile 不能完全替代 synchronized,因为它不能保证原子性,非线程安全,而加锁机制既可以确保可见性,又可以确保原子性。
信号量Semaphore
Semaphore 可以允许多个线程访问一个临界区
相比lock,Semaphore可以容许多个线程同时并发执行
线程死锁分析
死锁产生的四个必要条件
- 互斥条件:资源不能被共享。即任一时刻一个资源只能给一个进程使用,其他进程只能等待,直到资源被占有者释放。
- 不可剥夺条件:已经分配的资源不能从相应的进程中被强制地剥夺,而只能由获得该资源的进程自愿释放。
- 请求和保持条件:已经得到资源的进程可以再次申请新的资源。
- 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。
预防死锁的方法
合理对资源进行动态分配,以避免死锁
破坏死锁产生的四个必要条件
并发工具类
CyclicBarrier
循环栅栏
通俗讲:所有线程都等待完成后才会继续下一步行动。
让一组线程到达一个屏障时被阻塞,直到最后所有到线程达,屏障才会开门,所有被屏障拦截的线程才会继续干活
应用场景
多线程结果合并的操作,用于多线程计算数据,最后合并计算结果的应用场景
底层采用ReentrantLock + Condition实现
CountDownLatch
倒数的门栓
一次性的,一个线程等待其他多个线程完成后,再执行
用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
与CyclicBarrier区别
CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待
CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier
内部采用共享锁来实现
Semaphore
信号量
控制线程对公共资源,按一定数量进行并发
从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动
信号量Semaphore是一个非负整数(>=1)。当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1
应用场景
通常用于限制可以访问某些资源(物理或逻辑的)的线程数目,比如数据库连接
内部采用共享锁实现,同步队列更新后的唤醒传播
Exchanger
交换者
在同步点,两个线程可以交换彼此的数据
允许在并发任务之间交换数据。具体来说,Exchanger类允许在两个线程之间定义同步点。当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中
Java并发集合
ConcurrentHashMap
CAS + Synchronized 来保证并发更新的安全,底层采用数组+链表/红黑树的存储结构
桶中的结构可能是链表,也可能是红黑树,红黑树是为了提高查找效率
整个看起来就像是优化过且线程安全的HashMap
整个看起来就像是优化过且线程安全的HashMap
重要内部类
Node
key-value键值对
TreeNode
红黑树节点
TreeBin
就相当于一颗红黑树,其构造方法其实就是构造红黑树的过程
ForwardingNode
辅助节点,用于ConcurrentHashMap扩容操作
sizeCtl
控制标识符,用来控制table初始化和扩容操作的
含义
负数代表正在进行初始化或扩容操作
-1代表正在初始化
-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
重要操作
initTable
ConcurrentHashMap初始化方法
只能有一个线程参与初始化过程,其他线程必须挂起
构造函数不做初始化过程,初始化真正是在put操作触发
步骤
sizeCtl < 0 表示正在进行初始化,线程挂起
线程获取初始化资格(CAS(SIZECTL, sc, -1))进行初始化过程
初始化步骤完成后,设置sizeCtl = 0.75 * n(下一次扩容阈值),表示下一次扩容的大小
put
核心思想
根据hash值计算节点插入在table的位置,如果该位置为空,则直接插入,否则插入到链表或者树中
真实情况较为复杂
步骤
table为null,线程进入初始化步骤,如果有其他线程正在初始化,该线程挂起
如果插入的当前 i 位置 为null,说明该位置是第一次插入,利用CAS插入节点即可,插入成功,则调用addCount判断是否需要扩容。若插入失败,则继续匹配(自旋)
若该节点的hash == MOVED(-1),表示有线程正在进行扩容,则进入扩容进程中
其余情况就是按照链表或者红黑树结构插入节点,但是这个过程需要加锁(synchronized)
get
步骤
table ==null ;return null
从链表/红黑树节点获取
扩容
多线程扩容
步骤
构建一个nextTable,其大小为原来大小的两倍,这个步骤是在单线程环境下完成的
将原来table里面的内容复制到nextTable中,这个步骤是允许多线程操作
链表转换为红黑树过程
所在链表的元素个数达到了阈值 8,则将链表转换为红黑树
红黑树算法
1.8 与 1.7的区别
1.7使用锁分段
1.8使用CAS
非阻塞的设计
更新时局部阻塞,读是非阻塞
优点
写安全,效率高
缺点
读不能保证是最近的更新
ConcurrentLinkedQueue
基于链接节点的无边界的线程安全队列,采用FIFO原则对元素进行排序,内部采用CAS算法实现
不变性
在入队的最后一个元素的next为null
队列中所有未删除的节点的item都不能为null且都能从head节点遍历到
对于要删除的节点,不是直接将其设置为null,而是先将其item域设置为null(迭代器会跳过item为null的节点)
允许head和tail更新滞后。这是什么意思呢?意思就说是head、tail不总是指向第一个元素和最后一个元素(后面阐述)
head的不变性和可变性
tail的不变性和可变性
精妙之处:利用CAS来完成数据操作,同时允许队列的不一致性,弱一致性表现淋漓尽致
ConcurrentSkipListMap
第三种key-value数据结构:SkipList(跳表)
SkipList
平衡二叉树结构
Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,
在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率
在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率
特性
由很多层结构组成,level是通过一定的概率随机产生的
每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法
最底层(Level 1)的链表包含所有元素
如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出
每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素
查找、删除、添加
ConcurrentSkipListSet
内部采用ConcurrentSkipListMap实现
COW
CopyOnWriteList
CopyOnWriteArrayList
从字面上看就是”写时复制“。原理是当需要对集合中元素进行增删改操作时首先复制一个副本,对副本进行操作
适用于“读多写少”的并发场景
阻塞队列
ArrayBlockingQueue
一个由数组实现的FIFO有界阻塞队列
ArrayBlockingQueue有界且固定,在构造函数时确认大小,确认后不支持改变
在多线程环境下不保证“公平性”
实现
ReentrantLock
Condition
LinkedBlockingQueue
基于链接,无界的FIFO阻塞队列
PriorityBlockingQueue
支持优先级的无界阻塞队列
默认情况下元素采用自然顺序升序排序,可以通过指定Comparator来对元素进行排序
二叉堆
分类
最大堆
父节点的键值总是大于或等于任何一个子节点的键值
最小堆
父节点的键值总是小于或等于任何一个子节点的键值
添加操作则是不断“上冒”,而删除操作则是不断“下掉”
实现
ReentrantLock + Condition
二叉堆
DelayQueue
支持延时获取元素的无界阻塞队列
应用
缓存:清掉缓存中超时的缓存数据
任务超时处理
实现
ReentrantLock + Condition
根据Delay时间排序的优先级队列:PriorityQueue
Delayed接口
用来标记那些应该在给定延迟时间之后执行的对象
该接口要求实现它的实现类必须定义一个compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
SynchronousQueue
一个没有容量的阻塞队列
应用
交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务
难搞懂,与Exchanger 有一拼
LinkedTransferQueue
链表组成的的无界阻塞队列
相当于ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、无界的LinkedBlockingQueues等的超集
预占模式
有就直接拿走,没有就占着这个位置直到拿到或者超时或者中断
LinkedBlockingDeque
由链表组成的双向阻塞队列
容量可选,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE
运用
“工作窃取”模式
原子操作类 (Atomic)
基本类型类
用于通过原子的方式更新基本类型
AtomicBoolean
原子更新布尔类型
AtomicInteger
原子更新整型
AtomicLong
原子更新长整型
数组类型
通过原子的方式更新数组里的某个元素
AtomicIntegerArray
原子更新整型数组里的元素
AtomicLongArray
原子更新长整型数组里的元素
AtomicReferenceArray
原子更新引用类型数组里的元素
引用类型
如果要原子的更新多个变量,就需要使用这个原子更新引用类型提供的类
AtomicReference
原子更新引用类型
AtomicReferenceFieldUpdater
原子更新引用类型里的字段
AtomicMarkableReference
原子更新带有标记位的引用类型
字段类型
如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类
AtomicIntegerFieldUpdater
原子更新整型的字段的更新器
AtomicLongFieldUpdater
原子更新长整型字段的更新器
AtomicStampedReference
原子更新带有版本号的引用类型
JDK1.8新增XXXAdder
LongAdder
多个计数器让对个线程去竞争,减少CAS的失败
最终值:所有Cell变量的value值累加后再加上base
Java线程池
好处
降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行
提高线程的可管理性
进行统一分配、调优和监控
Executor
Executors
静态工厂类,提供了Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 、Callable 等类的静态工厂方法
ThreadPoolExecutor
参数含义
corePoolSize
线程池中核心线程的数量
maximumPoolSize
线程池中允许的最大线程数
keepAliveTime
线程空闲的时间
unit
keepAliveTime的单位
workQueue
用来保存等待执行的任务的阻塞队列
使用的阻塞队列
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
PriorityBlockingQueue
threadFactory
用于设置创建线程的工厂
DefaultThreadFactory
handler
RejectedExecutionHandler,线程池的拒绝策略
分类
AbortPolicy:直接抛出异常,默认策略
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
DiscardPolicy:直接丢弃任务
线程池分类
newFixedThreadPool
可重用固定线程数的线程池
分析
corePoolSize和maximumPoolSize一致
使用“无界”队列
LinkedBlockingQueue
LinkedBlockingQueue
maximumPoolSize、keepAliveTime、
RejectedExecutionHandler
无效
RejectedExecutionHandler
无效
newCachedThreadPool
使用单个worker线程的Executor
分析
corePoolSize和maximumPoolSize被设置为1
使用LinkedBlockingQueue作为workerQueue
newSingleThreadExecutor
会根据需要创建新线程的线程池
分析
corePoolSize被设置为0
maximumPoolSize被设置为Integer.MAX_VALUE
SynchronousQueue 作为WorkerQueue
如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程 ,可能会耗尽CPU和内存资源
任务提交
Executor.execute()
ExecutorService.submit()
任务执行
执行流程
线程池调优
两种模型
线程池监控
ScheduledThreadPoolExecutor
继承自ThreadPoolExecutor
给定的延迟之后运行任务,或者定期执行任务
内部使用DelayQueue来实现 ,会把调度的任务放入DelayQueue中。DelayQueue内部封装PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序
Future
异步计算
Future
提供操作
执行任务的取消
查询任务是否完成
获取任务的执行结果
FutureTask
实现RunnableFuture接口,既可以作为Runnable被执行,也可以作为Future得到Callable的返回值
内部基于AQS实现
线程池的作用
1、减少资源消耗,通过重复利用线程池中已创建的线程,减少频繁创建,销毁线程带来的资源消耗
2、提供效应速度,当线程池中有空闲线程,任务到来时无需创建线程就能立即被执行
3、提供线程的可管理性,由线程池对池中的线程进行统一管理和监控,可以防止无限制创建线程造成的资源浪费
线程池大小配置
一般需要根据任务类型来配置线程池大小:
1、如果是 CPU 密集型任务,就需要尽量压榨 CPU,参考值可以设为 NCPU + 1
2、如果是 IO 密集型任务,参考值可以设置为 2 * NCPU
通过 Runtime.getRuntime().availableProcessors() 获得当前 CPU 个数
异步任务
定时任务
Timer & TimerTask 类
Timer:任务调度器,通过
TimerTask:实现了 Runnable 接口。表示需要调度的任务,里面有一个run方法定义具体的任务。
schedule(TimerTask task, long delay)等方法进行调度。
TimerTask:实现了 Runnable 接口。表示需要调度的任务,里面有一个run方法定义具体的任务。
Timer 缺点:内部是单线程,因此如果有异常产生,线程将退出,整个定时任务就会失败
线程池
ScheduledExecutorService,它是 ExecutorService 的子接口。弥补了 Timer 的缺陷
通过
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
和
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
等方法来实现定时任务调度
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
和
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
等方法来实现定时任务调度
异步任务
只需创建并启动一个线程执行该任务即可,如果需要获取执行结果,则用Callable。也可以使用线程池
Callable<V> 接口
表示一个带有返回结果的任务
通常通过 ExecutorService 来使用
Future<V> 接口
包装异步任务的返回结果
常用方法
boolean isDone():用于判断任务是否执行完成
V get():获取执行结果
FutureTask
实现了Future和Runnable接口,表示一个异步任务
CompletableFuture
JDK1.8新增的一个更好的异步编程工具
实现了Future和CompletionStage接口
并发问题分析
安全性问题
可见性问题
原子性问题
有序性问题
安全性问题发生的场景
存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一
数据。
数据。
活跃性问题
所谓活跃性问题,指的是某个操作无法执行下去
死锁
都拿着已获得的锁不放,并且等待别人持有的锁
活锁
多个线程在发现自己需要的锁无法获取时都谦让释放已经获得的锁
解决方案
在释放锁之前随机等待一定时间
饥饿
所谓“饥饿”指的是线程因无法访问所需资源而无法执行下去
的情况。
的情况。
由于资源获得的优先级不均,导致低优先级线程很难获得执行机会
解决方案
一是保证资源充足
二是公平地分配资源
使用公平锁
三就是避免持有锁的线程长时间执行
性能问题
使用“锁”要非常小心,但是如果小心过度,也可能出“性能问题”。
锁”的过度使用可能导致串行化的范围过大
锁”的过度使用可能导致串行化的范围过大
解决方案
使用无锁的算法和数据结构
例如线程本地存储 (Thread Local Storage, TLS)
写入时复
制 (Copy-on-write)
制 (Copy-on-write)
乐观锁
第二,减少锁持有的时间,互斥锁本质上是将并行的程序串行化,所以要增加并行度,一定
要减少持有锁的时间。
要减少持有锁的时间。
使用细粒度的锁
ConcurrentHashMap,它使用了所谓分段锁的技术
还可以使用读写锁,也就是读是无锁的
创建多少线程才是合适的
I/O 密集型程序
单核CPU:最佳线程数 =1 +(I/O 耗时 / CPU 耗时)
多核cpu:最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
CPU 密集型程序
多线程本质上是提升多核 CPU 的利用率
所以对于一个 4 核的
CPU,每个核一个线程,理论上创建 4 个线程就可以了,再多创建线程也只是增加线程切
换的成本。所以,对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是
最合适的。不过在工程上,线程的数量一般会设置为“CPU 核数 +1”
CPU,每个核一个线程,理论上创建 4 个线程就可以了,再多创建线程也只是增加线程切
换的成本。所以,对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是
最合适的。不过在工程上,线程的数量一般会设置为“CPU 核数 +1”
方法中的局部变量为什么不存并发问题
图解
局部变量是和方法同生共死的,一个变量如果想跨
越方法的边界,就必须创建在堆里。
越方法的边界,就必须创建在堆里。
每个线程都有自己独立的调用栈
如何使用面向对象的方法解决并发问题
一、封装共享变量
利用面向对象思想写并发程序的思路,其实就这么简单:将共享变量作为对象属性封装在内
部,对所有公共方法制定并发访问策略。
部,对所有公共方法制定并发访问策略。
二、识别共享变量间的约束条件
三、制定并发访问策略
1. 避免共享:避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。
并发设计模式
Immutability模式:如何利用不变性解决并发问题?
Copy-on-Write模式:不是延时策略的COW
线程本地存储模式:没有共享,就没有伤害
0 条评论
下一页