实战java高并发程序设计模式

来源:转载

一:svn


1. svn linux中的基础。


2. svn有钩子脚本,支持提交前后的验证,支持自动同步,格式验证和控制等。


3. 小型公司运维发布方案:a,临时目录传完mv过去或link过去,时间非常短。B.一般是平滑下线。再更新,再提上。


4. 大型公司部署方案



平滑下线,流量低谷下线一半服务器保证不会瘫痪,然后挂到一个内部的测试lvs上测试。一般分2批上线,3组可能有明显变化。特点是所有的修改都经过svn的流程分发(避免异常修改,同时能记录修改情况)。



5. php发布肯定是不推荐直接往上推,用mv,link的方式,php不重启,相对java简单点。


6. 有的门户网站前端有DNS智能解析,可以分地区的给用户访问。可以控制影响范围。


7. 一般越是前端更新频率越高,越是后端更新频率越低。


8. 发布代码如何保证不影响用户,如何回滚。


9. 软件版本尽量单一,要么都用一遍,辞职了,麻烦,是为了控制成本。


10. IDC测试阶段就要压力测试,能程序测试的就程序测,但程序僵硬的地方还是人测。


11. 流量低谷下线过程一般通过脚本平滑下线。平滑的意思是提供服务的提供完再关闭,新来的就拒绝了(超市买东西)。


12. 一个严格的公司一定要文档,要么离职会要挟你,要挟不了。


13. 大公司网站资源和程序一般是分离的,图片视屏,上线尽量是全量上线就以svn为准,要保证svn代码是最新的。


14. 所有的配置文件都放svn上,保证运维的稳定。


15. 大公司一般设置配置管理的开发与运维的中间纽带,她控制svn管理,上线管理,流程申请,业务协调等工作。


16. 一般大公司或注意流程或开发能力强的公司会自己实现一个自动部署的平台。


17. 开发运维业务变更用管理平台处理,隔离具体的人,和保留变更历史核查问题。


18. 领导是想的多,架构是说的多,做的多。运维,程序员是做的多。


19. 没有架构师的参与,由程序员自己搞的项目并发撑不住多少。用申请的方式可以打掉一些不是必要的事情,或可以批量申请。流程有限制也有它的好处。


20. 你自己搞的东西,就算再正确,推行阻力也大,而首先是大多数认可你是前提,否则实施也有可能跑偏。所以要注意团结群众。否则在萌芽中就死掉了。


21. 淘宝门户每次build一个包,就有一个新版本,svn也是会打个tag,预发,到集群,批量发布减少用户影响的范围,实在出问题也只能回滚了,就实现了版本管理。


22. 灰度发布,分地区,分批,时间还比较长,所以效率比较低,一般核心流程,淘宝才走这种流程。比如说交易的代码变更要求走灰度。


23. 上线需要知晓产品,运营,市场,开发,运维,紧急上线可能需要CTO签字了。



运维产品更新有套流程,不是说更新完了就走人了,要么老大都不知道你在干啥。




24. 越往上走应该更重视流程和制度,而不仅仅是技术了,技术会的再多,也不如领导一句话工资高。


总结:


1. 有一点说的挺对的,一般从svn本地进行编译打包,然后统一推送到分发服务器。从架构上确保编译时的正确性。

二:java的并行

第一周:


1. 为什么要并行,单线程是可以的,只是有些调度问题,比较复杂。


2. Jvm中有grt gc main一些线程不但是串行而且有交叉,对调度来说就交给操作系统,业务的逻辑单元编码就需要线程,进程开销又太大。(业务需要)


3. Linus Torvalds觉得图像处理(计算密集型)和服务端编程(数据量大,需要大量的数据)。(多核上确实是知道的,而且java比较适合的领域)


4. 并行是因为多核,多核因为摩尔定律失效,cpu主频瓶颈到了,没有选择走到多核。


5. 并行是同时做(多cpu),并发是间歇的切换不同事情做(单cpu)。



6. 临界区是共享资源,可被多个线程使用,但每一次,只能有一个线程使用,一旦临界区被占用,其他线程想使用这个资源,就必须等待。(避免冲突,需要控制,有点像加锁的感觉)。



7. 阻塞(比如一线程占用了临界区资源,那么其他所需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起,这就是阻塞。如果占用线程一直不释放资源,那其他所有阻塞在这个临界区上的线程都不能工作。)


操作系统层面上下文切换需要8万个时钟周期,效率并不高。


8. 非阻塞(允许多个线程同时进入)


9. 死锁,所有线程卡死,是个静态问题,cpu=0。



10. 活锁,电梯遇人,A,B都释放,然后又获取,然后又释放,又获取,很不容易发现,而且占用资源。


11. 饥饿,可能是优先级问题分不到资源,或竞争数据总失败不能往下走。


