并发调试和JDK8新特性

第一个是多线程的调试方法,第二个是关于dump的分析,我们在系统出问题的时候,可以看有哪些线程在运行,

每个线程的堆栈是什么情况,如果我们发现有些线程被卡死了,我们从堆栈的分析当中呢,往往可以得到一些有价值的信息,

知道这个线程为什么卡到这里不动,我们介绍JDK8当中,对并发产生的一些新的支持,我们对内置的并发包当中呢,线程池,

ForkJoinPool,这些都是涵盖在JDK567当中的,JDK8他比较新,它会提供一些性能更加好的,使用更加方便的并发类我们也会在

这个课程中做一个介绍,首先我们来看一下多线程的调试

多线程调试呢和单线程相比呢,困难程度会有所增加,原因就是我们同样的代码,他可能被多个线程一起执行,

多线程并行的运行的先后顺序呢,还有些不确定的,有些时候线程会先执行,你在同样运行同样的程序,可能会有

完全不同的输出,并不像单线程的,串行程序一样,你只要起始条件一样,初始条件一样,那你的运行结果基本上都是

一样的,基本上起始和终止完全是一样的,也就是你所有的内容都是可以重现的,但是并行程序并不能保证,他的结果

是可以百分百可以重现的,有些问题有时候可以重现,有些时候不能重现,这种很有可能是多线程造成的,因此我们也需

要用到对多线程调试的方法,手动的在调试过程中哪个线程谁先执行谁后执行,执行到什么程度的时候,哪个线程执行

到哪个阶段,另外线程再切入进来,这里有一个简单的例子,就是我们的ArrayList,我们ArrayList他不是线程安全的,

这样的代码他一定是错误的,我们有两个线程去执行这个东西,这个程序一定是错的

add方法出了问题,ArrayList当时是为了实现一个无锁的Vector,所以也介绍了Vector的实现原理,大体上应该都是

相当的,因为内部实现是一个数组,当数组容量不够的时候呢,进行扩容,因此在加入一个元素之前呢,我当前的数组的

容量是不是够的,如果不够我就进行扩容

当这个条件成立的时候才会生效,这个条件断点的使用呢,在各种场合下都可以使用

现在我们再来看一下线程dump的分析,我们可以使用jstack的工具,找出正在虚拟机下面的,所有的线程,

然后我们注意看,每个线程当前正在做些什么事情,我们可以推测说,如果出问题的话,这个问题出在哪里,举个例子

比如说,你发现一个程序,他卡死了,那你可以找出来这个程序在干什么,他执行到哪行代码上上面,卡死了,他等待在

某个锁上面,某个对象上面,这个锁或对象呗占用了,在旁边等待,你可以通过dump出来的东西呢,你可以看到当前线程

有哪些问题,还有些情况呢,发现是卡住了,每次都是在同样的一段代码上面,但是也没有等待在某个锁上面,线程就陷入到

一个死循环上面去了,这个也是有遇到过的,但是没有在任何锁上做等待,这个多半就有可能是死循环

JDK8当中对多线程的一个新的支持,首先看LongAdder这么一个类,累加器,它是非常接近AtomicInteger,

它是一个long型的,他更加接近AtomicLong,Atomic长整形,他的性能在高并发的情况下呢,要比AtomicLong要

更好,AtomicLong已经要比锁要好很多,它是一个无锁的操作,为什么LongAdder要比原子操作性能要更好呢,

其实LongAdder里面也是使用原子操作,他用了一些额外的处理技术,热点分离,这个我们在计算ConcurrentHashMap

的时候呢,介绍过这种思想,一个大的HashMap,如果给大的对象加锁,性能会比较差,如果我们把它分成16个小HashMap,

每次只操作其中一个HashMap,我们只拿他的十六分之一进行加锁,这样冲突的可能性就会减少,如果冲突减少了,性能

自然就提高了,并不是无锁阻塞才需要做热点分离,才要做这个冲突的避免,无锁操作我也要冲突,如果我一个线程不断

尝试失败,那我还是要做这个循环,知道尝试成功为止,所以如果我把热点数据进行分离之后呢,减少这个冲突,获取每次

操作的成功率呢,会大大上升,只要我操作成功上升呢,我这个性能也能得到提高,本来我要两次循环,现在我一次循环就能够

做好,那我这个性能就提高了一倍,和原子类也比较像,add是原来上增加多少,increment增加1,decrement就是add -1,sum和

longValue是一样的意思,刚才有说过,因为LongAdder内部也做了热点分离,他把一个整数分解为若干个整数,最终的结果就是

若干个整数,数据的求和,这两个其实是等价的,这里就是把long转成一个整形,这里就是LongAdder的一些使用

内部LongAdder他会做一个什么事情呢,它会把原来作为一个整数的,数字分解为一个数组,分解的原因呢,

数组分解完之后呢,有一个cells,每一个小单元呢,都是一个整数,当你一个线程进来的时候,做CAS循环的

操作,因此失败率就会比较高,在高并发的时候,但是如果说,多个线程他对应的是不同的单元格,最后LongAdder

