线程作用
当服务器需要大量并发连接的时候,一般使用轻量级的线程来处理大量的连接,而不是重量级的进程。线程在资源使用上更宽松,因为它们会共享内存。使用线程来代替进程,可以再让你的服务器性能提升三倍。再结合使用可重用的线程池,在同样的硬件和网络连接下,服务器的运行可以快9倍多!采用多线程设计设计会更容易,可以将程序分解为多个线程,分别执行独立的操作。
由于现代虚拟机和操作系统中线程可以提供很高的性能,而且构建基于线程的服务器相对简单,所以开始时总会考虑采用基于线程的设计。如果确实遇到麻烦,应该考虑将应用分解到多个冗余的服务器上,而不要全倚仗一个服务器上的3倍性能提升。
运行线程
这里有两种方法运行线程:
- 对Thread类派生子类,覆盖其
run()
方法。 - 实现Runnable接口,写
run
方法,将Runnable对象传递给Thread构造函数。
不论采用哪种方法,关键都在于run()
方法:
public void run()
你应当把线程要做的工作都放在这个方法中,线程要在这里启动,并在这里结束。
- 多线程程序会在
main()
方法以及所有非守护线程(nondaemon thread)都返回时才退出(非守护线程是指后台线程,完成后台任务)。- Runnable接口不一定绝对优于继承Thread类。有些情况下在Thread类的子类构造函数调用Thread类的一些方法可能很有用。而在继承其他类的时候,就需要使用RUnnable接口。
- 有些崇尚面向对象的人认为,线程完成的任务实际上不是一种Thread,因此应当放在一个单独的类或接口(如Runnable),而不应该放在Thread的子类。作者部分同意这种观点,但不认为这个观点像其声称的那样理由充分。
- 尽量不要在构造函数在启动线程,否则可能会发生竞态条件。可能会在构造函数结束而且对象完全初始化之前允许新线程进行回调。
回调
一种简单有效的方法是让线程告诉主程序它何时结束,这是通过调用主类(即启动这个线程的类)中的一个方法来做到的,这被称为回调。这样一来,主程序就可以在等待线程结束期间休息,而不会占用运行线程的时间。
这里有两种普通做法:
- 调用主类的静态方法。
- 使用主类的实例方法。
使用实例方法要复杂一些,但有很多优点。它可以做到每个线程跟踪一个主类对象,不需要额外的数据结构。
同步
因为不同线程共享相同的内存,一个线程完全有可能会破坏另一个线程使用的变量的数据结构,这时候就需要同步技术来解决问题了。
同步块
把一个对象锁包围在synchronized块中,它会对这个对象进行同步。同步要求在同一个对象上同步的所有代码要连续地运行,而不能并行运行。
示例:
synchronized(System.out){
//同步连续过程
}
同步方法
由于用对象本身来同步整个方法体是很常见的,所以Java为此提供了一个快捷方式。对当前对象(this引用)同步整个方法。
public synchronized void fun() {
//同步连续过程
}
但是同步方法并不是万能的:
- 同步方法会使得VM性能严重下降(不过最新的VM已经大为改进)。
- 大大增加了死锁的可能。
- 不总是对象本身需要防止同时修改或访问,需要注意哪个才是关键要上锁的对象。
同步的替代方式
对于线程调度引起的行为不一致,但其实同步并不总是这个问题最好解决方案。
- 使用局部变量而不是字段。局部变量不存在同步问题。
- 基本类型的方法也可以在单独的线程中安全地修改,因为Java按值而不是按引用来传递基本类型对象。
- 不可变对象永远也不会改变状态。
- 构造函数一般不需要担心线程安全问题。在构造函数返回之前,没有线程有这个对象的引用,所以不可能有两个线程都有这个对象的引用。(但不等于构造函数启动线程,不会发生竞态条件。)
- 在类中利用不变性,将其所有字段声明为private和final,而且不要编写任何可能改变它们的方法。
- 将非线程安全的类用作线程安全的类的一个私有字段。只要包含类只以线程安全的方式访问这个非安全类,而且永远不让这个私有字段的引用泄露到另一个对象中,那么这个类就是安全的。
- 对于基本数据类型,使用java.util.concurrent.atomic包中特意设计为保证线程安全但可变的类。不过需要说明的是,这不会让对象本身也是线程安全的,只是该引用变量的获取和设置是线程安全的。
- 对于映射和列表等集合,使用java.util.Collections的方法把它们包装在一个线程安全的版本中。
不过这些只是单个的原子方法调用,如果需要作为一个原子连续地完成两个操作以上,中间不能有中断,就需要同步。
死锁
也有可能发生这样的情况,两个线程太过小心,每个线程都在等待对方的资源的独占访问权,却永远得不到,这会导致死锁。
- 同步应当式确保线程安全的最后一道防线。如果确实需要同步,要保持同步块尽可能小,而且尽量不要一次同步多个对象。
- 如果多个对象需要操作相同的共享资源集,要确保以相同的顺序请求这些资源。
线程调度
优先级
在Java中,10是最高优先级,0是最低优先级。
UNIX相反,0是最高级,10是最低级。而Windows只有7个优先级,1、2、3以及8、9会分别分配到两个相同的优先级。
线程的优先级可以使用setPriority()
来改变。
抢占
每个虚拟机都有一个线程调度器,确定在给定时刻运行哪个线程。主要有两种线程调度:抢占式(preemptive)和协作式(cooperative)。
下面是不同的方式可以让线程暂停或准备暂停:
阻塞
任何时候线程必须停下来等待它没有的资源时,就会发生阻塞。即使阻塞几毫秒,这一点时间也足够其他线程用来完成重要的任务。
放弃
显式地放弃可以通过调用Thread.yield()
静态方法来做到。
放弃并不会释放这个线程拥有的锁。因此,在理想情况下,在线程放弃时不应该做任何同步。
休眠
休眠是更有力的放弃方式。不管有没有其他线程准备运行,休眠线程都会暂停。这样一来,不只是其他有相同优先级的线程得到机会,还会给较低优先级的线程一个运行的机会。
通过调用两个重载的Thread.sleep()
静态方法之一,线程可以进入休眠。
想唤醒休眠的线程,可以通过休眠线程的interrupt()
方法来实现,会让休眠中的线程得到一个InterruptedException异常。
连接线程
Java提供三个join()
方法,允许一个线程在继续执行之前等待另一个线程结束。同样,连接的另一个的线程的线程可以被中断,可以调用通过该线程的interrupt()
方法来实现,会得到一个InterruptedException异常。
如今,连接线程可能没有Java5之前那么重要。具体来讲,很多原来需要
join()
的设计现在用Executor和Future更容易地实现。
等待一个对象
线程可以等待一个它锁定的对象。希望暂停的线程首先必须使用syschronized获得这个锁的对象,然后调用这个对象的三个重载wait()
方法之一。
线程会保持休眠,直到发生以下三种情况之一
- 超时。不过,如果线程不能立即重新获得所等待的对象的锁,它可能仍要阻塞一段时间。
- 中断。调用
interrupt()
方法。不过,在抛出异常前线程要重新获得所等待对象的锁,所以调用后,该线程可能仍要阻塞一段时间。 - 通知。在这个线程所等待的对象山调用
notify()
或notifyAll()
,就会发生通知。notify()
基本随机地从等待这个对象的线程列表中选择一个线程,并将它唤醒。notifyAll()
方法会唤醒等待指定对象的每一个线程。如果成功,继续执行。如果失败,会对这个对象阻塞,直到可以得到锁,然后继续执行。
不要假定因为线程得到了通知,对象现在就处于正确的状态。要保证对象进入正确的状态之前,再也不会进入不正确的状态,如果无法保证这一点,就要显式地进行检查,一般要将
wait ()
调用放在检查当前状态对象的循环中。
结束
当run()
方法返回时,线程将撤销,其他线程可以接管CPU。
线程池
如果并发线程达到4000至20000时,大多数Java虚拟机可能会由于内存耗尽而无法承受。不过,通过使用线程池而不是为每个连接生成新进程,服务器每分钟就可以用不到100个线程来处理数千个短连接。
使用Excutor类,只需要将各个任务作为一个Runnable对象提交给这个线程池,你就会得到一个Future对象,可以用来检查任务的进度。
- 调用
Executors.ewFixedThreadPool()
可以创建一个固定数目的线程池。 - 调用
submit()
用来提交线程任务。 - 调用
shutdown()
,不会终止等待的工作,只是通知线程池没有更多的任务需要增加到它的内部队列。而且一旦完成所有等待的工作,就应当关闭。 - 调用
shutdownNow()
,终止当前处理中的任务,并忽略所有等待的任务。
Excutor与回调
Java5引用了一个多线程编程的新方法,可以更容易处理回调。
不再是直接创建一个线程,你要创建一个ExcutorService,它会根据你需要的固定线程数为你创建线程(其实是创建一个线程池了),可以向ExecutorService提交Callable任务,每个Callable任务分别得到一个Future。之后可以向Future请求得到任务的结果。如果任务已经准备就绪,就会立即得到这个结果。如果还没有准备好,轮训线程会阻塞,直到结果准备就绪,再返回这个结果。
示例:
ExecutorService service = Executors.newFixedThreadPool(2)。
Future<Integer> future1 = service.submit(task1);
Future<Integer> future2 = service.submit(task2);
return Math.max(future1.get(), future2.get());