12.分阻塞/非阻塞



12. 无障碍是最弱的阻塞调度,一个线程进临界区并不要求其他线程等待。阻塞调度认为它是一种悲观策略,它认为大家一起修改数据可能把数据改坏,它自己认为是乐观的(宽进,严出,如果有竞争会回滚)。


13. 无锁,保证至少有一个线程能出去。保证可以顺畅执行下去。


14. 无等待,首选能进能出,若干步就能运行完成,无饥饿的,效率是比较高的。


如果有写,每次写前copy副本,然后修改副本不影响读,而且写也不用同步,只是在最后阶段覆盖原始数据是非常快的,不管哪个胜出,替换是一致的。数据还是安全的。


15. Amdahl定律,就是优化前后的比率。加速比=优化前系统耗时/优化后系统耗时。


有个公式 n个处理器的加速比T1(F+(1-F)/n)=Tn,这个公式说明并行程序要多而且处理器要多时可能会提高加速比,但是有个均衡的比例,这个可能最小投入得到最大加速比。


意思是说,如果总体是串行的,整体加很多cpu,用处不大。说明单纯增加cpu并不能提升加速比。


16. Gustafson定律 S(n)=n-F(n-1),F是线性比例,n=cpu个数,如果串行化比例足够小,并行比例足够大的话基本和cpu个数成正比。所以并行程序比例越大,cpu对加速的作用更明显。如果串行化比率一定的话,提升cpu个数理论能提高串行化比率(这个估计得限制一条,有可以进行并行化执行的程序)

第二周:


1. 进程里有很多线程,每个进程的切换是很重量级的。用进程并发是不会太高的。


2. Java中的线程会映射到操作系统中的线程,基本上是等价的(这个以前还不太确定的理解)。


3. 线程start-》runnable状态说线程准备好了准备执行了,但并不一定确实在cpu上执行,只是在java层面说我各种锁资源准备好多了,实体cpu未必分出时间片来给他执行,那么到底执行没执行就看cpu调度情况了。



4. 当线程准备进入临界区要执行,但是别的线程占用导致没有获得这把锁,就会进入到blocked被挂起的阻塞状态,比如(synchornized)。


5. 线程执行过程中如果调用了wait进入等待状态,那么就会等待别的线程来进行notify他,如果通知到了,那就又会进入runnable状态。有限的等待叫TIMED_WAITING。


6. Start()是开启一个线程,在新的操作系统线程上调用run()方法,最后掉的都是run()。


不过直接调用run(),只是在当前调用线程执行,而不能开启一个线程。


7. run()的实现,


8. stop()比较暴力,并不知道线程具体执行那个步骤,会释放掉所有monitor。如下图:


为了保持数据的一致性,对某个对象读写lock,当u1更新id后,执行stop,在name更新前就结束了线程并释放了lock,u2等待锁获得所后进行读取id,name就可能不一致了(因为u1的name还没有更新)。所以非常规情况,sun不建议使用这个方法。



9. 线程中断Thread.interrupt()就是给线程打了个招呼,它会把中断标志位给置上,有人更我打招呼后我可能就会处理某些事情,比stop这种暴力的方式。对于那些内有大的循环体的方法我们是才可能有意愿去把他stop掉的,几步能做完的线程没有必要去stop。


用下面这种通知方法去中断线程的好处是不影响正在执行的业务。



Sleep为什么要try catch呢?如果有人需要中断我的sleep,那么是允许中断处理的,但是抛出了interrupt异常interrupt状态会被清空,所以异常处理中还要再次调用interrupt。


10. suspend()得到lock后,临界资源是被suspend,临界资源并不会被释放,因此没有任何线程可以访问被锁住的资源,直到调用了resume方法,但如果resume提前调用了,还会冻结。像下面这种情况th1的线程被提前resume,导致没有其他线程可以再次resume,th1在resume后被suspend就会永久的挂在那里。



11. 一个好的线程的名字很重要,出问题的时候可以在错综复杂的信息中尽快找到问题。


12. Yield(),我希望其他线程有机会增多cpu,我释放掉当前占用的cpu,但没有放弃竞争机会。


13. Join(),我们只是开启了一个线程,因为是异步的,并不知道线程是否执行完毕,但是我又迫切的需要知道你是否执行完毕,我在迫切的等待你执行的一些数据,我会等你结束再来做事情,我要等下你,等一会儿咱们再一起走。


14. Join()的本质是wait(),那么那个线程执行完毕后会调用notifyAll去通知等在上面的线程。NotifyAll这个方法的调用实际是JVM中的c++去调用。因此不建议在线程实例上调永wait,notify,notifyall方法。


15. 守护线程,默默后台运行,和业务关系没那么大,起些赋值性作用,为整个系统提供支撑服务。非守护进程结束,虚拟就就会退出,守护线程不作为虚拟机退出的一个标志。



