Java并发工具包对并发编程的优化

2016-12-26 21:51史广
吉林省教育学院学报 2016年8期

史广

摘要:随着Java并发(concurrem)工具包的推出,并发程序的开发方式得到了极大的优化。较之以往的多线程设计机制,论文从四个方面深入探讨了并发(conCurrent)工具包是如何提升并发编程效能的。

关键词:并发;多线程;java

doi:10.16083/j.cnki.1671-1580.2016.08.025

中图分类号:TP311

文献标识码:A

文章编号:1671-1580(2016)08-0078-04

在JDKl.5出现之后,Sun推出了并发(concur-rent)工具包以简化并发编程,它为开发者提供了更为实用的并发程序模型,使得编写高效、易维护、结构清晰的Java多线程程序更为容易。

一、简化了线程的管理,提升了线程的执行效能

在JDKl.5之前,使用newThread()方式定义线程,需要考虑线程的创建、结束和结果的获取等诸多细节。尤其在需要很多线程时,线程的管理就会变得比较困难。不仅如此,使用newThread()方式定义线程,在时间和空间效率方面都存在不足。比如,定义好的线程不能重复利用;每次使用线程都需要向系统申请资源重新创建。另外,线程的创建和启动都需要一定的时间,这也会影响程序执行效率。

线程池(ThreadPool)是并发(Concurrent)工具包中引入的机制,它对以上问题进行了很好的优化。线程池通过一个缓存池的空间预先创建了一部分线程,在我们需要使用的时候就从里面直接将线程资源取出来使用。在线程使用完毕之后,线程池可以将线程回收进行重复利用。因此,在对性能要求较高或线程请求较多的情况下,线程池是一个很理想的选择。

上述程序创建了一个可缓存线程池(cachedThread Pool):只需通过循环的方式,便可把想要完成的任务数量传递给线程池,线程池会创建尽可能多的必须线程来并行执行。一旦前面的线程执行结束后可以被重复使用。

除了可缓存线程池(cached Thread Pool),定长线程池(Fix Thread Pool)可以创建固定数量的线程。在线程都被使用之后,后续申请使用的线程都会被阻塞。如果我们将上述代码中的new.CachedThreadPool改为newFixedThreadPool(2),括号中的数字2,表示创建了两个线程。下面的代码不变,则相等于把5个任务交给两个线程来完成。这就意味着,每一个线程完成一次任务后,并不会就此消逝,而是继续完成剩下的任务,直到所有任务完成。可见线程池确实简化了线程的管理,提升了线程的执行效能

二、强化了对多线程并发的控制能力。简化了控制线程间协调合作的方法

在并发(concurrent)工具包中,包含了一些同步辅助类,使得开发者能够更加轻松的对多线程进行协调控制,丰富了多个线程间协作的方式。下面我们以闭锁(countDownLatch)和信号量(sema-phore)为例进行说明。

1.闭锁(countDownLatch)是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。闭锁(CountDownLatch)以一个给定的计数初始化,每调用一次countDown(),这一数量就减一,然后通过调用await()方法,将阻塞线程,使其等待直到计数到达零。

在主方法中,除t1和t2线程外,还有语句“Sys.tern.out.println(“HelloWorld”)”所在的主线程,三个线程本来没有执行的先后顺序,但是如果运行代码,会发现主线程只在t1和t2完成后,才会执行。这就是因为我们对线程执行的顺序进行了人工干预,await()方法会一直阻塞主线程,直到countDown()方法将计数倒数至0。

2.信号量(Semaphore),有时被称为信号灯,它负责协调各个线程,以保证它们能够正确、合理的使用公共资源。信号量可以控制某个资源可被同时访问的个数,拿到信号量的线程可以进入代码,否则就等待,通过acquire()和release()获取和释放访

从上面代码中看出线程执行的任务是连接数据库。当线程成功连接数据库两秒后,连接将自动断开。重要的是,由于服务器资源有限,在给定时间内,可以提供的连接数必须受到严格控制。如果此时有200个线程同时访问数据库资源,但服务器在同一时问内只提供10个连接端口,这种情况就需要信号量来实现对线程的有效管控。

