本篇介绍线程的基础知识,包括:线程是什么、线程为什么有用处、怎样开始编写使用线程的程序、如何控制线程、如何在线程之间交换数据以及线程如何互相通信。
一、线程是什么
进程是在某种程度上相互隔离的、独立运行的程序。线程也称作轻量级进程,就像进程一样,线程在程序中是独立的、并发的执行,每个线程有自己的堆栈、程序计数器和局部变量。但是与进程想比,进程中的线程之间的隔离程度要小,线程共享内存、文件句柄和其他每个进程应有的状态。进程可以支持多个线程,这多个线程看似是同时执行,但相互之间并不同步。一个进程中的多个线程共享相同的内存地址空间,这就意味着一个进程中的多个线程可以访问相同的变量和对象,而且他们从同一堆中分配对象。尽管这让线程之间共享数据变得容易,但要注意确保它们不会干扰同一进程里的其他线程。
每个Java程序都至少有一个线程--主线程。当一个java程序启动时,jvm会创建主线程,并在改线程中调用程序的main方法。JVM还创建了其他线程,如垃圾收集、对象终止和其他JVM内务处理任务相关的线程。其他工具也创建线程,如servlet容器、RMI、应用程序服务器等。
二、线程为什么有用处
使用线程的好处如下:可以更高效利用多处理器系统、执行异步或后台处理、简化建模等。
(1) 利用多处理器系统:服务器系统甚至台式机系统都可能有多个处理器,可以利用多个处理器,并调度线程在任何可用的处理器上执行。调度的基本单位通常是线程。如果某个程序只有一个活动的线程,则一次只能在一个处理器上运行。如果某个程序有多个活动线程,那么可以同时调度多个线程。在设计良好的程序中,使用多个线程可以有效提高程序吞吐量和性能。
(2)异步或后台处理:举个例子,服务器应用程序从远程来源(如Socket套接字)获取输入。当读取套接字时,如果当前没有可用数据,那么对SocketInputStream.read()的调用将会阻塞,直到有可用数据为止。
第一种方法:单线程程序要读取套接字,而套接字另一端的实体并未发送任何数据,那么程序就永远等待而不执行其他操作。
第二种方法:轮询套接字检查是否有可用数据。但通常不使用这种做法,因为影响性能。
第三种方法:创建一个线程来读取套接字,那么当这个线程等待套接字中的输入时,主线程就可以执行其他任务。甚至可以创建多个线程来同时读取多个套接字。当有可用数据时,通过唤醒正在等待的线程,线程就得到了通知,而不必轮询检查是否有可用数据。
(3)简化建模
在某些场景下使用线程可以使程序编写和维护起来更简单。举2个例子。一个仿真应用程序,要模拟多个实体之间的交互作用。给每个实体一个自己的线程可以使建模简化。
第二个例子是使用单独线程来简化程序的实例。在一个应用程序有多个独立的事件驱动的组件时,例如一个应用程序可能有这样一个组件,该组件在某个事件之后用秒数倒计时,并更新屏幕显示。与其让一个主循环定期检查时间并更新显示,不如让一个线程什么也不做,一直休眠直到某事件发生后,更新屏幕上的计数器。这样程序就根本无需担心计数器。
示例:使用一个线程用于计时,并使用另一个线程完成工作。
三、线程API
Object类定义了wait(),notify(),notifyAll()方法,必须拥有相关对象的锁。这些方法是更复杂的锁定、排队和并发性代码的构建。但与其使用wait()和notify()来编写调度程序、线程池、队列和锁,不如使用上面介绍的java.util.concurrent包中的方法。
wait():会让调用线程休眠,直到用Thread.interrupt()中断它、过了指定时间、或者另一个线程用notify()或notifyAll()唤醒它。
notify():当对某个对象调用notify()时,如果有任何线程正在通过wait()等待该对象,那么就会唤醒其中一个线程。当对某个对象调用notifyAll()时,会唤醒所有正在等待该对象的线程。