16. 线程优先级,一般俩说高优先级的线程更可能竞争到执行资源,但不绝对。



17. 线程同步,如果一个线程被挂起被等待了,那如何通知呢,如果我们有资源竞争该怎么协调这个资源呢。Synchornized是个JVM内部自我实现的拿锁,挂起,优化,自旋,拿到对象的锁或监视器。


Synchornized,


a.对象锁,要获得给定对象的锁。


B.方法锁,对象实例的锁(同对象生效)。


C.静态方法,需要获得class锁(类一般都是生效的)。


18.


Object.wait()这个对象执行线程等待,前提是必须要先获得执行的对象的监视权(lock)才能执行而且同时wait也必须释放这个对象的所有权,要么其他对象就不能执行。


Object.notify(),也是需要获得object’mointor,而且只唤醒一个等待的这个对象实例。


Notify()后,wait的线程是被允许继续执行了,但要执行前也必须要获得这个监视器才行。

19.notifyall(),这个图还是挺形象的,让所有线程去争用监视资源。



20.原子是一个不可中断的,即使多线程一起执行,一旦开始接不被其他线程干扰。


i++(r,u,w)并不是一个原子的操作。有一点读32位的long不是原子性操作,读int是一个原子性操作。


21.有序性,并发时,程序的执行可能会出现乱序。比如这两个线程中的方法,writer时执行的顺序可能是flag=true在线。线程B读取时可能先看到的是flag=true,而a的值未必是1。



22.一条指令的执行会分很多步骤的(机器码-)汇编指令)指令的执行和硬件有关系,不会一组组的一条条的顺序执行完才行行下一组,而很可能是硬件指令排序执行的。


比如一套指令:if,id,ex,mem,wb这几个阶段。下面这个计算过程实际上能看出执行时cpu的竞争过程。x的过程,有的是数据没有到达,没办法做,有的是硬件竞争,空出一个时钟周期。指令重排可以让执行流程更加顺畅,运行周期更短。(但是原则上是执行没有改变语义的执行可能指令重排,重排只是编译器,cpu优化的一种方式)


23.可见性问题,


a.编译器优化:一个变量在寄存器,一个在高速缓存,对于每个独立的cpu有自己一套的cpu,对于同个变量不可能互相都知道一定一致。


b.硬件优化。(写到硬件队列中,批量操作)


cpu之间有一些数据一致同步的协议,如果没有可见性问题,那可能性能会变的很差。




25. 可见性问题在别的线程可能是看不见的。


大爷的编译重排是汇编层面的,肯定又是一些优化规则或算法?



总之多线程总会出现数据间优化替换的问题。


26. happen-before先行发生规则

线程内保证语义的串行性
Volatile变量的读前会强制发生变量的写,保证读到变化。
unlck解锁必发生lock之前。
A,B,C传递性。
Start()先于他的每个动作。
线程的所有动作先于线程的终结(Thread.join())。
线程的中断(interrupt())先于被中断线程的代码。
对象构造函数执行结束先用finalize()方法。

27. 线程安全


线程执行时是安全的,比如i++操作就是不安全的,一个语句是可能分多个汇编指令的操作的,而且有的会出现优化和重排。


三:无锁的算法


1. 无锁是无障碍的所有线程都能进入临界区,但是必须有一个线程能胜出。

cas(compare and swap)可以给个期望值,是否与数据相符,那么就可以进行下去,否则就被认为是被修改过。抱着乐观的态度去重试的。不会挂起,最多只是重试的重复操作,性能比阻塞的方式性能好很多。(这个是后面线程操作的核心,不会加锁变成串行,在cpu有自愿的前提下尽量重试。有点开放共享的意思。)

A1.compareAndSet,看某值的偏移量是多少,进行对比


A2.getAndIncrement,一般的尝试是放一个循环体里,不断尝试是否成功,这个期望值判断算法是个方法。


for(;;){


int current = get();


int next = current;


if(compareAndSet(curretn,next)){


return current;


}


}


B.unsafe的偏移量是相对于class的字段的偏移量。Unsafe的一些操作是关于偏移量和volatile的。一些高性能并发的框架也会使用unsavf类。



C.atomicreference为了保证引用修改的安全可以使用这种方式修改。


d.atomicstampedreference,有唯一性标志的字段,没有重复的递增数据。和过程状态相关的,和结果无关的,如果用普通的cas操作有的就不能明确区分了。那如何区分呢,那就是加入时间维度。可以在过程敏感的方法中来区分这样的问题。



E.atomicIntegerArray,接口多个下标参数。我们会看到jdk和并发的运算内部有挺多的位运算,位运算比传统运算性能要好一点点。


