Java并发理论体系
2023-10-11 23:28:23 0 举报
AI智能生成
Java并发知识大集合
作者其他创作
大纲/内容
④线程间通信
管道
PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
join
遇上interrupt
当join线程被阻塞之后,对阻塞的线程执行interrupt时会抛出异常
join(Long)
父线程只等待一定时间,超时后返回执行父线程代码
join(Long)和sleep(Long)的区别
join底层使用的是wait会释放锁,而sleep不会释放锁
作用
让所属线程正常执行run()方法中的任务,而使当前线程无线期阻塞,直到所属线程销毁之后再执行后面的程序
ThreadLocal
初始值
设置初始值
重写initialValue方法可以设置所属线程的初始值
作用
每个线程绑定自己的值
原理简介
ThreadLocal 就是一个工具壳,真正存储数据的是ThreadLocalMap,它是一个静态内部类。ThreadLocalMap存储于Thread中的threadLocals变量中,在使用ThreadLocal的时候会被创建并赋值给线程。
实现原理
内存泄漏的理解?还存在疑问
InheritableThreadLocal
修改子线程的值
重写childValue(Object parentValue)方法可以设置所属线程的新值(包括初始值和父线程之后设置的值)
作用
ThreadLocal的基础上,使得子线程可以继承父线程的值(包括初始值和父线程之后设置的值)
原理简介
在创建线程时,在构造函数里面会调用init 方法,此方法由于在构造方法中被调用,所以可以通过currentThread获取到父线程,然后通过判断父线程的inheritableThreadLocals 属性是否为空来决定是否要为将父线程的上下文复制给子类
等待/通知机制
wait()
作用对象
当前线程
作用
将当前线程置入“阻塞队列”,在wait()阻塞(状态为WAITING或者TIMED_WAITING)并释放锁, 接收到通知后唤醒(状态为RUNNABLE)。可被interrupt,抛出异常
遇见interrupt
抛出异常并释放锁
wait(long)
等待一定时间,如果超时则自动唤醒线程
使用前置条件
拥有对象级别锁(该对象的monitor)
notify()
作用对象
随机取一个呈WAITING或者TIMED_WAITING状态的线程
作用
将线程置入“就绪队列”,和其它线程竞争锁成功后继续执行wait()后的程序
使用前置条件
拥有对象级别锁
notifyAll()
作用对象
所有呈WAITING或者TIMED_WAITING状态的线程
作用
将线程置入“就绪队列”,和其它线程竞争锁成功后继续执行wait()后的程序
使用前置条件
拥有对象级别锁
生产/消费者模式
多生产/多消费
有可能出现假死
解决方式:使用notify()唤醒可能会导致消费者唤醒消费者,此时出现假死,需要使用notifyAll()
操作栈(list)
生产者将资源置于操作栈中,消费者从操作栈中取资源
一生产/一消费
与同步或while相比较
性能更好
⑥Java并发集合
ConcurrentHashMap
重要内部类
key-value键值对
待整理
⑦Atomic
初衷
synchronized太重而volatile又不能保证原子性,所以cas是个不错的方法。Atomic便是用cas+volatile来实现的
基本类型类
AtomicInteger
AtomicLong
AtomicBoolean
通过原子的方式更新基本类型
引用类型
AtomicReference
AtomicStampedRerence
AtomicMarkableReference
数组
通过原子的方式更新数组里的某个元素
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
对象属性原子修改器
AtomicIntegerFieldUpdater
不能修改包装类Integer,如需使用需要AtomicReferenceFieldUpdater
AtomicLongFieldUpdater
不能修改包装类Long,如需使用需要AtomicReferenceFieldUpdater
AtomicReferenceFieldUpdater
原子类型累加器(高并发时使用)
DoubleAccumulator
可以为累加器提供非0的初始值,后者只能提供默认的0值。另外,前者还可以指定累加规则,比如不进行累加而进行相乘,只需要在构造LongAccumulator时传入自定义的双目运算器即可,后者则内置累加的规则。
DoubleAdder
是特殊的DoubleAccumulator
LongAccumulator
LongAdder
Striped64
父类
① Java多线程内存模型(JMM)
线程通信机制
消息传递
Java采用
内存共享
内存模型
重排序
处理器重排序
编译器重排序
硬件:不存在数据依赖的情况下可以重排序,存在控制依赖依旧可以重排序。但是在多线程的情况下这会导致问题的出现。
一句话说明:不改变单线程结果的情况下可以重排序
JMM:插入内存屏障指令来限制处理器重排序
JMM:禁止特定类型编译器重排序来限制编译器重排序
顺序一致性
是多线程环境下的理论参考模型,为程序员提供了极强的内存可见性保证
特性
一个线程中的所有操作必须按照程序的顺序来执行
所有线程都只能看到一个单一的操作执行顺序
每个操作都必须原子执行且立刻对所有线程可见
JMM的具体方针
未同步程序的情况
在不改变(正确同步)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门
JMM不保证单线程的操作会按照顺序执行,比如:重排序
JMM不保证对64位的long和double变量的写操作具有原子性
单线程的情况
JMM无需额外处理,编译器、runtime、处理器会共同保证单线程执行结果与顺序一致性模型的的结果一致
正确同步的多线程情况
JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证
主内存与工作内存交互协议
happens-before
定义:是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性
as-if-serial
定义:编译器和处理器不管怎么重排序,单线程的执行结果不能被改变。编译器、runtime和处理器都必须遵守
不能对存在数据依赖关系的操作做重排序
可以对控制依赖的操作重排序
JMM内存交互协议
八种基本操作协议
read、load、unlock、lock等
CPU缓存架构&缓存一致性协议
总线锁
背景
总线锁就是在总线上加锁,早期还没有多级缓存,所以就出现了CPU寄存器直接访问主存的情况,那么就可能会出现两个CPU同时访问一个变量的情况,如果两个CPU同时对这个变量进行写的操作,那么肯定就会出现问题,所以这个时候就出现了总线锁,同一时间的时候只能有一个CPU去访问这个变量。
把所有处理器对内存的访问以串行化的方式来执行。在任意时间点,最多只能有一个处理器可以访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性
总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其他的处理器和I/O设备执行内存的读/写
分支主题
现代的处理器通常是多CPU多核三级缓存,多核可使用L3共享内存。这就会存在可见性的问题,我们目前日常使用的CPU解决这个问题的方式是通过总线窥探机制。在窥探到修改操作时,执行一个动作以确保缓存一致性,可以是刷新缓存块或者使缓存块失效,这具体取决于缓存一致性协议,MESI便是其中一种。
缓存一致性
背景
现在我们的CPU大多数是多核多CPU的,如果仍然采用总线锁的话,就只能发挥单CPU单核的性能,所以就出现多核CPU多级缓存一致性协议如MESI解决方案
总线窥探
缓存一致性协议
MESI
MSI
。。。。
目录
较大的系统(>64处理器)
volatile
特性
可见性
对一个volatile变量的读,总是能看见对这个volatile变量最后的写入
原子性
对任意单个volatile变量的读写具有原子性,但是a++这种复合操作不具有原子性
有序性
对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性
实现机制
内存屏障
硬件
LOCK
Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对
CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁
读屏障/写屏障/全能屏障
JVM
四种内存屏障
作用:1.让数据立即回写到主内存
2.让此变量在其他副本上失效
内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
语义增强
严格限制编译器和处理器对volatile变量与普通变量的重排序
synchronized
为何而来
volatile只能解决单个字段的可见性和有序性但是无法保证复合操作的原子性,通过cas+volatile实现的atomic类只能处理单个字段的复合操作。synchronized是一个同步的重量级锁
实现原理
synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低
同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;
同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
synchronized用的锁是存在java对象头里面的
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
锁对象
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号中的对象
锁优化
锁一共有四种状态,级别从低到高是:无锁、偏向锁、轻量级锁、重量级锁。锁只能升级不能降级
偏向锁
无多线程竞争的情况下,以后该线程进入和退出同步块的时候不需要进行CAS操作来加锁和解锁
出现竞争才会释放锁(需要等到全局安全点),竞争成功获得偏向锁,竞争失败会升级锁
轻量级锁
引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
出现竞争才会释放锁(需要等到全局安全点),竞争成功获得偏向锁,竞争失败会升级锁
自旋锁
避免线程上下文切换,但是可能会导致过度消耗CPU。1.6之后使用自适应自旋锁。在轻量级锁竞争中有使用。
锁消除
判断依据
逃逸分析
若不存在数据竞争的情况下,JVM会消除锁机制
锁粗化
多个连续加锁、解锁操作一起,扩展成一个范围更大的锁,例如for循环内部获取锁
内存屏障
类别
JVM内存屏障
硬件内存屏障
能力
阻止屏障两边的指令重排序
刷新处理器缓存/冲刷处理器缓存
为什么要有JVM内存屏障功能?
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码
②并发基础
AQS
背景
Java层面管程的实现:AQS抽象类为基础的一系列同步器
特点
阻塞等待队列
共享/独占
公平/非公平
可重入
允许中断
AQS即是AbstractQueuedSynchronizer,他是实现JUC核心基础组件
采用模板方法模式,AQS实现大量通用方法,子类通过继承方式实现其抽象方法来管理同步状态
CLH同步队列
FIFO双向队列,AQS以来它来解决同步状态的管理问题
首节点唤醒,等待队列加入CLH同步队列的尾部
同步状态的释放与获取
独占式
获取锁
获取同步状态:acquire
响应中断:acquireInterrupt
超时获取:tryAccquireNanos
释放锁
release
共享式
获取锁
acquireShared
释放锁
releaseShared
线程阻塞和唤醒
当有线程获取锁了,其他再次获取时需要阻塞,当线程释放锁后,AQS负责唤醒线程
LockSupport
是用来创建锁和其他同步器的基本线程阻塞原语
每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进程中使用,则调用park()将会理解返回,否则可能阻塞。如果许可上不可用,则可调用unpark使其可用
park()、unpark()
CAS
背景
1,在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
2,一个线程持有锁会导致其它所有需要此锁的线程挂起。
3,如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
4,volatile是不错的机制,但是volatile不能保证原子性(如a++)。因此对于同步最终还是要回到锁机制上来。
解决方案
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
定义
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术,是整个JUC体系最核心、最基础的理论。
详细内容
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
缺点
ABA问题
使用版本号解决(注意AtomicInteger存在ABA问题)
AtomicStampedReference
循环时间过程导致CPU一直空转
只能保证一个共享变量的原子操作
⑤ 其他
原子性
方式
通过 synchronized 关键字保证原子性
通过 Lock保证原子性
通过 CAS保证原子性
可见性
方式
通过 volatile 关键字保证可见性
通过 内存屏障保证可见性
通过 synchronized 关键字保证可见性
通过 Lock保证可见性
通过 final 关键字保证可见性
原理
内存屏障
上下文切换
有序性
方式
通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized关键字保证有序性。
通过 Lock保证有序性。
⑨线程池
作用
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,
还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
创建线程池
通过Executors,创建一个预设好的线程池(阿里规约不建议,因为手动指定线程池的各项参数效果会更好)
手动创建:new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
各参数解析
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线
程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任 务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,
线程池会提前创建并启动所有基本线程。
maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并
且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如
果使用了无界的任务队列这个参数就没什么效果
keepAliveTime(线程空闲的时间)。线程的创建和销毁是需要代价的。线程执行完任务后不会立即销毁,而是继续存活一段时间:keepAliveTime。默认情况下,该参数只有在线程数大于corePoolSize时才会生效。
keepAliveTime的单位。TimeUnit
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通 常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工 厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原
则对元素进行排序。
ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设 置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线
程设置有意义的名字,代码如下:
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状 态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法 处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
AbortPolicy:直接抛出异常。
使用ExecutorService或者Executor接口接收
线程池处理流程
1. 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作
线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2. 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这
个工作队列里。如果工作队列满了,则进入下个流程。
3. 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程
来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
ThreadPoolExecutor执行execute
2. 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3. 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执
行这一步骤需要获取全局锁)。
4. 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。
1. 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤
需要获取全局锁)。
向线程池提交任务
submit()
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方 法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线
程一段时间后立即返回,这时候有可能任务没有执行完。
execute()
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。 通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
批量提交任务,并返回一个List<Future<T>,适用于要获得所有任务结果集的情况
schedule(Runnable command, long delay, TimeUnit unit)
达到给定的延时时间后,执行任务。这里传入的是实现Runnable接口的任务,因此通过ScheduledFuture.get()获取结果为null
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
该方法第三个参数表示在上一个个任务开始执行之后延迟多少秒之后再执行, 是从上一个任务开始时开始计算,但是还是会等上一个任务执行完之后,下一个任务才开始执行,最后的结果,就是感觉延迟失去了作用
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
当达到延时时间initialDelay后,任务开始执行。上一个任务执行结束后到下一次任务执行,中间延时时间间隔为delay。以这种方式,周期性执行任务。
关闭线程池
shutdownNow(慎用)
shutdownNow首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
shutdown
shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线
程
合理配置线程池
IO密集型任务
2*Ncpu
混合型任务
判断是否可拆分
优先级任务
使用优先级队列PriorityBlockingQueue来处理
建议使用有界队列
防止队列长度过长导致oom
CPU密集型任务
Ncpu+1
Executor框架
三大组成部分
任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。
任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的 ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口 (ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
框架结构
使用示意
框架组要成员
ThreadPoolExecutor
FixedThreadPool
分支主题
SingleThreadExecutor
使用单个线程,适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。即核心线程数和最大线程数都为1,默认情况下使用LinkedBlockingQueue,且参数设置为无界
CachedThreadPool
最大线程数为Integer.MAX_VALUE,核心线程数为0,使用不存储数据的队列,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
适用于需要多个后台线程执行周期任务,同时为了满足资源 管理的需求而需要限制后台线程的数量的应用场景
SingleThreadScheduledExecutor
适用于需要单个后台线程执行周期任务,同时需要保证顺
序地执行各个任务的应用场景
Future接口
Future接口和实现Future接口的FutureTask类用来表示异步计算的结果
ScheduledThreadPoolExecutor详解
DelayQueue是一个无界队列,所以ThreadPoolExecutor的maximumPoolSize在ScheduledThreadPoolExecutor中没有什么意义(设置maximumPoolSize的大小没有什么效果)
收藏
0 条评论
下一页