并发编程
2019-02-20 09:43:59 59 举报
AI智能生成
并发编程【Java】
作者其他创作
大纲/内容
1 并发编程带来的挑战
1.1 上下文切換
1.1.1 时间片
即使是单核CPU也能运行多线程,多个线程会争抢CPU的执行权,也叫时间片
(通常是几十毫秒),CPU会通过时间片分配算法在之间来回切换,让用户
感觉这些程序像是在并行执行一样
(通常是几十毫秒),CPU会通过时间片分配算法在之间来回切换,让用户
感觉这些程序像是在并行执行一样
1.1.2 并行一定更快吗
相同的程序,并行不一定比串行快,因为CPU在由一个线程切换到另一个线程,需要
保留该线程当前的执行状态(比如执行到哪一行,有哪些变量和数据),在下次切换回
来继续执行该线程时可以恢复到原来的状态,这个保存和恢复会消耗额外的时间
保留该线程当前的执行状态(比如执行到哪一行,有哪些变量和数据),在下次切换回
来继续执行该线程时可以恢复到原来的状态,这个保存和恢复会消耗额外的时间
1.1.3 如何避免频繁的上下文切换
1.1.3.1 减低锁的粒度
对数据进行切分,分段id哈希取模,每个线程只操作一段数据,
就是concurrentHashMap1.7的分段锁的做法
就是concurrentHashMap1.7的分段锁的做法
1.1.3.2 CAS无锁化编程
synchronized的获取锁和释放锁,会引起上下文切换,可以用CAS操作
来代替原子性,但是频繁的cas也会消耗CPU
来代替原子性,但是频繁的cas也会消耗CPU
1.1.3.3 使用最少线程
线程的创建和销毁都要消耗系统资源,所以尽量使用池化技术来管理线程
同时也避免了CPU在大量线程之间切换的问题
同时也避免了CPU在大量线程之间切换的问题
1.2 死锁
如果出现死锁了,那么对于应用程序而言是灾难性的(无法响应)
如何避免死锁
避免一个线程获取多个锁
避免一个锁占用多个资源,这边可以借鉴ConcurrentHashMap分段锁的思想
避免获取不到锁就无限期等待
使用Lock.tryLock(Timeout)代替内部锁机制,一定时间内获取不到就返回
1.3 资源限制的挑战
软件资源
池化复用
数据库连接数
socket连接
硬件资源
带宽有限
再怎么并发编程,也无法使下载和上传速度超过带宽速度
CPU,内存有限
集群的方式来处理
硬盘读写速度有限
将数据放到内存中,如redis,elasticsearch
2 并发机制的底层实现原理
本地内存和线程安全
缓存行
CPU不会直接与内存交互,而是通过总线把数据读到自己的一个缓存行中
写缓冲区
CPU不会直接与内存交互,而是把要读取的数据更改
之后先写入自己的写缓冲区,随后刷新到内存
之后先写入自己的写缓冲区,随后刷新到内存
本地内存
这是虚拟出来的概念,实际上不存在,囊括了缓存行,写缓冲区等概念
线程安全问题
真实的场景中,多CPU来执行并发程序,不同的处理器来处理不同的线程,每个线程都拥有
自己处理器可见的本地内存,对于主内存i=1,俩个线程AB都从主内存中读取i=1到各自的本
地内存,做++的操作,最终刷回主内存出现i=2,线程安全问题由此产生,本质的原因就是多
线程都在各自的本地内存中操作共享变量,仅对自己可见而对其他线程不可见,而然他们什么
时候会把数据刷回主内存也是不可控的。
自己处理器可见的本地内存,对于主内存i=1,俩个线程AB都从主内存中读取i=1到各自的本
地内存,做++的操作,最终刷回主内存出现i=2,线程安全问题由此产生,本质的原因就是多
线程都在各自的本地内存中操作共享变量,仅对自己可见而对其他线程不可见,而然他们什么
时候会把数据刷回主内存也是不可控的。
volatile
概述
为了解决线程安全问题,保证共享变量的可见性,而被volatile修饰的变量在线程中
就是可见的,volatile能够保证共享变量能被一致性地更新
就是可见的,volatile能够保证共享变量能被一致性地更新
语义
写volatile共享变量会做俩件事
锁定缓存行
某个处理器将共享数据写入自己的写缓冲区(对应线程对本地内存的共享变量进行修改)时,会
使用缓存锁定其他也读取该共享变量的缓存行,让其他处理器不能访问该共享变量。
使用缓存锁定其他也读取该共享变量的缓存行,让其他处理器不能访问该共享变量。
刷新内存
缓存一致性
缓存一致性
该处理器将自己写缓冲区中的所有数据刷新到主内存(包括非volatile变量),由缓存一致性来保障
其他CPU重新读取数据(处理器通过总线嗅探其他处理器写缓冲区中的更改,一经发现就将自己的
缓存行设置为无效化,下次访问的时候就必须从主内存中读取)
其他CPU重新读取数据(处理器通过总线嗅探其他处理器写缓冲区中的更改,一经发现就将自己的
缓存行设置为无效化,下次访问的时候就必须从主内存中读取)
优化
由于缓存行为32字节或者64字节,因此为了避免缓存锁定多个共享资源
可以采取字节填充的方式来提高对象的并发性能
可以采取字节填充的方式来提高对象的并发性能
Synchronized
对象头
Mark Word
锁标志位等和锁有关系的信息
Class Meta Data
对象所属类的类元数据信息(字段,方法)
Array Length
对象为数组类型才有,记录数组长度
监视器Moniter
同步代码块
Moniterenter和Moniterexit,只能被一个对象持有
,如果其他对象来拿会进入阻塞队列
,如果其他对象来拿会进入阻塞队列
同步方法
方法修饰符会有ACC_Synchronized,原理跟上面差不多
锁优化
synchronized在JDK1.6已经得
到很大的提升了不会比volatile
差太多,主要是应用了如下的一些技术
到很大的提升了不会比volatile
差太多,主要是应用了如下的一些技术
锁膨胀
无锁
初始状态(没有任何线程访问过同步块)
偏向锁
大多数情况下,不会出现多线程并发访问共享变量的场景,而
是仅有少数的线程,还会出现同一个线程不断的访问共享变量,
如果使用锁释放,性能上非常不可取,所以针对并发强度小,
引入了偏向锁,在一个线程访问同步块会有如下操作。
是仅有少数的线程,还会出现同一个线程不断的访问共享变量,
如果使用锁释放,性能上非常不可取,所以针对并发强度小,
引入了偏向锁,在一个线程访问同步块会有如下操作。
1 )判断对象的Mark Word中
线程ID是否指的是当前的线程
如果是,直接进入同步块,
否则,执行步骤2
线程ID是否指的是当前的线程
如果是,直接进入同步块,
否则,执行步骤2
2) 如果对象头的MarkWord中的线程ID
不是指向当前线程,那么查看MarkDown
中的是否是偏向锁这个标志位是否是1,
如果是1,进行步骤3,如果不是则表示
是无锁化状态,CAS将MarkWord中的
线程ID指向当前线程,进入同步块。
不是指向当前线程,那么查看MarkDown
中的是否是偏向锁这个标志位是否是1,
如果是1,进行步骤3,如果不是则表示
是无锁化状态,CAS将MarkWord中的
线程ID指向当前线程,进入同步块。
3) 如果是1,说明是偏向锁,并且出现了锁
竞争,那么偏向锁升级为轻量级锁
竞争,那么偏向锁升级为轻量级锁
偏向锁的撤销
偏向锁总是偏向某个线程,如果线程挂了
那么偏向锁有个撤销机制,在一个安全节点
首先暂停偏向的线程,然后检查线程状态
如果线程挂了,那么将锁设置为无锁化
那么偏向锁有个撤销机制,在一个安全节点
首先暂停偏向的线程,然后检查线程状态
如果线程挂了,那么将锁设置为无锁化
轻量级锁
并发升级,偏向锁
升级成轻量级锁
升级成轻量级锁
如果说偏向锁是抹去同步
那么轻量级锁就是避免进入内核态
那么轻量级锁就是避免进入内核态
1) 首先把同步对象的MarkWord复制一份到当前线程栈帧的一块空间
并且使用CAS将同步对象的MarkWord更新为指向该空间的指针,如果
更改成功那么获取锁,否则就CAS自旋
并且使用CAS将同步对象的MarkWord更新为指向该空间的指针,如果
更改成功那么获取锁,否则就CAS自旋
2)获取锁的线程在执行完同步块释放锁时,CAS将同步对象中
的MarkWord去替换栈中原来保存的MarkWord,如果替换成
功则释放锁,失败说明有其他线程争用,锁升级成重量级锁
的MarkWord去替换栈中原来保存的MarkWord,如果替换成
功则释放锁,失败说明有其他线程争用,锁升级成重量级锁
重量级锁
排他锁
进入同步块必须获取moniter对象,退出释放moniter对象
如果获取时候获取不到,线程会进入阻塞队列,直到moniter
对象被释放,阻塞队列上的线程会开启新的一轮竞争
如果获取时候获取不到,线程会进入阻塞队列,直到moniter
对象被释放,阻塞队列上的线程会开启新的一轮竞争
自旋锁
轻量级锁失败之后,JVM为了避免线程在操作系统上挂起线程,对于互斥而言,性能影响最大的就是
阻塞的实现,挂起和恢复需要借助操作系统,会从用户态转化为内核态,很耗时间,有时候持有锁
可能就一会会,为了这么点时间恢复和挂起是得不偿失的,所以会让后面的线程先等一下,做一个
忙循环,这就是所谓的自旋锁
阻塞的实现,挂起和恢复需要借助操作系统,会从用户态转化为内核态,很耗时间,有时候持有锁
可能就一会会,为了这么点时间恢复和挂起是得不偿失的,所以会让后面的线程先等一下,做一个
忙循环,这就是所谓的自旋锁
适应性自旋
自旋时间会根据这个对象上一次的自旋时间来定义
锁粗化
同一个对象反复加锁解锁,会带来不必要的开销,JVM会扩大作用域
粗化到整个操作序列外部。只需要加锁一次即可
粗化到整个操作序列外部。只需要加锁一次即可
锁消除
如果检测到共享数据不可能存在竞争,那么执行锁消除,节省没必要的锁请求时间
Synchronized与volatile的区别
1)volatile是线程同步的轻量级实现,性能优于synchronized,
但是volatile只能用来修饰变量而synchronized可以用来修饰方法
但是volatile只能用来修饰变量而synchronized可以用来修饰方法
2)多线程访问volatile不会发生阻塞,但是synchronized可能发生阻塞
3)volatile只能保证可见性不能保证原子性,Synchronized俩者都能保证
4)volatile多用于多线程共享变量的可见性,而synchronized保证同步性
Atomic原子类
概念
具备原子或者原子操作性质的类
根据操作的数据类型可以把
JUC包中的原子类分成4种
JUC包中的原子类分成4种
基本类型
AtomicInteger
AtomicLong
AtomicBoolean
数组类型
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
引用类型
AtomicReference
AtomicStampedReference
AtomicMarkableReference
对象属性修改类型
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicStampedReference
使用Atomic原子类从某种程度也可以实现共享变量的线程安全
我们可以使用Atomic的自增或者赋值来实现线程安全
我们可以使用Atomic的自增或者赋值来实现线程安全
原理
主要是利用了CAS+volatile+native方法,
避免了synchronized的高开销,提升了执行效率
避免了synchronized的高开销,提升了执行效率
AQS
概念
AbstractQueuedSynchronizer
用来构筑锁和同步器的框架,能够简单
高效地构造应用广泛的同步器比如Reentrantlock
高效地构造应用广泛的同步器比如Reentrantlock
原理
线程请求共享资源,那么把该线程设置成有效的工作状态,并且把该共享资源设置为锁定
其他线程想要访问这个资源会被放入一套线程阻塞队列中CLH队列,实现锁分配
其他线程想要访问这个资源会被放入一套线程阻塞队列中CLH队列,实现锁分配
信号量Semaphore
允许多个线程同时拿某一个共享资源,通过设置许可值的获取和释放来控制
原理
分成俩种模式公平模式与非公平模式
前者是根据获取许可的顺序来控制队列
后者是抢占式的
前者是根据获取许可的顺序来控制队列
后者是抢占式的
获取许可
首先判断线程是否中断,若中断,直接抛出异常
然后,尝试获取许可,若失败则将线程加入等待队列
然后,尝试获取许可,若失败则将线程加入等待队列
尝试获取给完许可后的数量,如果许可
一旦不够,则阻塞后面的线程
一旦不够,则阻塞后面的线程
公平和非公平的区别在于,公平会优先判断当前队列是否有等待,
如果有就老老实实地进入等待队列,而非公平锁先试一把看能不
能拿到许可
如果有就老老实实地进入等待队列,而非公平锁先试一把看能不
能拿到许可
释放许可
首先尝试获取当前的许可数量,然后计算回收后的数量
如果说CAS值发生改变代表已经有许可释放了,马上调
用方法区释放阻塞的线程
如果说CAS值发生改变代表已经有许可释放了,马上调
用方法区释放阻塞的线程
倒计时器CountDownLatch
允许一个或多个线程一直等待,知道其他线程执行完毕再执行
典型用法
某个线程在开始运行前必须等待n个线程执行完毕,此时可以设置CountDownLatch为n
执行完一个就-1,直到0就可以唤醒主线程了,典型场景是跑服务之前必须启动n个组件
执行完一个就-1,直到0就可以唤醒主线程了,典型场景是跑服务之前必须启动n个组件
实现多线程开始执行任务的最大可能性,把多个线程放在一个起跑线上
然后设置CountDownLatch为1,主线程-1,然后同时唤醒多线程
然后设置CountDownLatch为1,主线程-1,然后同时唤醒多线程
死锁检测:多线程访问同一个共享变量,用来调整线程数,尝试出现死锁
原理
countdown方法
尝试释放锁,计数器-1,如果减完为0,那么表示
当前没有线程占用锁,那么需要唤醒被阻塞的线程
当前没有线程占用锁,那么需要唤醒被阻塞的线程
唤醒的时候还需要判断队列是否为null,为null直接退出
否则依次唤醒头结点先下一个节点相关的线程并且出队列
否则依次唤醒头结点先下一个节点相关的线程并且出队列
await
阻塞等待计数器,如果为0,则不执行阻塞
如果大于0,那么表示需要等待技术为0
当前线程封装Node队列,依次进入阻塞队列
循环尝试获取锁直到成功。
如果大于0,那么表示需要等待技术为0
当前线程封装Node队列,依次进入阻塞队列
循环尝试获取锁直到成功。
循环栅栏CyclicBarrier
功能类似于CountDownLatch,让多个线程到达一个屏障时被阻塞,知道最后一个线程进入屏障
才会被打开,默认需要赋拦截的线程数量。
才会被打开,默认需要赋拦截的线程数量。
与倒计时器的区别
CountDownLatch只能用一次,而CyclicBarrier可以通过reset功能
CountDownLatch:一个或多个线程等待,等其他n个完成之后,可以终止也可以等待
CyclicBarrier:多个线程,任意一个线程没完成,所有都必须等待
原理
基于Reentractlock与condition实现
在CyclicBarrier内部定义了一个lock对象,每个线程调用await,将拦截线程数-1
然后判断剩余拦截数是否为初始的parties,如果是,代表已经到同步点,尝试执行
下面的内容,否则进入条件队列等待
然后判断剩余拦截数是否为初始的parties,如果是,代表已经到同步点,尝试执行
下面的内容,否则进入条件队列等待
ReentrantLock
概念
基于AQS实现的显式锁,可重入锁
默认是非公平锁,可以通过构造设置为公平锁
原理
1)首先判断AQS中的state是否等于0,如果是
0的话,说明没有其他线程获取锁当前线程就可
以尝试获取锁,否则,执行步骤2
0的话,说明没有其他线程获取锁当前线程就可
以尝试获取锁,否则,执行步骤2
2) 如果state大于0,说明锁已经被获取了,
则需要判断获取锁的线程是否是当前线程
如果是的话,将state+1,然后把值更新
则需要判断获取锁的线程是否是当前线程
如果是的话,将state+1,然后把值更新
3)获取锁失败,则需要把当前线程写入队列中
lock和synchronized的区别
1)lock是接口,synchronized是关键字
2)synchronized隐式锁,lock显示锁,Synchronized
发生异常会释放锁,而lock一般我们是在finaliy中释放锁
发生异常会释放锁,而lock一般我们是在finaliy中释放锁
3)lock可以提高多线程的读写操作效率
读写锁
ReentrantReadWriteLock
维护了俩个锁,一个是读锁,一个是写锁
写锁可以获取读锁,但是读锁不可以获取写锁
读锁可以被多个线程持有,并且在持有阶段排斥写锁
可以先写锁然后读锁然后释放写锁,那么还保持读锁
线程池
重要参数
corepoolsize
核心池大小,创建线程之后,线程池数量为0,有任务则+1
直到数量到达corepoolsize后,把任务放在缓存队列中
直到数量到达corepoolsize后,把任务放在缓存队列中
maximumpoolsize
线程池中过最多创建多少个线程
keepalivetime
线程中没有任务执行,最多保持多久时间会终止,只有
线程数量大于corepoolsize这个机制才会生效
线程数量大于corepoolsize这个机制才会生效
WorkQueue
阻塞队列,要用来存放等待被执行的任务
ThreadFactory
线程工厂,用来创建线程
状态
running
线程池创建的初始状态
shutdown
调用shutdown方法,不再接受新任务,等待已有的任务执行完毕
stop
调用shutdownnow,不再接受新任务,尝试终止正在执行的任务
terminated
处于shutdown或者stop,所有工作线程都被销毁,缓存队列被清空
拒绝策略
abortpolicy
丢弃任务,抛出异常
discardpolicy
拒绝执行,不抛异常
discardOldestpolicy
丢弃缓存队列中最老的任务,尝试提交新任务
callerrunspolicy
有反馈机制,让任务提交速度变慢
种类
newsingleThreadExecutor
单线程的线程池,处理完一个任务接着下一个,若异常则起一个新的线程
newFixedThreadPool
指定数目的线程池,如果多于这个数目则加入缓存队列
newcachedThreadPool
不限数目的线程池,完全依赖于JVM能创建的线程数,可能出现内存不足
线程
实现线程的方式【3】
继承Thread
实现Runnable接口
futuretask+callable
可以有返回值
线程状态
新建
创建但是为启动的线程
就绪
等待CPU为他分配时间片
运行
运行中
等待
不会被分配时间片,分为有限期等待和无限期等待
阻塞
需要被唤醒
结束
已终止或者执行结束
sleep和wait区别
wait会放弃锁,需要通过notify或者notifyall唤醒
ThreadLocal
ThreadLocal为变量在每个线程中都创建了一个副本,
每个线程都可以访问自己内部的副本变量
每个线程都可以访问自己内部的副本变量
原理
每个线程内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量
这个变量用来存储实际的变量副本,键值为当前ThreadLocal变量,value
为变量副本,初始化的时候为空,此时会通过get或者set来初始化ThreadLocalMap
然后当前线程要使用副本变量时就取这个ThreadLocalMap
这个变量用来存储实际的变量副本,键值为当前ThreadLocal变量,value
为变量副本,初始化的时候为空,此时会通过get或者set来初始化ThreadLocalMap
然后当前线程要使用副本变量时就取这个ThreadLocalMap
0 条评论
下一页