F.atomicIntegerFieldUpdater,整数的字段更新,能够让普通变量也能实现原子更新。比如有的成员变量希望使用CAS操作,但是又不想修改数据类型。


G.atomicreferenceArrayReferenceArray把1维数组做成二维数组的样子。对于保证性能的并发操作郁闷的就是修改数据,如果把一部分内容固定下来,尽可能少的去修改原来的元素,因为同步是困难的。这个内部有很多basket,可以尝试修改内部某个一味数组的某个位置的值,可以重试,失败就失败了。这些内部的JDK算法还是有些复杂的(这哥们果然牛呀)。毕竟性能的提升是个系统的工程。调度分配,元素都很多。


四:并发1


JDK并发包1


1. ReentrantLock是synchornized的一个替代品,性能上jdk5以后差的不多了。

可重入。
可中断。(前面有个别的中断,后面会一直等待,那么可以让线程中断,可以重入。)
可限时。Lock.trylock如果没有获得锁,后面unlock时会抛出中断异常。所以先提前判断。
公平锁。(一般的锁并没有先后顺序,可能会导致某些线程变的饥饿。而公平锁是有公平顺序的,因为要额外维护排队顺序,所以性能要稍微差一些。)

2. condition类似于object的wait(await)和notify(signal),也需要先获得锁才能通知,也是先获得锁后才能继续执行。


3. semaphore是一个共享锁,运行多个线程,按系统的能力去分配许可,许可内就执行,超过了许可不可了就等待。当然这些许可是可以按需分配的。比如锁是个信号量为1的信号量。信号量的使用实际也是一种对资源的分配。


4. readwritelock,synchonize,reentrantlock不分读写,是有阻塞的并行。从功能上进行划分,可以让并行度加多,这个如果都是read的就可能做到无等待的并发,高并发有可能。(读读不互斥,读写互斥,谢谢互斥)倒是可以看看性能影响多大。


5. countdownlatch,比如火箭的发射每项的检查都由一个线程来执行。 每个执行完毕就会自动-1,到0后,等待在countdown上的主线程才会继续往下进行。是个时间栅栏,是个时间点。



实际任务中有很多场景需要做些前提准备,那怎么准备呢,就是countdown了。


6.cyclicbarrier是个在时间线上切一条线。 与countdownlatch不同的是可以循环的进行主线程工作。士兵集合完毕,下任务,任务完毕,继续执行,都可以用同个实例去进行。对于某个士兵被interrupted后,其他await的士兵觉得没有可能继续下去的话会主动抛出brokenbarrierexception异常。目的是可以一组组的复用,功能强大些。这个用途在那里呢。


6. locksupport,可以让线程挂起,和suspend不同是,locksupport可以正常使用。它的思想有些信号量的许可,park需求许可,unpark如果发生在之前那是挂不住的。调用了很多native Api


7. reentrantLock,主要是应用的实现,没有用到很多底层的api。A.CAS状态(判断是否修改成果)。B.维护一个等待队列。C.park()在队列中等待,等unlock后再执行unpark()。


8. hashmap不是线程安全。List,set都提供了synchornize方法。这个作为实用的工具类,并发量小的情况。因为内部将所有的操作方法都加了synchronized,这就导致把所有的操作变成了串行的实现。


9. ConcurrentHashMap是个高并发。首先hashmap是个数组,每个槽位都放一个链表头部,如果出现大量hash冲突,它就会退化成一个链表,所以hashmap一般不能放太满,太满了会有hash冲突,会导致性能降低。ConccurrentHashMap会在内部被拆分成很多segment,小的hashmap,每个线程进入的entry可能不同,减少了hash冲突。相应的put方法也没有同步,只是进行了cas运算。如果找有hash冲突就加入,那就把他串成一个链表,如果缺失是插入,否则是覆盖。如果这个尝试次数超过了最大次数,我就会暂时把自己挂起。若果有重hash的情况,可能尝试会重复做些事情。不会直接lock,是尽量尝试后再lock。只不过有个小弊端,hashmap分段后,统计总数时需要获得所有的锁,毕竟不能在变动时候统计。只不过Size()可能也不是一个高频方法。


10. BlockingQueue,阻塞队列,不是个高性能的并发容器,是个多线程的共享容器。读的时候,如果为null,就会等待其他线程放数据,等待线程会唤醒并获取数据。如果写数据时为满,那么写数据就会等待别的线程拿掉数据才能写进去。只所以不是高性能的是因为加过了锁。Take可以await(),put可以signal。就是个生产消费模式。



11. ConcurrentlinkedQueue是个高性能的并发安全的队列,如果想在多线程有个性能好的表现就是这个了,很少把线程挂起的操作。


五:并发2


