由于最近工作原因,很久没有写日志了,今天在这写篇文章,是关于java多线程的。这也是我对于多线程编程的一点浅显的认识与理解。
对于JAVA多线程的应用非常广泛,现在的系统没有多线程几乎什么也做不了,很多时候我们在何种场合如何应用多线程成为一种首先需要选择的问题,另外关于java多线程的知识也是非常的多,本文中先介绍和说明一些常用的,在后续文章中如果有必要再说明更加复杂的吧,本文主要说明多线程的一下几个内容:
1、在应用开发中什么时候选择多线程?
2、多线程应该注意些什么?
3、状态转换控制,如何解决死锁?
4、如何设计一个具有可扩展性的多线程处理器?
5、多线程联想:在多主机下的扩展-集群?
6、WEB应用的多线程以及长连接原理。
1、在应用开发中什么时候选择多线程。
在前序的文章中已经简单提及到过一些关于多线程应用的文章,通过对web的一些线程控制对下载流量的控制,其实那只是雕虫小技,也存在很多的问题需要去解决,不过面对用户量不大的人群一般问题不大而已。
多线程在生活中的体现就是将多个同样很多事情交给多个人来并行的完成,而中间有一个主线程起到调度者的作用,运行者可以强制依赖于主线程的存在而存在,也可以让主线程依赖于自身;曾经我听很多人说过如果你的机器是单CPU,多线程没有意义,其实我并不这么认为,以为内单个CPU只能证明在线程被调度的瞬间只能同时执行一条最底层的命令,而并不代表不可以在CPU的征用上提高效率,一个是内存级别的,而另一个是CPU级别的,效率上仍然存在很大差距的;(这个可以让一个程序单线程去循环10亿次(每次自增1),和让十个线程独立运行1亿次也是同样的动作,记住这里不要将每条数据System.out.println出来,一个是机器扛不住,另一个是这里会对测试数据产生影响,因为这个方法我前面的文章中已经说明会产生阻塞,尤其是在并发情况下的阻塞,即使在单CPU下结果肯定也是有很大差距的,我这暂时没有单核的PC机器,所以没法得到一些测试结果数据给大家,请有条件的朋友自己测试一下)。
在现在的系统中无时无刻都离不开多线程的思想,包括集群、分布式都可以理解为多线程的一种原理,那么什么是多线程的原理呢?多线程和多进程的是什么呢?
其实要实现分布最简单的思想就是多进程,其实类似于在系统分隔过程中的一种垂直分隔,将不同业务的系统分布在不同的节点上运行,他们彼此互不干扰,而多进程的申请、释放资源各方面的开销都很大,而且占用资源并非CPU级别的,而线程是属于进程内部更细节的内容,一个进程内部可以分配N个线程,这些线程会并行的征用CPU资源,如果你的机器是多核的处理器,并发将会带来异常的性能提升,其实原理上就是在有限的资源下,如何发挥出最大的性能优势(但是一定是资源有一定余量的情况下,正所谓做事不能做得太绝)。
在java中常用于实现多线程的方法有3中:
1、继承于Thread类,重写run方法
2、实现Runable接口,实现run方法
3、实现Callable接口,实现call方法(具有返回值)
至于调用的方法多种多样,可以直接用start启动,也可以使用java.util.concurrent.Executors来创建线程池来完成,创建的线程池也主要分为:
1、Executors.newSingleThreadScheduledExecutor() 创建一个顺序执行的线程池,你在run方法内部无需使用synchronized来同步,因为它本身是顺序的。
2、Executors.newCachedThreadPool()创建一个线程池,线程会并行的去执行它。
3、Executors.newFixedThreadPool(10)创建大小为10的一个线程池,这个线程池最多创建长度为10的队列,如果超过10个,就最多有10个线程在执行,即可以控制线程的数量,也可以让其并行执行。
如果你的系统是一个WEB应用,建议尽量不要再web应用中做多线程,因为这部分线程控制主要是由web容器控制的,如果在非得必要的情况下建立,尽量建立较少,或者尽量将可以不太频繁调度的线程使用完后直接释放掉,哪怕下次重建也无所谓。
如果你的多线程序是独立运行的,专门用于接受和和处理一些消息,那么我相信最少有一个线程是不断探测的(有很多程序会先休眠一点时间,如:TimeUnit.MINUTES.sleep(SLEEP_TIME)此方法是按照毫秒级进行休眠一段时间),这类程序,最好将线程设置为后台线程(setDaemon(true),一定要在线程调用run之前调用该方法有效),后台线程和非后台线程最大的区别在于:后台线程在所有非后台线程死掉后,后台线程自动会被杀死和回收;而正如你写其他的多线程程序,即使你的main方法完成(主线程),但是在main中申请的子线程没有完成,程序仍然不会结束。
总的来说,其实几乎每时每刻写的代码都是多线程的,只是很多事情容器帮助我们完成了,即使编写本地的AWT、SWING,也在很多控制处理中式异步的,只是这种异步相对较少,更多的异步可以由程序去编写,自定义的多线程一般用于在独立于前段容器应用的后台处理中。为什么类似web应用的前端会把多线程早就处理好呢,一个是因为为了减少程序和bug,另外一个就是要写好多线程的确不容易,这样会使得程序员去关心更多没有必要关心的东西,也需要程序员拥有很高的水准,但是如果要成为好的程序员就一定要懂多线程,我们接下来以几个问题入手,再进行说明:
如果一个系统专门用于时钟处理、触发器处理,这个系统可能是分布式的,那么在一个系统内部应该如何编写呢?另外多线程中编写的过程中我们最郁闷的事情、也是最难琢磨补丁的是什么:多线程现在的运行状况是怎样的?我的这个线程不能死掉,如果死掉了我怎么发现?发现到了如何处理(自动、人工、难道重启)?
带着这些问题,我们引出了文章下面的一些话题。
2、多线程应该注意些什么?
多线程用起来爽,出现问题你就不是那么爽了,简单说来,多线程你最纳闷的就是它的问题;但是不要害怕它,你害怕它就永远不能征服它,呵呵,只要摸清楚一些脾气,我们总有办法征服它的。
◆明白多线程有状态信息,和之间的转换规则?
◆多线程一般在什么情况下会出现焊住或者死掉的现象?
◆多线程焊住或者死掉如何捕获和处理?
这里仅仅是提出问题,提出问题后,在说到问题之前,先提及一下扩展知识点,下面的章节来说明这些问题。
开源多线程调度任务框架中的一个很好选择是:Quartz,有关它的文章可以到http://wenku.baidu.com/view/3220792eb4daa58da0114a01.html
下载这个文档,这个文档也讲述了大部分该框架的使用方法,不过由于该框架本身的封装层次较多,所以很多底层的实现内容并不是那么明显,而且对于线程池的管理基本是透明的,自己只能通过一些其他的手段得到这些内容。
所以拿到这个框架首先学习好它的特性后,进一步就是看如何进一步封装它得到最适合你项目的内容。
另外多线程在数据结构选项上也有很多技巧,关于多线程并发资源共享上的数据结构选型专门来和大家探讨,因为技巧的确很多,尤其是jdk 1.6以后提出了很多的数据结构,它参考了类似于oracle的版本号原理,在内存中做了数据复制以及原子拷贝的方法,实现了即保证一致性读写又在很大程度上降低了并发的征用;另外还有对于乐观锁机制,也是高性能的多线程设计中非常重要知识体系。
3、状态转换控制,如何解决死锁。
3.1.java默认线程的状态有哪些?(所谓默认线程就是自己没有重写)
NEW :刚刚创建的线程,什么也没有做,也就是还没有使用start命令启动的线程。
BLOCKED :阻塞或者叫梗阻,也就是线程此时由于锁或者某些网络原因造成阻塞,有焊住的迹象。
WAITING:等待锁状态,它在等待对一个资源的notify,即资源的一个锁机会,这个状态一般和一个静态资源绑定,并在使用中有synchronzed关键字的包装,当使用obj.wait()方法时,当前线程就会等待obj对象上的一个notify方法,这个对象可能是this,如果是this的话那么在方法体上面一般就会有一个synchronized关键字。
TIME_WAITDE:基于时间的等待,当线程使用了sleep命令后,就会处于时间等待状态,时间到的时候,恢复到running状态。
RUNNING:运行状态,即线程正在处于运行之中(当线程被梗阻)。
TERMINATED:线程已经完成,此时线程的isAlive()返回为false。
一般默认的线程状态就是这些,部分容器或者框架会把线程的状态等进行进一步的封装操作,线程的名称和状态的内容会有很多的变化,不过只要找好对应的原理也不会脱离于这个本质。
3.1.线程一般在什么情况下会死掉?
锁,相互交叉派对,最终导致死锁;可能是程序中自己导致,编写共享缓存以及自定义的一部分脱离于容器的线程池管理这里就需要注意了;还有就是有可能是分布式的一些共享文件或者分布式数据库的锁导致。
网络梗阻,网络不怕没有,也不怕太快,就怕时快时慢,现在的话叫太不给力了,伤不起啊!而国内现在往往还就是这样不给力;当去网络通信调用内容的时候(包括数据库交互一般也是通过网络的),就很容易产生焊住的现象,也就是假死,此时很难判定线程到底是怎么了,除非有提前的监控预案。
其他情况下线程还会死掉吗?就我个人的经验来说还没遇到过,但并非绝不可能,我想在常规的同一个JVM内部操作的线程会死掉的概率只有系统挂掉,不然SUN的java虚拟机也太不让人信任了;至少从这一点上我们可以决定在绝大部分情况下线程阻塞的主要原因是上述两个主要来源。
在明白绝大部分原因的基础上,这里已经提出了问题并初步分析了问题,那么继续来如何解决这些问题,或者说将问题的概率降低到非常低的程度(因为没有百分百的高可用性环境,我们只是要尽量去做到它尽量完美,亚马逊的云计算也有宕机的惊人时刻,呵呵)。
3.1. 多线程焊住或者死掉如何捕获和处理?
说到捕获,学习java朋友肯定第一想到的是try catch,但是线程假死根本不会抛异常,如何知道线程死掉了呢?
这需要从我们的设计层面下手,对于后来java提供的线程池也可以比较放心的使用,但是对于很多非常复杂的线程管理,需要我们自己来设计管理。如何捕获我们用一个生活中的例子来举例,下一长中将它反馈到实际的系统设计上。
首先多线程自己死掉了它肯定不知道,就想一个人自己喝醉了或者被被人打晕了一样,呵呵,那么如何才能知道它的现状了?提出两种现实思路,一个是有一个跟班的人,而另一种是它上面有一个领导带一群人出来玩,下面人丢了一个它肯定要去找。
先看看第一种思路,跟班那个我假如他平时什么也不做,就跟在前者后面,当发现前者倒下,自己马上跟上去顶替工作,这也是系统架构上经常采用的冗余主从切换,可能一主多从;而云计算也是在基础上的进一步做的异地分流切换和资源动态调度(也就是事情少了,这些人可以去做其他的事情或者睡觉养精神并且为国家节约粮食,当这边的事情忙不过来,会有做其它事情的人或者待命的人来帮着做这些事情;甚至于此地遭到地震洪水类天灾什么的,异地还有机构可以顶替同样的工作内容,这样让对外的服务永远不断间断下来,也就是传说中的24*7的高可用性服务),但是这样冗余太大,成本将会非常巨大。
再看看第二种服务,上面有一个老大,它过一小会看看这帮小弟在做什么,是不是遇到了困难,那里忙它在上面动态调配这资源;好像这种模式很好呢?小弟要是多了,它就忙不过来了,因为资源的分配是需要提前明白下面资源的细节的,不然这个领导不是好领导;那么再细想下去,我们可以用多个老大,每个老大带领一个小团队,团队之间可以资源调配,但是团队内部可以由老大自己掌控一切,老大的上面还有个老总它只关心于老大再做什么,而不需要关心小弟们的行为,这样大家的事情就平均起来了;那么问题了又出来了,小弟的问题是可以透明的看到了,要是那个老大出事了甚至于老总出事了怎么办?此时结合第一种思想,我们此时就只需要再老总下面挂一个跟班的,集合两种模式的特征,也就是小弟不需要配跟班的,这样就节约了很大的成本(因为叶子节点的数量是最多的),而上面的节点我们需要有跟班的,如果想最大程度节约成本,只需要让主节点配置一个或者多个跟班就可以,但是这样恢复成本就上去了,因为恢复信息需要逐层找到内容才行,一般我们没有必要在这个基础上再进一步去节约成本。
这些是现实的东西,如何结合到计算机系统架构中,再回到本文的多线程设计上,第四章中一起来探讨一下。
4、如何设计一个具有可扩展性的多线程处理器。
其实在第三章中,已经从生活的管理模式上找到了很多的解决方案,这也是我个人在解决问题上惯用的手段,因为个人认为再复杂的数学算法也没有人性本身的复杂,生活中的种种手段若用于计算机中可能会得到很多神奇的效果。
如果自己不使用任何开源技术,要做一个多线程处理的框架应该从何下手,在上面分析的基础上,我们一般会将一个专门处理多线程的系统至少分解为主次二层,也就是主线程引导多个运行线程去处理问题;好了,此时我们需要解决以下几个问题:
a)多个线程处理的内容是类似的,如何控制并发征用数据或者说降低并发热点的粒度。
方法1:hash散列思想将会是优秀的原则,按照数据特征进行分解数据框,每个框的数据规则按照一种hash规则分布,hash散列对于编程容易遍历,而且计算速度非常迅速,几乎可以忽略掉定位分组的时间,但结构扩展过程比较麻烦,但在多线程设计中一般不需要考虑这个问题。
方法2:range分布,range范 围分布数据是提前让管理者知道数据的大致分布情况,并按照一种较为平均的规则交给下面的运作线程去去处理自己范围内的数据,相互之间的数据也是没有任何交叉的,其扩展性较好,可以任意扩展,如果分解的数量不受控制的话,分解过多,会造成定位范围比较慢一点,但是多线程设计中也一般不用考虑这个问题,因为程序是由自己编写的。
方法3:位图分布,即数据具有位图规则,一般是状态,这种数据按照位图分布后,线程可以设立为位图个数,找到自己的位图段数据即可做操作,而不需要做进一步的更新,但是往往位图数量有限,而需要处理的数据量很大,一个线程处理一个位图下的所有数据也往往力不从心,若多个线程处理一个位图又会重蹈覆辙。
三种方法各自有优缺点,所以我们往往采用组合模式来讲真个系统的架构达到比较完美的状态,当然没有完美的东西,只有最适应于当前应用环境的架构,所以设计前需要考虑很多预见性问题;关于这种数据分布更多的用于架构,但是架构的基础也来源于程序设计思想,两者思想都是一致的,有关架构和数据存储分布,后面有机会单独讨论。
b)线程死掉如何发现(以及处理):
管理线程除有运行动作的线程外,还有1~N跟班,个数根据实际情况决定,至少要有一个当管理线程挂掉可以马上顶替工作,另外还有应当有一个线两程去定期检测线程的运行情况,由于它只负责这件事情,所以很简单,而且这一组中的线程谁死掉都可以相互替换工作并重启新的线程去替代,这个检测的周期不用太快、也不用太慢,只要应用可以接受就可以,因为挂掉些东西,应用阻塞一点时间是非常正常的事情。
发现线程有阻塞现象,在执行中找到了某种以外而阻塞,导致的原因我们上面已经分析过,解决的方法一般是在探测几次(这个次数一般是基于配置的)后发现都是处于阻塞状态,就基本可以认为它是错误的了;错误的情况此时需要给该线程执行一个interrupt()方法,此时线程内部的执行会自动的抛出一个异常,也就是理解执行线程的内容的时候尤其是带有网络操作的时候需要带上一个try catch,执行部分都在try中,当出现假死等现状的时候,外部探测到使用一个interrupt()方法,运行程序就会跳转到catch之中,这个里面就不存在征用资源的问题,而快速的将自己的需要回滚的内容执行完,并认为线程执行结束,相应的资源也会得到释放,而使用stop方法之所以现在不推荐是因为它不会释放资源,会导致很多的问题。
另外写代码之前如果涉及到一些网络操作,一定要对你所使用的网络交互程序有很多的深入认识,如socket交互时,一般情况下如果对方由于网络原因(一般是有IP当时端口不对或者网段的协议不通)导致在启动连接对方时,socket连接对方好几分钟后才会显示是超时连接,这是默认的,所以你需要提前设置一个启动连接超时保证网络是可以通信的,再进行执行(注意socket里面还有一个超时是连接后不断的时间,前者为连接之前设置的一个启动连接超时时间,一般这个时间很短,一般是2秒就很长了,因为2秒都连接不上这个网络就基本连接不上了,而后者是运行,有些交互可能长达几小时也有可能,但类似这种交互建议采用异步交互,以保证稳定运行)。
C)如果启动和管理二级管理线程组:
上面有一个主线程来控制启动和关闭,这里可以将这些线程在start前的setDaemon(true),那么该线程将会被设立为后台线程,所谓后台线程就是当主线程执行完毕释放资源后,被主线程创建的这些线程将会自动释放资源并死掉,如果一个线程被设置为后台线程,若在其run方法内部创建的其他子线程,将会自动被创建为后台线程(如果在构造方法中创建则不是这样)。
管理线程也可以像二级线程一样来管理子节点,只要你的程序不怕写得够复杂,虽然需要使用非常好的代码来编写,并且需要通过很复杂的测试才会稳定运行,但是一旦成功,这个框架将会是非常漂亮和稳定,而且也是高可用的。
5、多线程在多主机下的扩展-集群
其实我们在上面以及提及了一些分布式的知识,也可以叫做数据的分区知识(在网络环境利用PC实现类似于同一个主机上的分区模式,基本就可以称为数据是分布式存储的)。
但是这里提到的集群和这个有一些区别,可以说分布式中包含了集群的概念,但是一般集群的概念也有很多的区别,并且要分app集群和数据库集群。
集群一般是指同一个机组下多个节点(同一台机器也可以部署多个节点),这些节点几乎去完成同样的事情,或者说类似的事情,这就和多线程扯在一起了,多线程也正是如此,对比来看就是多线程调度在多主机群组下的实现,所以参照app集群来说,一般有一个管理节点,它几乎干很少的事情,因为我们不想让它挂掉,因为他虽然干的事情少,但是却非常重要,一个是从它那里可以得到每一个节点的一些应用部署和配置,以及状态等等信息;另外是代理节点或者叫做分发节点,它几乎在管理节点的控制之下只做分发的,当然要保证session一致性。
集群在多线程中的另一个体现就是挂掉一台,其余的可以顶替,而不会导致全盘死掉;而集群组相当于一个大的线程组,相关牵制管理,也相互可以失败切换,而多个业务会或者多种工具项会划分为不同的集群组,这就类似于我们设计线程中的三层线程模式的中多组线程组的模式,每组线程组内部都有自己个性化的属性和共享属性。
而面对数据库集群,就相对比app集群要复杂,app在垂直扩展时几乎只会受到分发节点能力的限制,而这部分是可以调整的,所以它在垂直扩展的过程中非常方便,而数据库集群则不一样,它必须保证事务一致性,并实现事务级别切换和一定程度上的网格计算能力,中间比较复杂的也在内存这块,因为它的数据读入到内存中要将多个主机的内存配置得像一个内存一样(通过心跳完成),而且需要得到动态扩展的能力,这也是数据库集群下扩展性收到限制发展的一个原因之一。
App难道没有和数据库一样的困难吗?有,但是粒度相对较小,app集群一般不需要考虑事务,因为一个用户的session一般在不出现宕机的情况下,是不会出现复制要求的,而是一直会访问指定的一台机器,所以它们之间几乎不需要通信;而耦合的粒度在于应用本身的设计,有部分应用系统会自己写代码将一些内容初始化注入到内存中,或者注入到app本地的一个文件中作为文件缓存;这样当这些数据发生改变时他们先改数据库,再修改内存或者通知内存失效;数据库由于集群使用心跳连接,所以保持一致性,而app这边的数据由于只修改掉了自身的内存相关信息,没有修改掉其他机器的内存信息,所以必然导致访问其他数据的机器上的内容是不一致的;至于这部分的解决方案,根据实际项目而定,有通过通信完成的,也有通过共享缓冲区完成(但这种方式又回到共享池资源征用产生的锁了),也有通过其他方式完成。
大型系统架构最终数据分布,集中式管理,分布式存储计算,业务级别横向切割,同业务下app垂直分隔,数据级别散列+range+位图分布结构,异地分流容灾,待命机组和资源调配的整合,这一切的基础都来源于多线程的设计思想架构在分布式机组上的实现。
6、WEB应用的多线程以及长连接原理
WEB应用中会对一些特殊的业务服务做特殊的服务器定制,类似一些高并发访问系统甚至于专门用于瞬间高并发的系统(很多时候系统不怕高并发,而是怕瞬间高并发)但他们的访问往往比较简单,主要用于事务的处理以及数据的一致性保障,他们在数据的处理上要求在数据库端也不允许有太大的计算量,计算一般在app中去完成,数据库一般只是做存、取、事务一致性动作,这类一般属于特殊的OLTP系统;还有大分类一类是属于并发量不算太大,但每次处理的数据和计算往往比较多,一把说的是OLAP类的系统,而数据的来源一般是OLTP,OLAP每次处理的数据量可能会非常大,一般在类型收集和统计上进行数据dump,需要将OLTP中的数据按照某种业务规则方面查询和检索的方法提取出来组织为有效信息存储在另一个地方,这个地方有可能还是数据库,但也有可能不是(数据库的计算能力虽然是数据上最强的但是它在实际应用中它是最慢的一种东西,因为数据库更多的是需要保证很多事务一致性和锁机制问题,以及一些中间解析和优化等等产生的开销是非常大的,而且应用程序与之交互过程是需要通过网络完成,所以很多数据在实际的应用中并不一定非要用数据库);这两类系统在设计和架构上都有很大的区别,但普通系统两者都有特征,但是都不是那么极端,所以不用考虑太多,这里需要提到的是一类非常特殊的系统,是实时性推送数据并高并发的系统,到目前为止我个人不知道将它归并到哪一类系统中,这的确很特殊的一类系统。
这类系统如:高并发访问中,而且需要将同一个平台下的数据让客户端较为实时的得到内容,这类网站不太可能一次获取非常多的内容到客户端再访问,而肯定是通过很多异步交互过程来完成的,下面简单说下这个异步交互。
Web异步交互的所有框架基础都是ajax,其余的类似框架都是在这个基础上完成的;那么此时ajax应该如何来控制交互才能得到几乎接近于实时的内容呢?难道通过客户端不断去刷新相同的URL?那要是客户端非常多,类似于一个大型网站,可能服务器端很快会宕机,除非用比正常情况高出很多倍的服务器成本去做,而且更多的服务器可能在架构上也需要改造才能发挥出他们的性能(因为在服务器的架构上,1 + 1永远是小于2的性能,更多的服务器在开销)。
想到的另一种办法就是从服务器端向客户端推送数据,那么问题是如何推送,这类操作是基于一种长连接机制完成,长连接即不断开的连接,客户端采用ajax与后端通信时,后端的反馈信息只要未曾断开就可视为一种长连接的机制;很多是通过socket与服务器端通信,也可以使用ajax,不过ajax需要在其上面做很多的处理才行。
服务器端也是必须使用对应的策略,现在较多的是javaNIO,相对BIO性能要低一点,但是也是很不错的,它在获取到用户请求时并不是马上为用户请求分配线程去处理,而是将请求进行排队,而排队的过程可以自己去控制粒度,而线程也将作为线程池的队列进行分配处理,也就是服务器端对客户端的请求是异步响应(注意这里不是ajax单纯的异步交互,而是服务器端对请求的异步响应),它对很多请求的响应并非及时,当发生数据变化时,服务器第一时间通过请求列表获取到客户端session列表并与之输出内容,类似于服务器端主动推送数据向客户端;而异步交互的好处是服务器端并不会为每一个客户端分配或新申请一个线程,这样会导致高并发时引起的资源分配不过来导致的内存溢出现象;解决了上述两个问题后,另外还有一个问题需要解决的是,当一个线程在处理一个请求任务时,由于线程处理一个任务完成前除非死掉或者焊住,否则是不会断开下来的,这个是肯定的(我们可以将一些大任务切割为一些小任务,线程就处理的速度就会快很多了),但是有一个问题是,服务器端的这个线程可能很快处理好了需要处理的数据内容并向客户端推送,但是客户端由于各类网络通信问题,导致迟迟不能接受完成,此时该线程也会被占用些不必要的时间,那么是否在这个中间需要进一步做一层断点传送的缓存呢?缓存不仅仅是属于在断点数据需要时取代应用服务器的内容,异步断点向客户端输出信息,同时将应用服务器处理的时间几乎全部集中在数据和业务处理,而不是输出网络上的很多占用,有关网络缓存有很多种做法,后续有机会和大家一起探讨关于网络缓存的知识吧。