线程.docx
- 文档编号:25966070
- 上传时间:2023-06-16
- 格式:DOCX
- 页数:32
- 大小:108.86KB
线程.docx
《线程.docx》由会员分享,可在线阅读,更多相关《线程.docx(32页珍藏版)》请在冰豆网上搜索。
线程
java.util.concurrent介绍
java.util.concurrent包含许多线程安全、测试良好、高性能的并发构建块。
不客气地说,创建java.util.concurrent的目的就是要实现Collection框架对数据结构所执行的并发操作。
通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性。
如果一些类名看起来相似,可能是因为java.util.concurrent中的许多概念源自DougLea的util.concurrent库(请参阅参考资料)。
JDK5.0中的并发改进可以分为三组:
•JVM级别更改。
大多数现代处理器对并发对某一硬件级别提供支持,通常以compare-and-swap(CAS)指令形式。
CAS是一种低级别的、细粒度的技术,它允许多个线程更新一个内存位置,同时能够检测其他线程的冲突并进行恢复。
它是许多高性能并发算法的基础。
在JDK5.0之前,Java语言中用于协调线程之间的访问的惟一原语是同步,同步是更重量级和粗粒度的。
公开CAS可以开发高度可伸缩的并发Java类。
这些更改主要由JDK库类使用,而不是由开发人员使用。
•低级实用程序类--锁定和原子类。
使用CAS作为并发原语,ReentrantLock类提供与synchronized原语相同的锁定和内存语义,然而这样可以更好地控制锁定(如计时的锁定等待、锁定轮询和可中断的锁定等待)和提供更好的可伸缩性(竞争时的高性能)。
大多数开发人员将不再直接使用ReentrantLock类,而是使用在ReentrantLock类上构建的高级类。
•高级实用程序类。
这些类实现并发构建块,每个计算机科学文本中都会讲述这些类--信号、互斥、闩锁、屏障、交换程序、线程池和线程安全集合类等。
大部分开发人员都可以在应用程序中用这些类,来替换许多(如果不是全部)同步、wait()和notify()的使用,从而提高性能、可读性和正确性。
本教程将重点介绍java.util.concurrent包提供的高级实用程序类--线程安全集合、线程池和同步实用程序。
这些是初学者和专家都可以使用的"现成"类。
在第一小节中,我们将回顾并发的基本知识,尽管它不应取代对线程和线程安全的了解。
那些一点都不熟悉线程的读者应该先参考一些关于线程的介绍,如"IntroductiontoJavaThreads"教程(请参阅参考资料)。
接下来的几个小节将研究java.util.concurrent中的高级实用程序类--线程安全集合、线程池、信号和同步工具。
最后一小节将介绍java.util.concurrent中的低级并发构建块,并提供一些性能测评来显示新java.util.concurrent类的可伸缩性的改进。
什么是线程?
所有重要的操作系统都支持进程的概念--独立运行的程序,在某种程度上相互隔离。
线程有时称为轻量级进程。
与进程一样,它们拥有通过程序运行的独立的并发路径,并且每个线程都有自己的程序计数器,称为堆栈和本地变量。
然而,线程存在于进程中,它们与同一进程内的其他线程共享内存、文件句柄以及每进程状态。
今天,几乎每个操作系统都支持线程,允许执行多个可独立调度的线程,以便共存于一个进程中。
因为一个进程中的线程是在同一个地址空间中执行的,所以多个线程可以同时访问相同对象,并且它们从同一堆栈中分配对象。
虽然这使线程更易于与其他线程共享信息,但也意味着您必须确保线程之间不相互干涉。
正确使用线程时,线程能带来诸多好处,其中包括更好的资源利用、简化开发、高吞吐量、更易响应的用户界面以及能执行异步处理。
Java语言包括用于协调线程行为的原语,从而可以在不违反设计原型或者不破坏数据结构的前提下安全地访问和修改共享变量。
线程有哪些功能?
在Java程序中存在很多理由使用线程,并且不管开发人员知道线程与否,几乎每个Java应用程序都使用线程。
许多J2SE和J2EE工具可以创建线程,如RMI、Servlet、EnterpriseJavaBeans组件和SwingGUI工具包。
使用线程的理由包括:
•更易响应的用户界面。
事件驱动的GUI工具包(如AWT或Swing)使用单独的事件线程来处理GUI事件。
从事件线程中调用通过GUI对象注册的事件监听器。
然而,如果事件监听器将执行冗长的任务(如文档拼写检查),那么UI将出现冻结,因为事件线程直到冗长任务完毕之后才能处理其他事件。
通过在单独线程中执行冗长操作,当执行冗长后台任务时,UI能继续响应。
•使用多处理器。
多处理器(MP)系统变得越来越便宜,并且分布越来越广泛。
因为调度的基本单位通常是线程,所以不管有多少处理器可用,一个线程的应用程序一次只能在一个处理器上运行。
在设计良好的程序中,通过更好地利用可用的计算机资源,多线程能够提高吞吐量和性能。
•简化建模。
有效使用线程能够使程序编写变得更简单,并易于维护。
通过合理使用线程,个别类可以避免一些调度的详细、交叉存取操作、异步IO和资源等待以及其他复杂问题。
相反,它们能专注于域的要求,简化开发并改进可靠性。
•异步或后台处理。
服务器应用程序可以同时服务于许多远程客户机。
如果应用程序从socket中读取数据,并且没有数据可以读取,那么对read()的调用将被阻塞,直到有数据可读。
在单线程应用程序中,这意味着当某一个线程被阻塞时,不仅处理相应请求要延迟,而且处理所有请求也将延迟。
然而,如果每个socket都有自己的IO线程,那么当一个线程被阻塞时,对其他并发请求行为没有影响。
线程安全
如果将这些类用于多线程环境中,虽然确保这些类的线程安全比较困难,但线程安全却是必需的。
java.util.concurrent规范进程的一个目标就是提供一组线程安全的、高性能的并发构建块,从而使开发人员能够减轻一些编写线程安全类的负担。
线程安全类非常难以明确定义,大多数定义似乎都是完全循环的。
快速Google搜索会显示下列线程安全代码定义的例子,但这些定义(或者更确切地说是描述)通常没什么帮助:
•...canbecalledfrommultipleprogrammingthreadswithoutunwantedinteractionbetweenthethreads.
•...maybecalledbymorethanonthreadatatimewithoutrequiringanyotheractiononthecaller'spart.
通过类似这样的定义,不奇怪我们为什么对线程安全如此迷惑。
这些定义几乎就是在说"如果可以从多个线程安全调用类,那么该类就是线程安全的"。
这当然是线程安全的解释,但对我们区别线程安全类和不安全类没有什么帮助。
我们使用"安全"是为了说明什么?
要成为线程安全的类,首先它必须在单线程环境中正确运行。
如果正确实现了类,那么说明它符合规范,对该类的对象的任何顺序的操作(公共字段的读写、公共方法的调用)都不应该使对象处于无效状态;观察将处于无效状态的对象;或违反类的任何变量、前置条件或后置条件。
而且,要成为线程安全的类,在从多个线程访问时,它必须继续正确运行,而不管运行时环境执行那些线程的调度和交叉,且无需对部分调用代码执行任何其他同步。
结果是对线程安全对象的操作将用于按固定的整体一致顺序出现所有线程。
如果没有线程之间的某种明确协调,比如锁定,运行时可以随意在需要时在多线程中交叉操作执行。
在JDK5.0之前,确保线程安全的主要机制是synchronized原语。
访问共享变量(那些可以由多个线程访问的变量)的线程必须使用同步来协调对共享变量的读写访问。
java.util.concurrent包提供了一些备用并发原语,以及一组不需要任何其他同步的线程安全实用程序类。
令人厌烦的并发
即使您的程序从没有明确创建线程,也可能会有许多工具或框架代表您创建了线程,这时要求从这些线程调用的类是线程安全的。
这样会对开发人员带来较大的设计和实现负担,因为开发线程安全类比开发非线程安全类有更多要注意的事项,且需要更多的分析。
AWT和Swing
这些GUI工具包创建了称为时间线程的后台线程,将从该线程调用通过GUI组件注册的监听器。
因此,实现这些监听器的类必须是线程安全的。
TimerTask
JDK1.3中引入的TimerTask工具允许稍后执行任务或计划定期执行任务。
在Timer线程中执行TimerTask事件,这意味着作为TimerTask执行的任务必须是线程安全的。
Servlet和JavaServerPage技术
Servlet容器可以创建多个线程,在多个线程中同时调用给定servlet,从而进行多个请求。
因此servlet类必须是线程安全的。
RMI
远程方法调用(remotemethodinvocation,RMI)工具允许调用其他JVM中运行的操作。
实现远程对象最普遍的方法是扩展UnicastRemoteObject。
例示UnicastRemoteObject时,它是通过RMI调度器注册的,该调度器可能创建一个或多个线程,将在这些线程中执行远程方法。
因此,远程类必须是线程安全的。
正如所看到的,即使应用程序没有明确创建线程,也会发生许多可能会从其他线程调用类的情况。
幸运的是,java.util.concurrent中的类可以大大简化编写线程安全类的任务。
例子--非线程安全servlet
下列servlet看起来像无害的留言板servlet,它保存每个来访者的姓名。
然而,该servlet不是线程安全的,而这个servlet应该是线程安全的。
问题在于它使用HashSet存储来访者的姓名,HashSet不是线程安全的类。
当我们说这个servlet不是线程安全的时,是说它所造成的破坏不仅仅是丢失留言板输入。
在最坏的情况下,留言板数据结构都可能被破坏并且无法恢复。
publicclassUnsafeGuestbookServletextendsHttpServlet{
privateSetvisitorSet=newHashSet();
protectedvoiddoGet(HttpServletRequesthttpServletRequest,
HttpServletResponsehttpServletResponse)throwsServletException,IOException{
StringvisitorName=httpServletRequest.getParameter("NAME");
if(visitorName!
=null)
visitorSet.add(visitorName);
}
}
通过将visitorSet的定义更改为下列代码,可以使该类变为线程安全的:
privateSetvisitorSet=Collections.synchronizedSet(newHashSet());
如上所示的例子显示线程的内置支持是一把双刃剑--虽然它使构建多线程应用程序变得很容易,但它同时要求开发人员更加注意并发问题,甚至在使用留言板servlet这样普通的东西时也是如此。
线程安全集合
JDK1.2中引入的Collection框架是一种表示对象集合的高度灵活的框架,它使用基本接口List、Set和Map。
通过JDK提供每个集合的多次实现(HashMap、Hashtable、TreeMap、WeakHashMap、HashSet、TreeSet、Vector、ArrayList、LinkedList等等)。
其中一些集合已经是线程安全的(Hashtable和Vector),通过同步的封装工厂(Collections.synchronizedMap()、synchronizedList()和synchronizedSet()),其余的集合均可表现为线程安全的。
java.util.concurrent包添加了多个新的线程安全集合类(ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet)。
这些类的目的是提供高性能、高度可伸缩性、线程安全的基本集合类型版本。
java.util中的线程集合仍有一些缺点。
例如,在迭代锁定时,通常需要将该锁定保留在集合中,否则,会有抛出ConcurrentModificationException的危险。
(这个特性有时称为条件线程安全;有关的更多说明,请参阅参考资料。
)此外,如果从多个线程频繁地访问集合,则常常不能很好地执行这些类。
java.util.concurrent中的新集合类允许通过在语义中的少量更改来获得更高的并发。
JDK5.0还提供了两个新集合接口--Queue和BlockingQueue。
Queue接口与List类似,但它只允许从后面插入,从前面删除。
通过消除List的随机访问要求,可以创建比现有ArrayList和LinkedList实现性能更好的Queue实现。
因为List的许多应用程序实际上不需要随机访问,所以Queue通常可以替代List,来获得更好的性能。
弱一致的迭代器
java.util包中的集合类都返回fail-fast迭代器,这意味着它们假设线程在集合内容中进行迭代时,集合不会更改它的内容。
如果fail-fast迭代器检测到在迭代过程中进行了更改操作,那么它会抛出ConcurrentModificationException,这是不可控异常。
在迭代过程中不更改集合的要求通常会对许多并发应用程序造成不便。
相反,比较好的是它允许并发修改并确保迭代器只要进行合理操作,就可以提供集合的一致视图,如java.util.concurrent集合类中的迭代器所做的那样。
java.util.concurrent集合返回的迭代器称为弱一致的(weaklyconsistent)迭代器。
对于这些类,如果元素自从迭代开始已经删除,且尚未由next()方法返回,那么它将不返回到调用者。
如果元素自迭代开始已经添加,那么它可能返回调用者,也可能不返回。
在一次迭代中,无论如何更改底层集合,元素不会被返回两次。
CopyOnWriteArrayList和CopyOnWriteArraySet
可以用两种方法创建线程安全支持数据的List--Vector或封装ArrayList和Collections.synchronizedList()。
java.util.concurrent包添加了名称繁琐的CopyOnWriteArrayList。
为什么我们想要新的线程安全的List类?
为什么Vector还不够?
最简单的答案是与迭代和并发修改之间的交互有关。
使用Vector或使用同步的List封装器,返回的迭代器是fail-fast的,这意味着如果在迭代过程中任何其他线程修改List,迭代可能失败。
Vector的非常普遍的应用程序是存储通过组件注册的监听器的列表。
当发生适合的事件时,该组件将在监听器的列表中迭代,调用每个监听器。
为了防止ConcurrentModificationException,迭代线程必须复制列表或锁定列表,以便进行整体迭代,而这两种情况都需要大量的性能成本。
CopyOnWriteArrayList类通过每次添加或删除元素时创建支持数组的新副本,避免了这个问题,但是进行中的迭代保持对创建迭代器时的当前副本进行操作。
虽然复制也会有一些成本,但是在许多情况下,迭代要比修改多得多,在这些情况下,写入时复制要比其他备用方法具有更好的性能和并发性。
如果应用程序需要Set语义,而不是List,那么还有一个Set版本--CopyOnWriteArraySet。
ConcurrentHashMap
正如已经存在线程安全的List的实现,您可以用多种方法创建线程安全的、基于hash的Map--Hashtable,并使用Collections.synchronizedMap()封装HashMap。
JDK5.0添加了ConcurrentHashMap实现,该实现提供了相同的基本线程安全的Map功能,但它大大提高了并发性。
Hashtable和synchronizedMap所采取的获得同步的简单方法(同步Hashtable中或者同步的Map封装器对象中的每个方法)有两个主要的不足。
首先,这种方法对于可伸缩性是一种障碍,因为一次只能有一个线程可以访问hash表。
同时,这样仍不足以提供真正的线程安全性,许多公用的混合操作仍然需要额外的同步。
虽然诸如get()和put()之类的简单操作可以在不需要额外同步的情况下安全地完成,但还是有一些公用的操作序列,例如迭代或者put-if-absent(空则放入),需要外部的同步,以避免数据争用。
Hashtable和Collections.synchronizedMap通过同步每个方法获得线程安全。
这意味着当一个线程执行一个Map方法时,无论其他线程要对Map进行什么样操作,都不能执行,直到第一个线程结束才可以。
对比来说,ConcurrentHashMap允许多个读取几乎总是并发执行,读和写操作通常并发执行,多个同时写入经常并发执行。
结果是当多个线程需要访问同一Map时,可以获得更高的并发性。
在大多数情况下,ConcurrentHashMap是Hashtable或Collections.synchronizedMap(newHashMap())的简单替换。
然而,其中有一个显著不同,即ConcurrentHashMap实例中的同步不锁定映射进行独占使用。
实际上,没有办法锁定ConcurrentHashMap进行独占使用,它被设计用于进行并发访问。
为了使集合不被锁定进行独占使用,还提供了公用的混合操作的其他(原子)方法,如put-if-absent。
ConcurrentHashMap返回的迭代器是弱一致的,意味着它们将不抛出ConcurrentModificationException,将进行"合理操作"来反映迭代过程中其他线程对Map的修改。
队列
原始集合框架包含三个接口:
List、Map和Set。
List描述了元素的有序集合,支持完全随即访问--可以在任何位置添加、提取或删除元素。
LinkedList类经常用于存储工作元素(等待执行的任务)的列表或队列。
然而,List提供的灵活性比该公用应用程序所需要的多得多,这个应用程序通常在后面插入元素,从前面删除元素。
但是要支持完整List接口则意味着LinkedList对于这项任务不像原来那样有效。
Queue接口比List简单得多,仅包含put()和take()方法,并允许比LinkedList更有效的实现。
Queue接口还允许实现来确定存储元素的顺序。
ConcurrentLinkedQueue类实现先进先出(first-in-first-out,FIFO)队列,而PriorityQueue类实现优先级队列(也称为堆),它对于构建调度器非常有用,调度器必须按优先级或预期的执行时间执行任务。
interfaceQueueextendsCollection{
booleanoffer(Ex);
Epoll();
Eremove()throwsNoSuchElementException;
Epeek();
Eelement()throwsNoSuchElementException;
}
实现Queue的类是:
•LinkedList已经进行了改进来实现Queue。
•PriorityQueue非线程安全的优先级对列(堆)实现,根据自然顺序或比较器返回元素。
•ConcurrentLinkedQueue快速、线程安全的、无阻塞FIFO队列。
任务管理之线程创建
线程最普遍的一个应用程序是创建一个或多个线程,以执行特定类型的任务。
Timer类创建线程来执行TimerTask对象,Swing创建线程来处理UI事件。
在这两种情况中,在单独线程中执行的任务都假定是短期的,这些线程是为了处理大量短期任务而存在的。
在其中每种情况中,这些线程一般都有非常简单的结构:
while(true){
if(notasks)
waitforatask;
executethetask;
}
通过例示从Thread获得的对象并调用Thread.start()方法来创建线程。
可以用两种方法创建线程:
通过扩展Thread和覆盖run()方法,或者通过实现Runnable接口和使用Thread(Runnable)构造函数:
classWorkerThreadextendsThread{
publicvoidrun(){/*dowork*/}
}
Threadt=newWorkerThread();
t.start();
或者:
Threadt=newThread(newRunnable(){
publicvoidrun(){/*dowork*/}
}
t.start();
重新使用线程
因为多个原因,类似SwingGUI的框架为事件任务创建单一线程,而不是为每项任务创建新的线程。
首先是因为创建线程会有间接成本,所以创建线程来执行简单任务将是一种资源浪费。
通过重新使用事件线程来处理多个事件,启动和拆卸成本(随平台而变)会分摊在多个事件上。
Swing为事件使用单一后台线程的另一个原因是确保事件不会互相干涉,因为直到前一事件结束,下一事件才开始处理。
该方法简化了事件处理程序的编写。
使用多个线程,将要做更多的工作来确保一次仅一个线程地执行线程相关的代码。
如何不对任务进行管理
大多数服务器应用程序(如Web服务器、POP服务器、数据库服务器或文件服务器)代表远程客户机处理请求,这些客户机通常使用socket连接到服务器。
对于每个请求,通常要进行少量处理(获得该文件的代码块,并将其发送回socket),但是可能
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 线程