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