某些编程语言被设计为可以将并发任务彼此隔离,这些语言通常称为函数型语言,其中每个函数调用都不会产生任何副作用(并因此而不能干涉其他函数),并因此可以当做独立的任务来驱动。Erlang就是这样的语言。
Java采取了更加传统的方式,在顺序性语言的基础上提供对线程的支持。与在多任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建任务。这种方式产生的一个好处是操作系统的透明性,这对Java而言,是一个重要的设计指标。
Java的线程机制是抢占式的,这表示调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动他的任务。
当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处——它不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。
将Runnable对象转变为工作任务的传统方式时把它提交给一个Thread构造器。Thread构造器只需要一个Runnable对象。调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()
方法,以便在这个新线程中启动该任务。

多线程交互模式:
- 不进行交互的模式:
- 线程间不需要处理共享的数据,也不需要进行动作协调,那么将会非常简单,就是多个独立的线程各自完成自己线程中的工作。
- 基于共享容器协同的多线程模式:
- 多个线程间对共享的数据进行处理。例如经典的生产者消费者例子,我们有一个队列用于生产和消费,那么这个队列就是多个线程会共享的一个容器或者数据对象,多个线程会并发地访问这个队列。
在多线程环境下对同一份数据的访问,我们需要有所保护和控制以保证访问的正确性。对于存储数据的容器或者对象,有线程安全和线程不安全之分,而对于线程不安全的容器或对象,一般可以通过
加锁或者通过Copy On Write的方式来控制并发访问。 - 使用加锁方式时,如果数据在多线程中的读写比例很高,则一般会采用读写锁而非简单的互斥锁。
- 对于线程安全的容器和对象,我们就可以在多线程环境下直接使用它们了。在这些线程安全的容器和对象中,有些是支持并发的,这种方式的效率会比简单的加互斥锁的实现更好,在JDK中的
java.util.concurrent包中有很多这样的容器类。(注:比如HashMap改CurrentHashMap时,要注意之前的锁粒度,操作的原子性)
- 多个线程间对共享的数据进行处理。例如经典的生产者消费者例子,我们有一个队列用于生产和消费,那么这个队列就是多个线程会共享的一个容器或者数据对象,多个线程会并发地访问这个队列。
- 通过事件协同的多线程模式:
- 除了并发访问的控制,线程间会存在着协调的需求,例如A、B两个线程,B线程需要等到某个状态或事件发生后才能继续自己的工作,而这个状态改变或者事件产生需要和A线程相关。那么在这个场景
下,就需要线程间的协调。 - 避免死锁,一般来说,能够原子性的获取需要的多个锁,或者注意调整获取多个锁的获取顺序,就会比较好地避免死锁。
- 此外,线程之间还会传递数据。因为这些线程共用进程的内存空间,所以线程间的传递数据就相对容易一些了。
- 除了并发访问的控制,线程间会存在着协调的需求,例如A、B两个线程,B线程需要等到某个状态或事件发生后才能继续自己的工作,而这个状态改变或者事件产生需要和A线程相关。那么在这个场景
- 多进程模式:
- 多进程相对于单进程下的多线程方式来说,资源控制会更容易实现,此外,多进程中的单个进程问题,不会造成整体的不可用。这两天是多进程区别于单进程多线程方式的两个特点。
当然,使用多进程会比多线程稍微复杂一些。多进程间可以共享数据,但是其代价比多线程要大,会涉及序列化与反序列化的开销。 - 我们的分布式系统是多机组成的系统,可以近似看作是把单机多进程变成了多机的多进程。
- 多进程相对于单进程下的多线程方式来说,资源控制会更容易实现,此外,多进程中的单个进程问题,不会造成整体的不可用。这两天是多进程区别于单进程多线程方式的两个特点。
可用性分析:
单线程和单进程多线程的程序在遇到机器故障、OS问题或者自身的进程问题时,会导致整个功能不可用。
对于多进程的系统,如果遇到机器故障或者OS问题,那么功能也会整体不可用,而如果是多进程中的某个进程问题,那么是有可能保持系统的部分功能正常的——当然这取决于多进程系统自身的实现方式。
而在多机系统中,如果遇到某些机器故障、OS问题或者某些机器的进程问题,我们都有机会来保证整体的功能大体可用——可能是部分功能失效,也可能是不再能承担正常情况下那么大的系统压力了。
综上,不同的方式可能造成的故障影响面也是不同的。
节选《大型网站系统与Java中间件实践》