他所有的值呢,是所有的cells相加得来的,这只是一个累加器,那这种情况呢,打散之后呢,他们的冲突概率就减小,

这样CAS更新的成功率就提高,性能就会增加,但是换句话说,如果当前的数据竞争并不是很厉害,并没有多少线程

在参与这个事情,可能就是一两个线程在做这个操作,如果像这种情况,你也去分出多个单元格来,然后再做累加求和,

这样也是有点浪费资源,因为你完全用一个数字就可以解决问题了,我真的有很多线程在做这个操作的时候,我这样做

是有意义的,LongAdder他本身也考虑到了这种情况,他也知道说,并不是所有的LongAdder在高并发的操作下,有时候我

可能就是一个线程或者两个线程,因此它并不是无条件的把数据打散到不同的cell上面去,他在正常执行的时候呢,内部也会

维护一个base的元素,就是原子的long型的数据,但是每次你去做操作的时候呢,它会对这个数据进行一个累加,加或者减,

但是他一旦发现冲突,他只要发现又一次冲突之后呢,他就会去创建这个数组,创建这个cells,cell数组在最初的时候,LongAdder

刚刚创建的时候,是并不存在的,数组当中的元素也不会太多,也就两个,每当发现cell操作有冲突,他就会对这个cell做扩展,

不停的扩展,只要他没有发现冲突,就不会做这个扩展,但是他发现冲突,才会做这个扩展,因此在最终这个场合下面,他很有可能

就永远不发生冲突,这个就是LongAdder的基本思想,可以说有点自适应的感觉,不停的把容量给扩大,避免冲突的产生,内部大概

就是使用这么一种策略,当你求和的时候,你把所有的部分都要加起来

下面我们来看JDK8当中另外的一个类,方便大家使用的一个工具类,CompletableFuture,完成的Future接口,

他实现的是CompletableStage接口,这个接口大概有40个方法,单纯从面向对象的角度来说,我们接口方法还是少一点,

为什么接口当中有40个方法呢,因为我们在JDK8当中,流式的API,一个套一个的去写,这个在后面会做一个简单的介绍,

函数编程当中我们看到了这个东西,本质上就是一个匿名的内部类,相当于在内部实现了一个Runnable的方法,CompletableFuture

呢,也就是对我们以前的Future模式,他的一个增强版

这里是一个简单的实现,一个简单的应用,我们使用Future模式的一个功能呢,可以提交我们的请求,然后拿到Future,

类似订单的一个东西,如果我们后台任务没有完成,那我们去get这个结果的时候呢,会进行一个等待,当后台数据完成之后,

我们再去get他,我们就可以马上得到这个信息,这是future的一个基本的功能,对于CompletableFuture来讲,他把完成动作的

这个功能呢,开放给我,比如在这个地方,我们可以新建一个CompletableFuture,一个可完成的Future,然后我们会把Future

传给AskThread线程,这个Ask线程会做真什么事情呢,做一个平方然后返回,要有数据才能做平方,如果没有数据get肯定就是

一个阻塞,那你什么时候Future能够完成呢,对于CompletableFuture来讲,完成Future的时间点,我们在Future当中就有一个

complete方法,来告知完成,你想他什么时候完成,他就什么时候完成,当你运行这个方法之后,这个地方,他就能得到通知说,

前面的Future已经完成了,继续往下走,他把完成操作这个功能,时间点开放出来,因此来讲是一个更加灵活的Future

他可以做异步的执行,和普通的future比较的接近,CompletableFuture有一个supplyAsync,他要求把一个函数,模拟一个比较长

时间的执行,在这把一个东西去执行它,它是一个工厂方法,他并不是让你去new一个CompletableFuture出来,而是内部去创建

一个对象实例,然后我们可以得到他的结果,这个跟一般的使用方法比较接近

说白了就是线程池,线程池当中去执行它,run和supply有什么区别呢,没有返回值的,run是单纯的runnable接口,

他是没有返回值的

CompletableFuture他可以通过工厂方法呢,把对象实例创建出来之后,我们可以在这个实例之上,再进行流式结果的

处理,建立一个平方,我们可以对结果进一步处理,把这个结果转成字符串,一连串的操作可以在一个语句里完成,有一种

函数式编程的倾向

把另外一个对象实例进行组装,这个就是CompletableFuture的一个功能,为了支持函数式编程,原本Future模式,

让你决定什么时候来完成通知,就这个类而言,跟性能本身是没有关系的,更多的是方便性的一个操作,其实是可以

压缩编码量的

最后来看一下StampedLock这个东西,前面我们有说过读写锁,读写锁要比单纯锁的功能要好很多,读写锁不会

读和读之间的竞争,他认为所有的读都是可以并发的,只有当读和读,写和写的时候呢,他才会等待,读很多的情况之下,

读写锁对性能的提升是有帮助,但是StampedLock在读写上面呢,他又更近了一步,他认为说,对于读来讲,如果我读也

堵塞了写,他认为不是一件很好的事情,他认为我读也不应该堵塞写,而是说我在读的时候,发生了写,那我读要做重读,

而不是说读不让写去操作,那这样的思路有什么好处呢,但有大量的读线程存在,其实写线程是有可能发生饥饿现象,