在上述代码中,将信号量定义为10,每条线程在进行数据库连接时,必须首先通过acquire()获得信号,并且在断开数据库连接后,通过release()释放获得的信号,以便其他线程获取。也就是说,虽然有200个线程同时想要进行数据库连接,但是在同一时间内,只能有10个线程获得信号来并发执行。可以看出,信号量的应用,丰富了我们对线程的管控方式,也简化了操作过程。

三、提供了多个具有线程安全性的类

在iaval.5出现之前,多线程访问共享数据时造成的数据访问冲突问题,往往需要耗费程序员大量时间进行调试规避。但是在并发(Concurrent)工具包中,提供的很多类本身就是线程安全的,这样大大简化了并发编程的难度。比如前文中提到的闭锁就是一个线程安全类。在闭锁的示例代码中,对象latch被两条线程并发共享,且没有synchronized关键字锁定,但程序并不会出现访问冲突。除Count.DownLatch以外,阻塞队列(BlockingQueue)的线程安全性也为开发并发程序带来了极大方便。

在经典的“生产者”(producer)和“消费者”(con-sumer)模型中,通过队列可以很便利地实现两者之问的数据共享。但是,在生产者和消费者数据处理速度不匹配,且生产者产出数据的速度远大于消费者消费速度的情况下,生产者必须在数据累积到一定程度时暂停下来(阻塞生产者线程),以便消费者线程把累积的数据处理完毕。然而,在并发(concur-rent)工具包发布以前,开发者不仅需要考虑所有上述细节,还要兼顾效率和线程安全,开发的复杂度可见一斑。阻塞队列很好地解决了如何在“生产者”(producer)和“消费者”(consumer)之间高效安全“传输”数据的问题。阻塞队列是一个高效并且线程安全的队列类,它为我们快速搭建高质量的多线程程序带来极大的便利。

上面是一个典型的“生产者”(producer)和“消费者”(consumer)模型,生产者和消费者共享阻塞队列queue。生产者的生产速度远大于消费者,因为消费者每取出一个数据需要等待100毫秒。幸运的是,阻塞队列首先是一个线程安全队列,其次,当生产者将队列长度填充到10时,put()方法会进入等待状态,同理,当队列长度缩减到0时take()也会进行等待。这就使得使用阻塞队列开发多线程常见模型的复杂度大大降低。

四、针对并发程序可能引起的死锁问题给出了更为便捷的解决方案

当线程需要同时获得多个互斥锁才可运行时,如果有多条线程并行且获取互斥锁的顺序不同,就可能引发死锁(DeadLock)现象。

在并发(concurrent)工具包中,引入了重入锁(Re-entrantlock)。重入锁的基本原理和synchronized语句块相似,都是通过加互斥锁的方式限定线程的行为,它们的第一个不同点在于,首先当需要加一个以上的互斥锁时,使用Reentrant lock避免了像synchronized语句块一样的嵌套。更为重要的是,re-entrant lock有tryLock()功能,它使得线程在需要同时获得多个互斥锁的情况下,具备了更加灵活的应对机制。

上述代码中,acquireLocks()方法的作用就是帮助线程分别获得两个互斥锁,当只获得一个,另一个获取失败的情况下,由于tryLock()方法的作用,使得程序可以在此情况下,放弃已经获得的互斥锁,以便其它线程同时获取。这也就很巧妙的避免了死锁现象的出现。

五、结语

并发(concurrent)工具包的推出,使得原来很麻烦的并发处理得以轻松完成,它实现了很多{avathread原生API很费时才能实现的功能。并发(Con.current)工具包的优点可以概括为:简化了线程的管理,提升了线程的执行效能;强化了对多线程并发的控制能力,简化了控制线程问协调合作的方法;提供了多个具有线程安全性的类;针对并发程序可能引起的死锁等问题给出了更为便捷的解决方案。当然,随着iava语言的发展,我们相信新的工具包还会不断更新,来满足日益繁杂的开发需求。