1. 线程池,线程的创建和销毁和业务无关,只关注线程执行业务。那么我们放一定数量的常驻线程,就可能节省一些cpu时间。核心就是我们要保留我们的线程,工作做完了要做些特殊的事情避免线程的退出。如果有空闲线程,直接拿过来用,没有就会创建线程再执行。用完放回池里并等待,取出设置线程后会通知。


2. 一个稳定商用线程池。这个callable运行后有个返回值。



3. newfixedThreadPool(固定),newSingleThreadExecutor,(来一个执行一个),newCachedThreadPool(一段时间没有任务会慢慢减少),newScheduleThreadPool(比如每5分钟执行一个任务等,类似计划任务的功能),前3个实现本质是相同。都是指定了执行线程的数量和存活时间,然后后进的任务如果继续进入会不停的加入到队列中。newCachedThreadPool并不会开线程执行,而是有线程正好要拿和塞数据,起到了数据传输作用,而是执行。


4. ExecutorService可以得到线程执行前后的信息。


5. 如果有大片任务上来,发现负载过大,那么我们就需要丢弃一些任务,有些拒绝的策略,有不同的操作方式。RejectedExecution,如果出现异常会把异常信息打印出来。Discardpolicy,至二级丢弃。Callerrunspolicy,我做不了交给调用者来做。DiscardOldestpolicy,丢弃最老的没有处理的请求。这个案例说明在高负载的时候我们该怎么做要好一些。


6. 我们可以自定义一个线程工厂,比如名字,优先级,是否守卫进程等。


7. 简单线程池实现原理,提交一个任务去线程池中执行,如果没有可能放入一个workQueue的BlockingQueue中,ctl是个状态参数,表明有效的线程数,表明库是否runnig或shutting(running,shutdown,stop)等。如果我们自定义的话,会把两个参数放在一个对象当中,就要有方法从中抽取出来。如果状态正确就会运行,否则会拒绝。


8. Forkjoin,就是拆分任务收集结果,分隔任务也是有边界的,如果分割完毕后,执行task.fork()推向线程池。Forkjoin主要是思想,小任务直接执行,大任务则分解。有一些个ForkJoinPool。Ctl中很多变量被打包在一个64位字节中,那比分拆的多字段有什么优势呢?这个就是避免多线程synchornize,影响性能的可能性,而这样做呢,一个cas操作就能完成。而多个变量也避免了多个cas的操作。这显然不用加锁的好处。


六:设计模式

什么是设计模式?是工程普遍存在通用存在的,是从建筑中引入计算机当中的。科学和艺术相融合的艺术,建筑是艺术和科学的结合体。有些优雅常用的结构和组件我们可以使用一些好的解决方法。
架构模式(MVC,分层) 设计模式(提炼系统中的组件) 代码模式(与底层,编码直接相关DCL)
单例和多线程相关,可能是个系统全局的对象,有利于我们协调系统的整体行为。


System.out.println(Singleton.STATUS);时就会生成这个单例对象,但是可能你只是想访问变量却生成了这个单例。

像这个单例是典型的延迟加载,只能是需要的时候一个线程进入创建。问题是高频访问的时候可能会影响到性能。


5.这种方式相对好些,原因是首次访问StaticSingleton时并不会初始化SingletonHolder,只有真正调用时候才进行初始化,后期调用直接返回即可。而且注意类的构造函数初始化用的是private。


不变模式,只读对象,整个生命周期都不会改变,因为高并发可能有同步,不变模式不需要同步,如果需要高性能的访问某些对象,可以初始化一个不变的只读对象。

7.确保父类是不可继承的,字段是不能重新修改的,保证类是不可变的。



9. 不变模式案例,String创建后不会改变,哪些看起来像修改了String内容的操作实际上也是在新String进行的操作。很多基础的数据类型其实都是不变模式的思想,包括Integer的i++实际都是封装在了自动拆箱装箱的过程中。如果它在原程序上的修修补补就不能保证多线程的安全性了。



10. future模式,说白了就是异步操作。一个很好的比喻就是先立即返回给你个订单,至于数据以后再返回。先给你我的承若,其他事情你可以继续做。


11.如果FutureDate没有找到返回数据就会等待,等确实执行完设置了真实数据后进行线程的通知。



12.这个异步线程执行完毕会向原线程设置真是的数据。开启线程就会进行异步执行。这就是后台把同步的方式写成异步的调用方式。


12.这就是jdk封装图的future模式的支持,callable相对runnable可以返回相关的执行类型。只是想调用完了拿到一点返回值,可以把同步调用变成异步调用还是个不错的用法。核心的原理还是future的异步方式。



12. 生产消费模式,两个线程如何共享数据,双方怎么才能松散的知道呢,紧密的知道对方,一个模块对外知道的越少越好,甚至不知道更好。内存缓冲区。大家只知道缓冲区。Currentlinkedblockingqueue是个高并发的解决方案。



