Another Java Geek

其实我不是geek,我和你一样,我是凡客

正在浏览 并发 里的文章

在我们的web服务程序中,对每一个流入的用户请求,都需要执行N个不同任务才能完成整个处理过程。执行这些任务的多线程并发处理策略经历了几次演变。 在一开始的时候(约两年前),由于历史原因,对每一个任务,都是新开一个线程,如下: Thread t = new Thread(task); t.start(); 所以对每个用户请求,在请求处理主线程中,都new了N个线程,每个线程的执行结果通过polling方式获取,即主线程在busy wait中轮循。这种方式非常不好:新创建的线程,执行一次任务,就被销毁,不能重用,线程创建/销毁的开销是很大的;而且不能限制资源消耗,只要有用户请求流入,就会不断地创建新线程,若流量太大,服务器很容易崩掉;且请求处理主线程存在busy wait,弊端多多,参见Future模式。 因此,对并发处理策略进行了改造(约一年前):通过线程池和Future模式,实现了在一个共享线程池中执行所有用户请求的所有任务。这样基本上避免了线程创建/销毁的开销(在服务启动时会一次性创建线程池中所有线程),可以通过设定线程池大小来限制服务器的资源占用进而提升稳定性,消除了busy wait。 随着时间的推移,共享线程池的弊端也慢慢浮现出来。由于N种不同任务的执行耗时是不一样的,线程池中总的线程数有限,因此耗时最长的一种任务就会较多地占用线程池中的线程,当流量低于服务器处理能力时,这不会有什么问题。而当流量超出服务器承载能力时,那些执行时间较短的任务就得不到可用的线程去执行,进而导致请求阻塞且不会超时,最终造成整个服务处理效率低下,响应非常缓慢。流量超出服务器处理能力越多,每种任务的执行耗时差别越大,后果越明显。 针对共享线程池的问题,再次进行了改造(约一月前):按照任务类型的不同,将共享线程池分割为多个线程池,同种任务由同一个线程池来处理,避免了不同任务之间竞争资源,造成某些任务得不到可用线程而饿死。这样虽然在流量过大时,同样会存在问题(这是不可避免的),但可以通过不同的线程池,为每种任务设定不同地资源上限,预留最少资源,来缓解共享线程池的问题。这种分割线程池的方式,是物理划分,也可以通过物理共享、逻辑划分的方式来实现,在淘宝数据平台博客上有一篇支持配额的共享线程池即是如此。综合来看,物理划分和逻辑划分两种方式是不相上下的,在我们当前的应用场景中,物理划分的方式基本上算是做到了尽头。

在实际开发过程中,常见使用Double Check Locking(DCL)实现延迟初始化的单例模式。在Java中,虽然这早已被证实是一种有害的编程习惯,但这并不能阻止它在程序员之间的传播。DCL存在线程安全隐患,不少Java编程书上有关于这个问题的介绍,网上也很多讨论。不过通常情况(并发量不大、安全性要求不高)下,它能够工作得很好,这大概就是它得以流传的原因吧。 DCL是一种反模式,典型的DCL如下: public class Singleton { private static Singleton instance = null; private Singleton() { initialize(); } public static Singleton getInstance() { if (instance == null) { sychronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } 按照网上人们对DCL的分析,线程安全问题在于instance = new Singleton();这行代码:第一个线程执行到此处时,可能是先分配对象的内存空间,在尚未初始化对象之前,已将这块空间的地址赋值给instance变量,之所以这样,是因为编译器会执行指令重排(statement reordering)的优化,使得实际执行的指令顺序并非按照语句的自然顺序,即乱序(out of order)执行。进而导致第二个线程在外层if条件处跳过,直接返回instance引用,使得对象处在不完整的状态,这被称为Unsafe [...]

线程同步是进行多线程编程时所必须考虑的一个问题。之所以要进行同步,是因为多个线程需要访问共享资源,典型的是共享内存数据。如果能为每个线程提供一份需要共享的数据的copy,那么对该数据的访问也就没有必要进行同步了。Thread Local Storage就是能够达到这个目的的一个多线程设计模式。Thread Local Storage,顾名思义,就是“线程本地数据”,指每个线程拥有各自独立的数据拷贝。Thread Local Storage还有另外一些称呼:Thread Specific Storage,Thread Specific Data等。 Java类库中的ThreadLocal类就是该模式的一个实现。在该类的api文档中有如此说明:ThreadLocal类型的变量不同于普通变量,每个访问它的线程都有一份各自独立初始化的copy,对它的访问是通过get/set方法实现的。ThreadLocal实例典型情况下是类的private static字段。 下面一段程序使用了ThreadLocal类。这个程序假定用于对网站的访问日志进行某种处理,如果有N个日志文件需要处理,就启动N个线程,需要记录处理每个日志文件所花费的时间。 public class LogStats { private static final ThreadLocal<String> logFile = new ThreadLocal<String>(); private static final ThreadLocal<Long> startTime = new ThreadLocal<Long>(); public static void init(String logFileName) { logFile.set(logFileName); startTime.set(System.currentTimeMillis()); } public static void process() { open(logFile.get()); readAndProcessEachRecord(); long time = System.currentTimeMillis() – [...]

在做应用开发时,有时会遇到一种场景:在完成某个主任务的同时,需要处理一些其它的子任务,为了加快响应速度,会将子任务放在另外的线程中执行。例如,搜索引擎在处理用户的查询请求时,不仅要从本地数据库查找匹配的结果,同时可能会向远程的广告服务器请求广告数据,最后将搜索结果和广告一起返回给用户。在这个例子中,主线程从本地查找搜索结果,同时启动子线程从远程服务器获取广告数据,当主线程查找结束时,会将子线程得到的广告数据与搜索结果合并,最后发送给用户。Java代码如下: public void searchService(query) { AdThread adThread = new AdThread(); adThread.start(); findResult(query); respond2User(); } 主线程启动子线程后,子线程的执行已不受主线程的控制,其何时执行完毕,主线程无法预知,而其执行结果是由主线程来主动索取的。为了做到这一点,就需要用到Future模式。 Future模式是现实中提货单的抽象,好比去摄影店拍照,照片需要过些时候才能洗出来,而我们不可能一直等下去,商家一般会给我们一张单据,并告知第二天10:00以后凭此单领取照片,而我们就可以暂时离开去做其它事情,等到第二天再带着单据来到摄影店领取照片,如果我们9:30就到了,照片还没有洗出来,我们就会继续等一会儿,直到照片洗出来。以下代码用Future模式实现前述的主线程和广告子线程之间的协作: 主线程: public void searchService(query) { FutureAd future = startAdThread(); SearchResult result = findResult(query); Ad ad = future.getAd(); respond2User(result, ad); } /* 返回FutureAd对象,类似提货单,主线程稍后通过FutureAd获取准备好的广告数据 */ FutureAd startAdThread() { FutureAd future = new FutureAd(); new AdThread(future).start(); return future; } 广告线程: public class [...]