并发编程
2022-03-11 00:28:20 1 举报
AI智能生成
并发编程关键知识
作者其他创作
大纲/内容
基础概念整理
为什么需要并发
充分利用多核CPU的计算能力
方便业务拆分,提示系统的并发能力和性能
并发编程的缺点:内存泄漏、死锁、线程安全、上下文切换等
并发编程3要素
原子性:一批操作要么全部成功要么全部失败
可见性:一个线程对共享变量的修改,其它线程能立即看到(synchronize、volatile)
有序性:指令重排序,需按照代码定义的先后顺序执行
线程安全出现的原因和解决方法
线程切换导致的原子性问题:Atomic开头的原子类、synchronize、lock方法解决
变量缓存导致的可见性问题:synchronize、volatile、lock方法解决
编译优化带来的有序性问题:happens-before原则
线程与进程的区别
根本区别:线程是处理器调度和执行的基本单位,进程是操作系统资源分配的基本单位
资源开销:进程拥有自己独立的代码和数据空间(程序上下文),切换有较大开销;同一类线程共享代码和数据空间,有自己独立的运行栈和程序计数器PC,线程切换开销小
包含关系:进程可包含一个或多个线程
内存分配:同一线程共享地址空间和资源;进程之间的空间和资源是相互独立的
健壮性:一个线程奔溃会关闭整个进程;而一个进程关闭不会影响到其它进程
执行过程:每个进程有独立的程序入口、执行过程、程序出口;线程不能单独运行,由进程进行控制
上下文切换相关
什么叫做上下文切换:线程的CPU时间片使用完毕,进入就绪状态并保存相关的环境信息,线程从保存到加载这个过程就是一次上下文切换
上下文切换一般是计算密集型的,需要相当可观的CPU时间,频繁的切换意味着消耗大量的时间和资源
Linux系统相比其它操作系统,上下文切换的时间比较少
线程与线程池
死锁
概念:两个或两个以上的线程由于资源竞争或通信导致的一种阻塞现象
形成死锁的四个必要条件
互斥条件:线程对分配到的资源具有排他互斥性,资源只能被一个线程占用,直到被该线程释放
请求与保持条件:一个线程对资源的申请造成阻塞时,对已申请的资源占用保持不变
不可剥夺条件:线程对已获得的资源未使用完毕之前不可强行剥夺,只有自身使用完毕才释放
循环等待条件:发生死锁时,阻塞的线程之间会形成循环链路(死循环),造成永久阻塞
如何避免死锁(破坏其中一个必要条件)
破坏请求与保持条件:一次性的分配所有所需的资源
破坏不可剥夺条件:线程申请不到资源时,主动释放已申请的资源
破坏循环等待条件:按顺序申请资源,反序释放已申请的资源
创建线程的四种方式
继承Thread类:重写run方法-->new 该类-->调用start方法
实现Runable接口:重写run方法-->new该类-->Thread类接收该类-->调用线程的start方法
实现Callable接口:重写call方法-->new 该类-->用FutureTask接收该类-->用FutureTask作为参数给Thread类-->调用start方法
使用Executors工具类创建线程池:用Executors创建线程池-->用实现runable接口的类作为参数执行execute方法
为什么创建好线程调用的
是start方法而不是run方法
从线程状态来说,new一个Thread,会进入新建状态,调用start方法,进入就绪状态,会等待操作系统调度执行run方法里面的代码逻辑,是真正的多线程操作
直接调用run方法,会被主线程当做一个普通方法去执行,还是在主线程内,不是多线程编程
线程池
executor与threadpoolexecutor
创建线程的区别
阿里巴巴规约约定,不要使用executors创建线程
executors:堆积的请求处理队列会占用很大的内存、线程最大数是Integer.MAX_VALUE,会创建过多的线程
threadpoolexecutor只有一种通过自己指定参数的方式创建线程
线程池核心参数
核心线程数(corepoolsize):最小可同时运行的线程数量
最大线程数(maxnumpoollsize):线程池中最大的工作线程数量
工作队列(workqueue):新任务到来时当前运行的线程数达到核心线程数时,存放任务的存储单元
等待时间(keepAliveTime):线程池中数量大于核心线程数时,没新任务提交,核心线程数外的线程销毁之前的等待时间
等待时间单位(unit):等待时间的单位
线程工厂(threadFactory):为线程池提供创建新线程的线程工厂
饱和拒绝策略(handler):线程池任务队列超过最大定义数量且超过最大线程数的拒绝策略
线程池的拒绝策略
直接拒绝(abort),默认使用
伸缩队列使用策略(callerruns),不拒绝任务,调用自己的线程运行
直接丢弃(discard),不处理新任务,也不抛出异常
顺序丢弃(discardorders):丢弃最早未处理的任务
JAVA内存模型(JMM)
概念
https://blog.csdn.net/suifeng3051/article/details/52611310
定义了JVM在计算机内存(RAM)中的工作方式,JMM隶属于JVM
线程之间的通信:指明线程之间以何种方式进行信息交换。分为共享内存和消息传递两个方式
共享内存:线程之间共享相同的变量,通过读写内存中的公共状态隐式地完成通信
消息传递:线程之间不通过共享变量,必须通过明确的消息发送来显式进行通信,比如常见的wait和notify方法
java线程之间通过共享内存模型的方式进行通信的模型,就是JMM
java内存模型理论基础
JMM决定一个线程对共享变量的写入何时对另一个线程可见
抽象角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存在于主内存中,每个线程有私有的本地内存存储变量副本
线程本地内存:抽象化的概念,并发真实存在,涵盖了缓存、缓冲区、寄存器等
线程之间若需要通信,A线程需要将A的本地内存的变量刷新到主内存,B线程需要从主内存读取变量并更新到B的本地内存。JMM控制了主内存与本地内存的交互
JVM对JMM的内存实现
JMM将内存模型分为两部分
线程栈区:每个线程独有,线程之间不共享,包含本地变量、原始数据类型,线程直接变量的传递只能通过副本的方式
堆区:包含了java应用创建的所有对象信息,不管是哪个线程创建的,包括原始数据类型的包装类,无论对象属于成员还是方法,都会存储到堆区
本地变量
可能是原始类型,也有可能是引用类型
栈区中本地变量的引用类型,只保存了引用的指针,对象的存储还是处于堆区
成员变量:不管是原始类型还是引用类型,都会存储到堆区
成员方法:方法中的本地变量存储到栈区,尽管它们所属的对象在堆区
static变量、类信息存储在堆区
硬件内存架构与JMM桥接关系
级别由内而外为:CPU内置寄存器-->CPU高速缓存Cache-->RAM主存
CPU访问主存步骤:RAM主存-->CPU Cache-->CPU寄存器。写入主存与此相反
JMM与硬件结构并无直接的关联,是一种互相交叉的关系。
硬件内存并无栈、堆的关系,大部分数据都会存储到主存之中,少部分会存储在CPU缓存和寄存器中
共享对象的可见性
多个线程同时操作同一个变量时,如果没有及时且有效的同步刷新主存、缓存机制,会影响可见性,导致线程安全问题
使用volatile关键字可以保证变量直接从主存读取,变量的更改也能及时刷新到主存。基于内存屏障实现
线程竞争现象
多个线程同时修改共享对象,就产生了竞争现象
举例:共享变量v=1,线程A加1,线程B同时也从主存读取了变量v并加1,此时无论哪个线程先刷新到主存,都只会保留一个结果。此处可以看出,volatile不能保证线程安全
解决:使用synchronize关键字,保证同一个时刻只有一个线程进入竞态区,同时该关键字也能保证所有变量从主存读取,退出代码块时也会及时刷新主存。
支撑JMM的基础原理
指令重排序
内存屏障:是一条CPU指令,插入一条该指令,能禁止重排序,同时强制刷出各项CPU缓存
如果一个变量是volatile修饰的,JMM会在写入这个字段之后插入一条写入屏障指令;并在读这个变量前插入读取屏障指令。
happens-before原则:避免指令重排序等编译优化导致的并发异常,规定了两个操作需要严格遵守的顺序
并发关键字
synchronize
控制线程同步,保证同一代码块同一时刻只有一个线程执行
早期属于重量级锁,基于底层操作系统的Mutex-Lock实现,涉及到用户态到内核态的转换,时间成本高。JDK6后从JVM层面进行了优化,如自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术减少开销
修饰实例方法:锁住当前对象;修饰静态方法:锁定当前类,作用于当前类的所有实例(注意锁住类和锁住实例不互斥);修饰代码块:锁住指定的对象
使用举例:单例模式的double-check
底层原理:查看字节码反编译后的文件,发现使用synchronize关键字修饰的代码块进入之前,有monitorenter指令,离开代码块后有monitorexit指令,代表了获取锁和释放锁的过程(有两个monitorexit指令,其中一个是防止异常退出的)
可重入原理:维护一个计数器,线程每次获取锁都加1,释放时减1,直到0的代表锁未被持有,其它线程可以竞争获取锁。synchronize是可重入的
锁升级原理:锁对象头维护threadId字段,第一次获取锁时该字段为空,jvm让其持有偏向锁并将threadId置为当前线程ID;再次进入的时候如果threadID一致,可以直接使用该对象,否则升级为轻量级锁,并通过一定的自旋来获取锁;执行一定的自旋次数后仍没获取到锁,则会再次升级成重量级锁(目的:减少锁带来的性能消耗)
volatile
禁止指令重排序和保证内存变量可见性,提供happen-before保证
实践角度而言,与CAS共用,保证原子性,比如java.util.concurrent.automic包下的类,或加锁同步
可以创建一个volatile数组,但仅仅保护的是数组的引用,不对数组的元素内容进行保护
注意:修饰long和double变量可以保证原子性, 因为它们是64位的,所以读写分为两次,额外保证了原子性
实践:单例模式的double-check机制
Lock与AQS体系
lock
lock接口比synchronize提供了更具扩展性的锁操作,可允许更灵活的结构
优势:锁更加公平、可在等待锁的时候响应中断、可尝试(在一定时间内获取锁)、在不同的范围以不同的顺序释放锁
详解:提供了无条件、可轮询(tryLock)、定时的(tryLock带参数)、可中断的(lockInterrupt)、可多条件队列(newCondition)锁操作。另外Lock的实现大部分支持公平与公平操作
锁
悲观锁:总是假设最坏的情况,每次拿数据都认为别人会修改,所以都会上锁,别的线程都会阻塞。常见的有数据库的表锁、行锁、读锁、写锁等,synchronize的实现也说悲观锁
乐观锁:读数据不上锁,更新的时候会判断别人有没有更新过数据,如数据库的wirte_condition机制
CAS实现(CPU指令):三个操作数:内存位置V、预估原值A、新值B。从V中读取A相匹配,则处理器自动将该位置更新为B。失败会返回该值再次自旋
版本号机制:每更新一次都会带版本号,读出的版本会跟提交时的版本作比较,一致才能更新成功,失败丢弃或重试
AQS
基础概念
构建锁和同步器的框架,常见的FutureTask、Semaphore、ReentryLock、都是基于AQS
原理:如果被请求的共享资源空闲,则将当前请求线程置为有效线程,并将共享资源锁定。其余线程阻塞并放入CLH队列,等待唤醒
CLH:虚拟双向队列,仅节点间存储联系,FIFO
资源共享方式
独占:只有一个线程能执行,比如reentrantLock。
共享:多个线程同时执行,比如Semphore、countDownLatch、CylicBarrier、readWriteLock
semphore:可指定多个线程同时访问资源,构造参数定义线程数量,多于线程会阻塞等待
CountDownLatch:倒计数器,控制线程等待,让一个线程等待一段时间再执行
cyclicBarrier:循环栅栏,与countDownLatch类似,但提供了更强大的功能:让一组线程到达一个屏障(同步点)被阻塞,直到所有线程都到达屏障时,线程才被激活
并发容器
概念:通过协调多个线程调用同步容器的方法,保证线程安全操作的容器类型。一般来说是串行执行。常见的有vector、hashtable、collections.synchronizedSet等
concurrentHashMap
安全高效的hashMap实现,JDK6使用锁分段技术保证;JDK8采用CAS+synchronize来保证并发安全
synchronizedMap一次性锁定一整个hash表,同一时间只能有一个线程访问来保证线程安全,而concurrentHashMap则将hash表默认分为16个桶,一次只锁住一个hash桶保证
copyOnWriteArrayList
非复合场景下并发安全,多个迭代器修改列表时,写入时创建整个底层数据副本,原数组保持,保证读写并发进行
设计思想:1、读写分离;2、最终一致性;3、另外开辟空间解决冲突
缺点:1、写操作需要拷贝数组,比较耗资源;2、无法满足实时性
ThreadLocal
通过每个线程创建一个线程局部变量threadlocalMap的方式,以空间换时间,每个线程访问内部map,避免多线程之间资源共享的方式保证线程安全
使用场景:为每个线程创建一个JDBC-Connection、session管理
内存泄漏原因:threadLocalMap使用的key threadLocal是弱引用的,value是强引用。threadLocal没被外部引用的情况下会被GC回收,而value不会被回收。就会产生内存泄漏问题
内存泄漏解决方案:每次用完threadlocal,都调用remove()方法清除数据,特别是线程池的场景下
blockingQueue
支持两个附加操作的queue:1、当队列为空时,获取队列元素的线程阻塞直到队列非空;2、当队列已满时,添加元素的线程阻塞直到队列不满
使用场景:生产者—消费者模式
底层通过ReentrantLock和condition实现
0 条评论
下一页