13. 消费者如果有非常多的数据要消费,要扣考虑性能的话,那么可以考虑把循环写死疯狂的获得数据。



七:NIO与并发的关系


1. 它改变了线程在应用层使用的一种方式,解决了一些实际的困难,节省了一些成本。


2. NIO的存储是基于block的,他以块为基本单位,为原始的类型都有buffer,原始的I/O是流,它支持Channel。锁存在的话,表示当前线程可能占用了这把锁,其他线程如果用这把锁就会等待,使用完了会删除掉锁,用文件做为锁,和原来的对比是用某个整数来的做为锁的。文件映射到内存比传统方式快的多。


3.NIO所有的读写都先通过channel读写到buffer中。



3. Buffer有3个重要的操作。


a. Position 写(当前的下个)(存后的位置,flip然后置0)


b. Capacity 缓存区总容量上限()


c. Limit 实际的容量小于等于容量(flip后limit到position位置)。


4. Flip通常之前的是写,之后的是读缓冲区。读写转换的时候通常使用。



5. 文件映射到内存,将整个文件读到内存中了,这个速度是比传统的快的。


6. NIO的网络编程,多线程网络服务器的一般结构。客户端被派发给线程做处理,当然也把socket给线程,每个线程做处理。当然涉及到大量的r/w。



7. 这个的关键是从线程池中挑一个线程处理客户端请求。新线程做客户端的请求,主线程继续做8000端口监听的线程等待工作。Serversocket就像个tomcat的容器(先dispatcher线程给特定的action),然后action创建HandleMsg线程,进行业务的处理,而且都是针对客户端的读取和写出。





8. 这么做有什么问题呢?


如果某个客户端出现异常延时的话,网络可能是不太稳定的,情况也比较多(通讯信道并不可靠,中断延时肯定会产生一些影响,数据的准备读取都是在线程中负责)如果并发大,就会对付这些卡死的线程。那么NIO读取是不阻塞的,只有准备好数据了线程才会真正开始工作,没准备好的话是不会启动工作的。如果是原始的读取操作每个客户端都会卡6s钟

如果是NIO来处理,chanel类似socket的流,可以和文件或网络socket对应,一个线程对应一个selector,一个selector可以选择多个channel每个channel对应一个socket,他要看那个客户端准备好了,就是看那个准备好了。




Select:会告诉当前有客户端准备好数据了,否则select调用select是会阻塞,如果有则返回selectionkey,这是一对select和chanel准备好的对象,这样这个线程就可以用少量线程监控大量客户端,那么总有几个客户端可能是准备好的。那么直接拿过来用就好了。


Selectnow:和select功能相同,直接返回,如果有准备好的直接返回数据,没有的话就返回0,大部分时候select确实会进入等待。

9. 代码解释


ServerSocketChannel ssc = ServerSocketChannel.open();


ssc.configureBlocking(false);//非阻塞不会做accept等待,而是说有人连上来,accept之后,我会得到一个accept通知。如果是阻塞模式那和传统的编程是类似的。


10. NIO是将数据准备好了再交由应用进行处理。把这个事交给专门少量线程来管理,为真正执行业务的线程节省了资源。只是把等待的时间在专门的线程中等待。


11. AIO,比NIO更进一步,说等读完了,写完了再来通知我。AIO速度并没有加快,一个更合理线程和时间的调度,很有ajax异步的感觉。主要是定义大量的业务函数进行回调处理。


Server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));


Public abstract void accept(A attachment,CompletionHandler handler);


当真有accept信息上来后,才会真正调用。


12. AIO的异步方法是立刻返回的。有的网络协议报文头部是固定的,所以用这种方法来过滤掉头部内容。Read(ByteBuffer[],int,int,long,TimeUnit,A,CompletionHandler)。Write也有得到future,最后看写了多少内容。比如下面这个读写都是返回的future对象。这样做又有点像把异步的操作变成了同步的阻塞操作。



八.Java并发课程


1.锁有阻塞非阻塞的,非阻塞(无障,无锁),一旦用到了锁,那并发性能永远比不上无锁的方式。那怎么在阻塞情况下让性能得到优化,那么怎么让这种阻塞的影响降到最低。


2.如果是用try lock 这种cas不认为是锁,不挂起。


a.降低持有锁的时间,降低同时进入临界区的时间。


Public synchronized void syncMethod(){ meth1();meth2();meth3();}减少


Public void syncMethod(){ meth1();synchronized(this){ meth2()};meth3();}减少持有时间和范围缩小,减少冲突性,这个synchronized(this),加锁的this确实是那个对象的锁。


B.减小锁粒度,一个对象可能很多线程访问,拆分对象,增加并行度,降低锁竞争,偏向锁,轻量锁成功率提高。比如hashmap的同步实现读写会互相阻塞的。



