Java并发体系
2020-09-29 15:50:30 0 举报
AI智能生成
登录查看完整内容
并发体系脑图
作者其他创作
大纲/内容
J.U.C
Atomic
CAS
Compare And Swap 整个JUC体系最核心最基础的理论
内存值V,旧的预期值A,要更新的值B,当且仅当内存值V等于旧的预期值时才会将内存值V得值修改为B,否则什么都不干
Unsafe
Unsafe,是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据
var1:对象
var2:对象的地址
var4:预期值
var5:修改值
缺陷
循环时间长
如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。
只能保证一个共享变量的原子操作
ABA问题
解决方案
版本号
AtomicStampedReference
expectedReference:预期引用
newReference:更新后的引用
expectedStamp:预期标志
newStamp:更新后的标志
基本数据类型
用于通过原子的方式更新基本类型
AtomicBoolean
原子更新布尔类型
AtomicInteger
原子更新整型
AtomicLong
原子更新长整型
LongAdder
将之前单个节点的并发分散到各个节点的,这样从而提高在高并发时候的效率。
数组
通过原子的方式更新数组里的某个元素
AtomicIntegerArray
原子更新整型数组里的元素
AtomicLongArray
原子更新长整型数组里的元素
AtomicReferenceArray
原子更新引用类型数组里面的元素
引用类型
如果要原子更新多个成员变量,就需要使用这个原子更新引用类型提供的类
AtomicReference
原子更新引用类型
AtomicReferenceFieldUpdater
原子更新引用类型里的字段
AtomicMarkableReference
原子更新带有标记位的引用类型
字段类
如果我们需要某个类里的某个字段,那么就需要使用原子更新字段类
AtomicIntegerFieldUpdater
原子更新整型字段的更新器
AtomicLongFieldUpdater
原子更新长整型字段的更新器
AuomicStampedReference
原子更新带有版本号的引用类型
BlockQueue
BlockingQueue
BlockingQueue接口实现Queue接口,它支持两个附加操作:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。相对于同一操作他提供了四种机制:抛出异常、返回特殊值、阻塞等待、超时
四种机制
抛出异常
boolean add(E e);
boolean remove(Object o);
E element();
特殊值
boolean offer(E e);
E peek();
E poll();
阻塞等待
void put(E e) throws InterruptedException;
阻塞添加 所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。
E take() throws InterruptedException;
阻塞删除 阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)
超时
ArrayBlockingQueue
ArrayBlockingQueue,一个由数组实现的有界阻塞队列。该队列采用FIFO的原则对元素进行排序添加的。ArrayBlockingQueue为有界且固定,其大小在构造时由构造函数来决定,确认之后就不能再改变了。
入队
add(E e) :将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则抛出 IllegalStateException
offer(E e) :将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则返回 false
put(E e) :将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间
出队
poll() :获取并移除此队列的头,如果此队列为空,则返回 null
remove(Object o) :从此队列中移除指定元素的单个实例(如果存在)
take() :获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)
在多线程下不保证“公平性”
使用 ReentrantLock 和 Condition
PriorityBlockingQueue
同ArrayBlockingQueue队列类似,也是通过数组+ReentryLock实现,不同之处在于优先级队列是无界队列,容量可以扩容
分支主题
LinkedBlockingQueue
LinkedBlockingQueue是一个由链表实现的有界队列阻塞队列,但大小默认值为Integer.MAX_VALUE,所以我们在使用LinkedBlockingQueue时建议手动传值,为其提供我们所需的大小,避免队列过大造成机器负载或者内存爆满等情况。
LinkedBlockingQueue和ArrayBlockingQueue迥异
1.队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。
2.数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。
3.由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。
4.两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
LinkedBlockingDeque
LinkedBlockingDeque则是一个由链表组成的双向阻塞队列,双向队列就意味着可以从对头、对尾两端插入和移除元素,同样意味着LinkedBlockingDeque支持FIFO、FILO两种操作方式。
LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。
\"工作窃取\"模式
DelayQueue
Executor
线程池的优点
降低资源消耗
通过重复利用已经创建的线程降低创建和销毁线程造成的消耗
提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行
提高线程的可管理性
进行统一分配,调优和监控
Executors
静态工厂类,提供了Executors、ExecutorService、SchduledExecutorService、ThreadFactory、Callable等类的静态工厂方法
ThreadPoolExecutor
参数含义
corePoolSize
线程池中核心线程的数量。当提交一个任务时,线程池会新建一个线程来执行任务,直到当前线程数等于corePoolSize。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
maxmumPoolSize
线程池中允许的最大线程数。线程池的阻塞队列满了之后,如果还有任务提交,如果当前的线程数小于maximumPoolSize,则会新建线程来执行任务。注意,如果使用的是无界队列,该参数也就没有什么效果了。
keepAliveTime
线程空闲的时间。线程的创建和销毁是需要代价的。线程执行完任务后不会立即销毁,而是继续存活一段时间:keepAliveTime。默认情况下,该参数只有在线程数大于corePoolSize时才会生效。
unit
keepAliveTime的单位
workQueue
用来保存等待执行的任务的阻塞队列
使用的阻塞队列
SynchronoursQueue
threadFactory
用于设置创建线程的工厂
DefaultThreadFactory
Thread newThread(Runnable r);
handler
RejectedExecutionHandler,线程池的拒绝策略
分类
AborPolicy : 直接抛出异常。默认策略
CallerRunsPolicy : 用调用者所在的线程来执行
DiscardOldesPolicy ; 丢弃阻塞队列中最前的任务,并执行当前任务。
DiscardPolicy : 直接丢弃任务
当然我们也可以实现自己的拒绝策略,例如记录日志等等,实现RejectedExecutionHandler接口即可。
线程池分类Ecexutors工厂提供的
newFixedThreadPool
可重用固定线程数的线程池
分析
corePoolSize 和 maximumPoolSize一致
使用无界队列LinkedBlockingQueue
maximumPoolSize、keepAliveTime、RejectedExecutionHandler无效
newCachedThreadPool
newSingleThreadExecutor
corePoolSize被设置为0
maximumPoolSize设置为Integer.MAX_VALUE
SynchronousQueue 作为 workerQueue
如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,会不断添加到队列中,可能耗尽计算机资源
特性
并保证所有任务按照指定顺序(FIFO或优先级)执行。
当线程执行中出现异常,去创建一个新的线程替换之。
newScheduledThreadPool
任务提交
Executor.execute()
ExecutorService.submit()
可以获取该任务执行的Future
任务执行
执行流程
线程池监控
使用Executors创建的线程池不能进行监控
参数作用顺序
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满时
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,抛出异常,执行拒绝任务
参数计算
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
线程池数量
CPU密集型任务
CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。
IO密集型任务
一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
ScheduledThreadPoolExecutor
继承自ThreadPoolExecutor
给定的延迟之后运行任务,或者定期执行任务
Feture
异步计算
Future
提供操作
执行任务的取消
查询任务是否完成
获取任务的执行结果
原理
Future future = ExecutorService.submit(Runnable);Future future = ExecutorService.submit(Callable);
1、将提交的任务(Runnable或Callable)封装到FutureTask,有点装饰着模式的感觉
3、执行FutureTask.run(),在run方法中调用实际任务(call())
4、将call()返回的值暴露给FutureTask的属性(outcome)
5、因为FutureTask对象相对于主线程和新线程是共享的,所以主线程可以获取返回数据等操作
Tools
AQS
解决了子类实现同步器涉及大量细节的问题,例如获取同步状态,FIFO同步队列采用模板方法设计模式,AQS实现大量通用方法,子类通过集继承方式实现其抽象方法来管理同步状态
CLH同步队列
一个节点表示一个线程 构造函数
Node() // Used to establish initial head or SHARED marker
FIFO双向队列,AQS依赖它解决同步状态的管理问题
入列
CLH队列入列是再简单不过了,无非就是tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。 private Node addWaiter(Node mode)
在enq(Node node)方法中,AQS通过“死循环”的方式来保证节点可以正确添加,只有成功添加后,当前线程才会从该方法返回,否则会一直执行下去。private Node enq(final Node node)
出列
CLH同步队列遵循FIFO,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单,head执行该节点并断开原首节点的next和当前节点的prev即可,注意在这个过程是不需要使用CAS来保证的,因为只有一个线程能够成功获取到同步状态。
首节点唤醒,等待队列加入到CLH同步队列的尾部
同步状态获取与释放
独占式(同一时刻仅有一个线程持有同步状态)
获取锁
获取同步状态:acquire
响应中断: acquireInterruptibly
超时获取: tryAcquireNanos
释放锁
relesae
共享式
acquireShared
releaseShared
线程阻塞和唤醒
当有线程获取锁了,其他再次获取时需要阻塞,当线程释放锁后,AQS负责唤醒线程
java.util.concurrent.locks.LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语
每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用unpark使其可用
park()阻塞 unpark()唤醒 其实还是unsafe的方法
CyclicBarrier
它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。
底层采用:ReetrantLock + Condiction
核心方法
void reset()
通过breakBarrier()来终止所有的线程
过nextGeneration()进行更新换地操作,在这个步骤中,做了三件事:唤醒所有线程,重置count,generation。
int getNumberWaiting()
可以获得正在阻塞的线程数量
boolean isBroken()
阻塞的线程是否中断
应用场景
CyclicBarrier适用于多线程结果合并的操作,用于多线程计算数据,最后合并计算结果的应用场景。比如我们需要统计多个Excel中的数据,然后等到一个总结果。我们可以通过多线程处理每一个Excel,执行完成后得到相应的结果,最后通过barrierAction来计算这些线程的计算结果,得到所有Excel的总和。
更适合处理复杂业务场景。如果发生计算错误,可以重置计数器,并让线程重新执行一次。
CountDownLatch
完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
底层采用:共享锁
和CyclicBarrier区别
CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待
CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier
1:CountDownLatch是把主干线程挂起,在任务线程中进行倒数计数,直到任务线程执行完才唤醒主干线程继续执行;CyclicBarrier是把任务线程挂起,直到所有任务线程执行到屏障处再放行继续执行;2:CountDownLatch达到屏障放行标准后放行的是主干线程;CyclicBarrier达到屏障放行标准后放行的是任务线程,并且还会额外地触发一个达到标准后执行的响应线程
闭锁用于等待事件,而栅栏用于等待其他线程。
有条件,有前提的一组操作
Semaphore
信号量
一个控制访问多个共享资源的计数器
从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个acquire(),然后再获取该许可,每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是不实际的许可对象,Semaphore只对可用许可的号码进行计数,并采用相应的行动
void acquire() throws InterruptedException
获取许可证(开始阻塞)
boolean tryAcquire()
尝试获取许可证
void release()
释放许可证(开始执行)
流量控制,特别是公共资源有限的应用场景,如数据库连接
面试题 三个线程按顺序循环输出
Exchanger
可以在对中对元素进行配对和交换的线程的同步点。
Exchanger,它允许在并发任务之间交换数据。具体来说,Exchanger类允许在两个线程之间定义同步点。当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中。
exchange()
遗传算法
校对工作:如对账
Collections
ConcurrentHashMap
CAS+Synchronized 来保证并发更新的安全,底层采用数组+链表/红黑树的存储结构
核心内部类
Node
key-value键值对
对value和next属性设置了volatile同步锁(与JDK7的Segment相同),它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。
TreeNode
红黑树节点
并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装
TreeBin
就相当于一颗红黑树,其构造函数其实就是构造红黑树的过程
ForwardingNode
辅助节点,用于ConcurrentHashMap扩容
unsafe
sizeCtl含义
负数表示正在进行初始化或扩容操作
-1 表示正在初始化
-N 表示有N-1 个线程正在进行扩容操作
正数或者0表示hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
重要操作
initTable
ConcurrentHashMap初始化方法
只能有一个线程参与初始化,其他线程必须挂起
构造函数不做初始化过程,初始化正是在put操作触发
步骤
sizeCtl 0 表示正在进行初始化,线程挂起
初始化步骤完成后,设置sizeCtl = 0.75*n(下一次扩容阈值),表示下一次扩容的大小
transfer
多线程扩容
构建一个nextTable,其大小是原来大小的两倍。这个步骤是在单线程环境下完成的。这个单线程的保证是通过RESIZE_STAMP_SHIFT这个常量经过一次运算来保证的
如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发扩容的关键点;
如果这个位置是Node节点(fh=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上
如果这个位置是TreeBin节点(fh0),也做一个反序处理,并且判断是否需要untreefi,把处理的结果分别放在nextTable的i和i+n的位置上
遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍 ,完成扩容。
将原来table里面的内容复制到nextTable中,这个步骤是允许多线程操作的
遍历到的节点是forward节点,就向后继续遍历,再加上给节点上锁的机制,就完成了多线程的控制。
put
核心思想
根据hash值计算节点插入在table的位置,如果该位置为空,则直接插入,否则插入到链表或者树中不允许key或value为null值
真实情况较为复杂
如果插入的当前i位置null,说明该位置是第一次插入,利用CAS插入节点即可,插入成功,则调用addCount判断是否需要扩容。若插入失败则继续匹配(自旋)
若该节点的hash == MOVED(-1),表示有线程在进行扩容,则进入扩容进程中
其余情况就是按照链表或者红黑树结构插入,但是这个过程需要加锁(synchronized)
get
table==null ; return null;
从链表/红黑树节点获取
treeifyBin
所在链表的元素个数达到了阈值8且总元素个数大于64,则将链表转换为红黑树
红黑树算法
helpTransfer
size
采用类似LongAdder的拆分到volatile value 数组后再进行求和的算法
jdk 1.8 和 jdk 1.7 的区别
不采用segment而采用node,锁住node来实现减小锁粒度。
使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
sizeCtl的不同值来代表不同含义,起到了控制的作用。
ConcurrentLinkedQueue
ConcurrentSkipListMap
ConcurrentSkipListSet
Copy-On-Write容器
CopyOnWrite容器即写时复制的容器
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWriteArrayList
实现原理
添加元素的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的
并发容器用于读多写少的并发场景
黑白名单
注意事项
减少扩容开销
使用批量添加addBlackList方法
缺点
内存占用问题
数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
CopyOnWriteArraySet
并行
Fork/Join
一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每一个小任务结果够得到大任务结果的框架
分治 ——“分而治之”
fork分解任务,join收集数据
工作窃取
某个线程从其他队列窃取任务来执行
执行块的线程版主执行慢的线程执行任务,提高整个任务效率
队列要采取双向队列
核心类
ForkJoinPool
执行任务的线程池
ForkJoinTask
表示任务,用于ForkJoinPool的任务抽象
ForkJoinWorderThread
执行任务的工作线程
Parallel streams
获取任务结果的顺序:按照提交顺序获取结果
CPU高速轮询,耗资源,可以使用,但不推荐
遍历FutureList,内部wehile(true){if(){}else{ // 每次轮询休息1毫秒(CPU纳秒级),避免CPU高速轮循耗空CPUThread.sleep(1);}}
FutureTask
RunnableFuture接口继承自Future+Runnable:
1.Runnable接口,可开启单个线程执行。
2.Future接口,可接受Callable接口的返回值,futureTask.get()阻塞获取结果。
例如有十分耗时的业务但有依赖于其他业务不一定非要执行的
CompletionService
内部通过阻塞队列+FutureTask,实现了任务先完成可优先获取到,即结果按照完成先后顺序排序。
CompletableFuture
CompletableFuture实现了CompletionStage接口的如下策略:
为了完成当前的CompletableFuture接口或者其他完成方法的回调函数的线程,提供了非异步的完成操作。
没有显式入参Executor的所有async方法都使用ForkJoinPool.commonPool()为了简化监视、调试和跟踪,所有生成的异步任务都是标记接口AsynchronousCompletionTask的实例。
所有的CompletionStage方法都是独立于其他共有方法实现的,因此一个方法的行为不会受到子类中其他方法的覆盖。
CompletableFuture实现了Futurre接口的如下策略:
CompletableFuture无法直接控制完成,所以cancel操作被视为是另一种异常完成形式。方法isCompletedExceptionally可以用来确定一个CompletableFuture是否以任何异常的方式完成。
4个异步执行任务静态方法
static CompletableFuture supplyAsync(Supplier supplier)
static CompletableFuture runAsync(Runnable runnable)
其中supplyAsync用于有返回值的任务,runAsync则用于没有返回值的任务。Executor参数可以手动指定线程池,否则默认ForkJoinPool.commonPool()系统级公共线程池,
注意:这些线程都是Daemon线程,主线程结束Daemon线程不结束,只有JVM关闭时,生命周期终止。
流还是completableFutures的建议
1)如果进行的是计算密集型的操作,并且没有I/O,那么推荐使用Stream接口,因为实现简单,同时效率也可能是最高的
2)如果并行的工作单元还涉及等待I/O的操作,那么使用CompletableFuture灵活性更好。
Java并发体系
基础知识
并发线程的优缺点
为什么要用到并发(优点)
充分利用多核CPU的运算能力
方便业务拆分
并发编程的缺点
频繁的上下文切换
线程安全(常见的避免死锁方式)
易混淆的概念
同步VS异步
同步和异步是相对于操作结果来说,会不会等待结果返回
并发VS并行
并发:交替做不同事情的能力并行:同时做不同事情的能力
阻塞VS非阻塞
阻塞和非阻塞是相对于线程是否被阻塞
临界区资源
临界区资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源
线程的状态和基本操作
如何新建线程
继承Thread类
继承Runnable接口
继承Callable接口
线程状态的转换
NEW
RUNNABLE
WAITING
BLOCKING
TERMINATED
线程的基本操作
interrupt
运行时
轮询中断标志isInterrupted
阻塞时
抛出InterruptException异常,并清空中断标记。重入锁ReentrantLock,等待可中断,但Synchroinzd等待不可中断只有获取锁后才相应中断
sleep
不释放锁
join
等待子线程执行完成后再执行,释放锁
yield
让步(让出CPU执行时间),但不一定让出。不释放锁
wait
阻塞线程并释放锁资源
守护线程Daemon
当所有用户线程执行完毕后,守护线程也会死亡及时没有执行完毕,比如垃圾回收线程main函数所启动的线程也是用户线程
死锁
产生死锁的原因主要是
因为系统资源不足。
进程运行推进的顺序不合适。
资源分配不当等。
总结
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
死锁产生的四个必要条件
互斥条件
一个资源每次只能被一个进程使用。
请求和保持条件
一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不可剥夺条件
进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件
若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
如何避免
加锁顺序
加锁时限
死锁检测
jstack pid
JMM
线程通信机制
内存共享
java采用
synchronized + wait + notify
使用Condition控制线程通信lock + condition + await + signal
使用阻塞队列(BlockingQueue)控制线程通信
消息传递
内存模型
内存模型抽象结构
线程之间发消息
████ ╭ ────── ╮ ╔═════╗线程A ↔ │本地内存A │ → ║ ║↓ 发 ↓ ╰ ────── ╯ ║ 主 ║↓ 消 ↓ ║ 内 ║↓ 息 ↓ ╭ ────── ╮ ║ 存 ║线程B ↔ │本地内存B │ ← ║ ║\t████ ╰ ────── ╯ ╚═════╝
处理器和内存交互
█████ ╭ ────── ╮ ╔═════╗处理器A 写 → │写缓存区A │ 刷新 → ║█████║\t ↓ 读 ╰ ────── ╯ ║ █ 内 █ ║\t → → 读 →→读 → →读 → → ║█████║ ↑ 读 ╭ ────── ╮ ║ █ 存 █║处理器B 写→ │写缓存区B │ 刷新 → ║█████║█████ ╰ ────── ╯ ╚═════╝
重排序
为了程序性能,处理器,编译器都会对程序进行重排序
条件
在单线程环境下不能改变程序的运行结果
存在数据依赖关系的不允许重排序
问题
重排序在多线程环境下可能导致数据不安全
编译器优化的重排序
编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
指令级并行的重排序
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序
由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
顺序一致性
多线程环境下的理论参考模型
为程序提供了极强的内存可见性保证
一个线程中的所有操作必须按照程序的顺序来执行
所有线程都只能看到一个单一的执行顺序,不管是否同步
每个操作都必须原子执行且立刻对所有程序可见
happens-before
JMM中最核心的理论,使用happens-before的概念阐述操作间的内存可见性。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。(这里的两个线程可以在一个线程之内,也可以在不同线程之间)
理论
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等
规则
程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
管程锁定规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对于一个volatile域的写,happens-before于任意后续对这个volatile域的读。
线程启动规则: Thread对象的start()方法先行发生于此线程的每一个动作。
线程终止规则: 线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
传递性:若A happens-before B 、B happens-before C ,那么A happens-before C。
as-if-serial
语义:不管怎么重排序,(单线程)程序执行结果不能被改变。
在单线程环境下,指令执行的最终效果应当与其在顺序执行下的效果一致,否则这种优化便会失去意义
保护了单线程程序的执行顺序
happens-before 和 as-if-serial
happens-before保证了多线程程序执行结果不被改变。as-if-serial保证了单线程程序执行结果不被改变。
同步原语
Synchronized
同步,重量级锁(其实也没那么夸张的重)
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临时取,同时它还可以保证共享变量的内存可见性
锁对象(应用方式)
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
实现机制
Java对象头
synchronized的锁就是保存在Java对象头中的
包括两部分数据
Mark Work(标记字段:主要存储对象自身的运行数据)
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的的状态复用自己的存储空间
包括
哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳
Klass Pointer / Class Metadata Address(类型指针:对象指向它类元数据的指针)
类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
由来
在hotspot虚拟机中,对象在内存的分布分为3个部分
对象头
实例数据
存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
对齐填充
由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
monitor(进入和退出管程)
每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
ObjectMonitor:在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的
_WaitSet :保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象)
_EntryList:保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象)
_Owner:_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
锁优化
自旋锁
该线程等待一段时间,不会被立即挂起,看持有锁的线程是否很快释放锁(循环方式)
自旋次数较难控制(-XX:preBlockSpin)
存在理论:线程的频繁挂起,唤醒负担较重,可以认为一个线程占有锁的时间很短,线程挂起在唤醒得不偿失
自旋次数无法确定
适应性自旋锁
自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
锁消除
若不存在数据竞争的情况,JVM会消除锁机制
判断依据
逃逸分析
锁粗化
将多个连续的加锁,解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获取锁
轻量级锁
在没有竞争的前提下,轻量级锁使用CAS操作避免了使用互斥量的开销
通过CAS来获取锁和释放锁
性能依据
在绝大部分的锁,在整个生命周期内都是不会存在竞争的
如果存在竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢
目标
轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗
偏向锁
为了在无多线程的情况下尽量减少不必要的轻量级锁执行路径
主要尽可能避免不必要的CAS操作,如果竞争锁失败,则升级为轻量级锁
volatile
定义
Java语言规范允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。
实现过程
处理器不直接和内存进行通信,而是先将系统内存读取到CUP内部缓存再进行操作。
当声明了volatile变量进行写操作,jvm会向处理器发送一条LOCK指令,将这个变量所在的内存行的数据写回系统内存。
但是,就算是写回到内存,如果其他处理器缓存的值还是旧的,执行计算还有问题。
多处理器下,实现了缓存一致性协议,如果发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态。
当处理器进行修改时,会重新从系统内存中把数据读到处理器缓存。
可见性:对一个volatile的读,总是可以看到这个变量的最终的写
Thread1 和 Thread2 执行以下操作: Thread1 ★★ Thread2 r1 = i; ★★ r3 = i; r2 = r1 + 1; ★★ r4 = r3 + 1; i = r2; ★★ i = r4;
有序性
通过添加内存屏障实现
写-读的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到住内存中
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
操作系统语义
内存、高速缓存(线程私有)缓存一致?
解决方案(实现原则)
通过在总线上加LOCK#锁的方式
通过缓存一致性协议(MESI协议(修改,独占,共享,无效))类协议有MSI、MESI、MOSI及Dragon Protocol等
五个使用场景
1.作为状态标志
2.一次性安全发布
3.独立观察
4.volatile bean模式
5.开销较低的读写锁策略
final
重排序规则
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
初次读一个包含final域的对象引用,与随后初次读这个final域,这两个操作之间不能重排序。
DCL(Double Check Lock)
①懒汉式--②对方法上锁--③解决效率低(检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量)对代码块上锁,并做两次检验--④volatile关键字保证
步骤③问题
实例化对象的步骤
分配内存空间
初始化对象
将内存空间的地址赋值给对应的引用
重排序的缘故,步骤2、3可能会发生重排序,如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象
问题解决方案
基于volatile解决方案
将变量singleton生命为volatile即可,当singleton声明为volatile后,步骤2、步骤3就不会被重排序了,也就可以解决上面那问题了。
基于类初始化的解决方案
利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
这种解决方案的实质是:运行步骤2和步骤3重排序,但是不允许其他线程看见。
线程
JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。
线程分类
用户线程
是用户创建的一般线程,如继承Thread类或实现Runnable接口等实现的线程
守护线程
是为用户线程提供服务的线程,如JVM的垃圾回收、内存管理等线程。
任何线程都可以是守护线程或者用户线程,所有线程一开始都是用户线程。涉及守护线程的方法有两个:setDaemon( )和 isDaemon()。
线程的状态
新建状态NEW
Thread类
Runnable接口
静态代理模式
就绪状态RUNNABLE
调用start()
阻塞状态BLOCKED
等待阻塞
运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒
同步阻塞
运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
其他阻塞
运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态DEAD
状态间转换
线程间通信
java.lang.Object——————public final native void wait(long timeout)
wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法。
java.lang.Object——————public final native void notify();
notify()方法会唤醒一个等待当前对象的锁的线程(一般是高优先级的线程)开始排队
java.lang.Object——————public final native void notifyAll();
notifyAll()方法会唤醒其它所有等待当前对象的锁的线程
这三个方法用于协调多个线程对共享数据的存取(获取锁和释放锁),所以必须在synchronized语句块内使用,否则会报IllegalMonitorStateException异常。
Thread类常用方法
start方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
run方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
sleep方法
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象
yield方法
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
join方法
当调用thread.join()方法后,当前线程会进入等待,然后等待thread执行完之后再继续执行。实际上调用join方法是调用了Object的wait方法
由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。
interrupt方法
首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
而 Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。
public void interrupt() //将调用者线程的中断状态设为true。public boolean isInterrupted() //判断调用者线程的中断状态。public static boolean interrupted //只能通过Thread.interrupted()调用。 它会做两步操作:返回当前线程的中断状态;将当前线程的中断状态设为false;
锁
公平锁/非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
独享锁/共享锁
独享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。
互斥锁/读写锁
独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
可重入锁,是一种递归无阻塞的同步队列机制
比synchronized更强大、灵活的锁机制,可以减少死锁的发生概率
可以分为公平锁和非公平锁
底层采用AQS实现,通过内部SYNC继承AQS
公平锁FairSync
非公平锁NonfairSync
读写锁在Java中的具体实现就是ReentrantReadWriteLock
读写锁两把锁,共享锁; 共享锁-读锁;排它锁:写锁
支持公平性,非公平性,可重入和锁降级
锁降级:遵循获取写锁,获取读锁在释放写锁的次序,写锁能够降级成为读锁
Condition
乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
乐观锁
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
悲观锁
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
适用场景
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
共享对象
安全发布对象
对象初始化过程
方法逸出
线程逸出
线程封闭
java.lang.ThreadLocal线程封闭:特别好的封闭方法
一种解决多线程环境下成员变量的问题方案,但是与线程同步无关。其思路是为每一个线程创建一个单独的变量副本,从而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本
ThreadLocal不是用于解决共享变量的问题的,也不是为了协调线程同步而存在的,而是为了方便每个线程处理自己的状态而引入一个机制。
四个核心方法
public T get():返回此线程局部变量的当前线程副本的值
protected T initialValue():返回此线程局部变量的当前线程的“初始值”
public void remove():移除此线程局部变量当前线程的值
public void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值
ThreadLocalMap
是ThreadLocal的静态内部类,其中含有静态内部类Entry
public class ThreadLocal { static class ThreadLocalMap { static class Entry extends WeakReferenceThreadLocal {
每个Thread内部都有一个ThreadLoacl.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的TheadLocal变量副本。
Thread.java源码:// 与此线程相关的ThreadLocal值。这个映射由ThreadLocal类维护。ThreadLocal.ThreadLocalMap threadLocals = null;// 与此线程相关的可继承threadlocal值。该映射由可继承的threadlocal类维护。ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
提供了一个用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应的线程的变量副本
注意点
ThreadLocal实例本身不存储值,它值是提供了一个在当前线程中找到副本值的Key
是ThreadLoca包含在Thread中
内存泄漏
key弱引用value强引用,无法回收value
解决方案:显式调用remover()
Ad-hoc 线程封闭:程序控制实现,最糟糕,忽略
堆栈封闭:局部变量,无并发问题
发布与逸出
可不变对象
0 条评论
回复 删除
下一页