因为你有太多的读,写线程不能得到及时的调度,你写不进去,因为读太多了,而你采用StampedLock,他写不会阻塞读,

读很容易拿到读锁,而写的时候很容易拿到写锁,然后进行写,因为你写的时候很容易破坏数据,没有写完的时候数据

是不一致的,当读线程发现数据不一致的时候,他其实是可以做一个重读的操作,从这个角度上来说,当你读写线程在

混用的时候,读和写是不会阻塞对方,这里有一个例子,点有x和y,这里我们就是用StampedLock,Stamped就是时间戳的

意思,更像是一个时间戳,类似一个邮戳,你每次做写操作的时候呢,其实我们都可以对时间戳进行加1,内部实现可能

不是加1,累加上去之后呢,我邮戳这个值,不停的变化,我可以通过这个变化,来判断当前这个锁,或者当前这个数据,

究竟有没有被人占用,对于读来讲呢,他支持读锁,但是他这里会有一个新的乐观的读,就是tryOptimisticRead,就是

乐观读,乐观读就是采用我们刚才的思想,他不会阻塞写,乐观读读到Stamped的时候,就相当于我拿到了邮戳是多少,

然后我把x,y读出来,就是类当中的x,y,因为我在读x的时候呢,y肯定被人改掉了,因此我最终读到的x,y,currentX,currentY,

并不一定是一个一致的数据,那有可能就是不一致的,因为你在读x的时候是好的,y被人改了,y可能就是另外一个点的数据,

因此并不一定是一致的,全部读完之后,我要去做一个验证,我验证刚刚拿到的读写锁,乐观读的stamped,到底是不是可用的,

validate成功的前提是什么呢,条件是什么呢,我在拿乐观读的时候,进行乐观读的时候,写锁并没有被占用,如果当前的

写锁是被占用的,乐观锁一定要宣告失败,就相当于我乐观锁就返回一个0,0一定是一个失败的乐观读,因为如果当前数据正在

写,假设这个地方是一个合理的数据,但是我要验证Stamped的时候,你当前的stamped,StampedLock存在一个类似时间戳

的东西,你每做一个writeLock,时间戳都会变化,都相当于加1,当你每unlockWrite,他也会去发生变化,如果没有发生溢出,

它会重复的,每次加锁和解锁的时候,stamped是会发生变化的,因此我刚才拿到的stamped的值,是不是和内部的sl的值

是不是一样的,如果是一样的呢,说明在整个读的过程当中,并没有人去加锁,并且我在try乐观锁的时候,也没有人去加锁,

那我就可以认为,乐观读是成功的,只要我乐观读成功,我就要做我要做的事情,比如我这里是计算,点到原点的距离,因为我

这个读是成功的,我就可以直接把他给算出来,currentX和currentY是合理的,是一致的,当我这个地方验证不成功,也就是我

在执行这句话的时候,可能有人加了writeLock,这个时候我stamped的值,内部保存stamped的值,他不一致,那我就认为你验证失败,

验证失败其实有好几种处理方法,一种是我验证失败我再去验证一次,再去加一次乐观锁,就是我们CAS操作的这种思路,就是我写一个

死循环,就是不停的拿乐观锁,知道我成功为止,这个地方演示了readLock的使用,那么我就放弃了乐观锁,我就使用悲观的策略,

这就相当于是读锁,然后去做数据的读,然后就释放读锁,这种情况就是一种悲观的读法,StampedLock提供了乐观读的思路

他也会使用CLH自旋的策略,这个自旋是什么意思呢,如果我们发现读失败的情况,他不会立即把这个线程挂起,

锁当中会维护一个队列,所有申请锁,但是没有成功的线程都记录在这个队列中,每一个节点会包含一个信息,

共同组成一个链表,它会保存Lock的一个标记位,前面的线程是不是持有了这个锁,判断当前线程是否已经释放了

这个锁,当一个线程试图获得锁的时候呢,它会取得当前等待队列尾部结点作为其前节点,并且使用下面类似方法

进行加锁判断,前面节点的锁有没有释放掉,如果前面节点的锁有释放掉呢,那我就可以记录这个事情,否则我就应该

继续循环等待,这个就是一个死循环,这个死循环其实就是不停的等待,前面节点去释放这个锁,但是当前线程本身呢,

在这个循环当中,所以他是不会被操作系统挂起的,这个就是自旋锁的一个基本思想,他把所有的拿不到锁的,想要锁的

线程呢,保留起来,不断地做这个循环

每一个线程看前面的线程是不是已经释放了锁,一旦发现释放了就开始执行,开始往后执行,但是StampedLock

不会无休止的执行,如果所有的线程拿不到锁,无休止的轮询,那肯定是不行的,那CPU的占用率是很高,所以这里

只是一段示意代码,会做自旋,但是无限制的自旋,他的自旋是有一定的次数的,超过某一个次数之后,比如超过1千次

之后,他就会把这个线程进行park操作,就是把它作为一个等待操作,否则就会做若干个自旋,这个也是StampedLock

内部实现的机制

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值