那么concurrentHashMap就是把hashmap拆分成多个segment后,粒度减小后,concurrenthashmap就允许若干个线程同时进入。


C.锁分离,就是读写锁的思想,读读不影响(基本是无等待并发),写写同步,读多写少的情况下能提高性能。如果说读写分离的延生的话,就是让尽量减少冲突区域和冲突时间。LinkedBlockingQueue一个从头部,一个从尾拿,用热点分离的方法。



D.锁粗化,当重复的获得释放频率过高,导致锁同步的判断性能影响大于了加锁的性能时还不如加大锁的宽度。像这种情况的1个锁性能优于多个锁。



比如,for循环的加锁情况优化。除非说for内的内容太多也等不起,就在内部加锁让线程竞争执行更好。



所以锁是个度的平衡。


E.锁消除,StringBuffer竟然是同步的。看局部方法是否可能是全局影响的变量,有的可以开启逃逸分析。JDK判断是否需要移除锁的限制。




F.虚拟机内的锁优化


九:锁偏向,每个对象有对象头,MarkWord,描述hash,锁,垃圾标记,年龄,保存一些对象信息。


十:偏向锁,很偏心,偏向当前已占有锁的线程。有时会出现某线程不停的请求同一把锁,这是有可能的,如果你已经占有这个锁了,那么你就进去。锁是一种悲观的策略,竞争,冲突是可能有的,实际很多情况竞争可能是不存在的,或着并不激烈,系统已经发现我是被偏向的那个线程,那么我就是不是偏向模式,并且是我,那我就直接不用锁的操作进到锁里,提高的进入速度和性能。如果没有竞争并反复请求同个锁,那么效率的改善是很明显的。实现的时候,对象标记mark为偏向,并且将线程id写入对象Mark就好了。但竞争激烈的情况下偏向锁反而是种负担。所有任何事情都是有两面性的。



十一:轻量级锁,最好不用动用操作系统级的互斥,而最好在JVM应用层面解决这个问题。判断某线程是否持有某对象锁,那么只要看看lock是否设置了某对象的mark值,如果是的话,那么就说锁持有对象,对象头的指针指向了lock,lock呢又存在thread的栈空间当中,判断线程是否持有对象的锁,只要判断对象头部指向的位置是否在线程栈的地址空间当中。



如果轻量锁失败,说明有竞争,升级为常规锁(操作系统层面的同步)。如果竞争激烈,轻量锁会做很多额外操作,导致性能下降。说白了就是尝试拿锁的一个cas操作。


十二:自旋锁


如果轻量级锁失败的话,可能会动用自旋锁,那可能会试着while try lock的操作,而不要直接挂起,因为消耗时钟周期太长了。(像现实中一样,等待的代价和时钟周期远远大于不断增加的尝试)。有时候可能别人会把资源释放掉了,对于快速的cpu来说等待的代价大于不停的尝试。说的是减少减少锁的持有时间,那么自旋的成功概率就会上升。更可能避免线程的挂起。轻量锁和自旋锁都是在JVM层面做的一些优化。


十三:这个代码非常隐蔽呀,Integer的i不是一个


十四:ThreadLocal,不同的线程只访问自己的对象实例,那么锁就没必要存在。


如果某对象被多线程访问时可能会导致对象异常,当然可以加锁,但是高并发性能会有限制,那么我们就可以用ThreadLocal的思想来定义自己使用的变量。我们知道SimpleDateFormat是线程不安全的。在hibernate的connection的保存中用了ThreadLocal类,一些公共类,工具类,每个自己的线程持有一份,不像vector数据间会相互影响。(这个实现的原理还没太懂,需要了解下hashmap的原理),这是ThreadLocal的基本结构图。



补充hashmap原理:


a.就是利用数组的查找性能和链表的添加修改的性能优点。



Hash(k)%len = slot_index,hash(k)是一个非一一映射的int值函数,计算出的值是确定的 。所以可能hash冲突。实际存在的是一个Entry[] table。

那存取的时候首先能获得那个index的位置,Entry[0]=value,数组中存的是最后加入进来的数据。到这里只是看到了相同hash值和相同key或equal后的key存在时替换值的情况。可以肯定的是虽然key的hash值相等,还不能保证取正确的值,还要保证key的地址或内容相同才行。(也有点类似个二维的经典表格)hash冲突是如何解决的呢??

十五:并发调试


1. 线程dump及分析。如果线程卡死了。多线程执行是不确定顺序的并且不能保证问题的重现,那么怎么手动控制多线程的调试呢?设置条件断点是中方法。先知道这块有个条件断点的事,不满足条件的就进不来了。Suspend VM可以中断整个虚拟机,会断所有线程,有时可能会死掉。



2. 线程dump分析jstack.有时可以分析出死锁或死循环的线程。断点文件是可以调试的,前提是先连接上资源。


3. 比如下面的异常信息,可以用条件断点的多线程临界点来调试。


public boolean add(Object obj)


{


ensureCapacity(size + 1);


1.t1,t2都判断通过并且认为满足容量限制


elementData[size++] = obj;


2. t2设置值完毕后,t1并不知道size已经是11了,而此时扩容的条件判断已经做过了,容量并没有得到扩充,index=11这个单元并没有分配空间,所以t1此时设置值时候就会抛出异常(


Exception in thread "t1" java.lang.ArrayIndexOutOfBoundsException: 7747


at java.util.ArrayList.add(ArrayList.java:352)


at Test$RT.run(Test.java:19)


at java.lang.Thread.run(Thread.java:662)



return true;


}


3. 使用命令,jstack –l 进程号,有时候可以查看一些死锁信息。有的也并不一定是死锁,只是占用某些资源不释放。


十五:JDK8并发新支持


1. LongAdder,也是类似于热点分离成,拆分成多个增阿基cas操作的成功性。


比如 long add会把整数成cell的数组,多线程进入操作打散的cell时减少冲突的可能性。淡然有的并发小的时候,初始1个cell,当执行有冲突的时候会扩大cell的数量,并且并发增多的时候可能还会往上扩充,然后分配映射,目的还是减少冲突。


这个有些有冲突的优化算法,


2.CompletableFuture是future模式的增强版,特点是它会把完成的点开放出来供大家用。


这个接口更有一些函数式编程的倾向在里面。



2. stampedLock,有读写锁的有点,rr不阻塞。而且有改进的地方时是在读的时候也不阻塞写,只是读的时候判断有写时会进行重读。当有大量读操作,而写操作很少的时候,可能写操作会有饥饿现象。同时的理解是写时候可以很容易的拿到读锁,当读后发现数据不一致会从新读。


3. 先采用乐观读,如果读取成功就执行修改,读取没有成功就退化成悲观读。



4. stampedLock采用了CLH自旋锁的思想。锁维护一个等待队列,对象放到队列里,对象有一个锁的标记位。每个加入的对象会不断的循环等待前面对象释放锁。因为当前对象在循环中,所以并不会挂起,但是有一定的循环次数去等待,并不会不休止的循环,当达到一定次数后就会将线程等待。


Jetty:


1.jetty是个http server,甚至可以做一个嵌入式的引入,new了并启动起来。看他后面是如何用多线程实现高吞吐量和性能。


New Server()(需要实例化,把待执行的放入线程池中)


Server start()(需要启动,ScheduleExecutorSecheduler (定时调度),ByteBufferPool(是个可复用对象池,比如NIO中的channel不用每次去new,里面有特别的安全的无锁线程池-ArrayByteBufferPool),)


http请求(servlet,容器,jsp等)


2. 服务器原理是相通的,如果一个线程发起,那么就放到线程池的队列里就好了,是个BlockingQueue,那么可能是个优化的点。对象池的对象是相似的,ByteBufferPool内的对象大小不同,不可能把所有大小的内容都new出来,Bucket有size和queue,queue是延时加载的。Acquire获得,找到合适的bucket。Release,找到合适大小的bucket,归还buffer,清空buffer的标志位,归还到queue中去。如果ByteBuffer过大过小无法归还,会被GC回收。它是无锁的,大小不同处理不同。ConnectionFactory,accept后,要创建一个对象来维护连接。会获得cpu的数量,由cpu数量算出线程数量,那么会分配几个线程等待连接。创建acceptor线程组。初始化serverconnectorManager,然后会计算出有多少个selector来做这个等待。总之selectoer要比accept多。 设置端口,关联server.


doStart 有个lifecycle的管理器方法,类似spring 对bean的管理。拿到线程池,每个connector有多个acceptor,acceptor有会有多个线程,如果需要的线程多余200,那么启动ThreadPool启动并从队列中拿出任务去执行,当然还有AppContext的一些功能参数去维护。启动connector,获得connectionFactory,根据seelctor的数量启动selector,然后等待acceptor进行连接,创建线程并执行,SocketChannel channel = ServerConnector.accept(),如果accept为0,那么会配置成阻塞模式。http请求,accept成功后配置为非阻塞,配置socket,选择可用的managedSelector线程,虽然不是线程安全的也可以。提交任务到执行队列中,ConcurrentArrayQueue保存元素而不是node,需要更少GC,更少对象(真是越到底部优化的越精细呀),所以性能更好一些,所以它是个高性能高并发的实现,所以即便有大量任务上来。ManagedSelector.run()方法runChanges()。然后获得SelectionKey=channel.selector,获得连接对象。然后等待select()操作,确实会用新的线程进行业务的处理,不会占用很多selected()线程。中间还是有很多关联的。


分享给朋友:
您可能感兴趣的文章:
随机阅读: