本文整理自《Java并发编程实战》一书。
线程允许在同一个进程中同时存在多个程序控制流,线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器program counter、栈以及局部变量等。线程还提供了一种直观的分解模式来充分利用多处理器系统中的硬件并行性,而在同一个程序中的多个线程也可以被同时调度到多个cpu上运行。
线程也被称为轻量级进程,在大多数现代操作系统中,都是以线程为基本的调度单位,而不是进程。如果没有明确的协调机制,那么线程将彼此独立执行。由于同一个进程中的所有线程都将共享进程的内存地址空间,因此这些线程都能访问相同的变量并在同一个堆上分配对象,这就需要实现一种比在进程间共享数据颗粒度更细的数据共享机制。如果没有明确的同步机制来执行对共享数据的访问,那么当一个线程正在使用某个变量时,另外一个线程可能同时访问这个变量,这将造成不可预测的结果。
线程无处不在:每个Java应用程序都会使用线程。当JVM启动时,它将为JVM的内部任务(例如,垃圾收集、终结操作等)创建后台线程,并创建一个主线程来允许main方法。
要编写线程安全的代码,其核心在于对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。Java中的主要同步机制是关键字synchronized,它提供了一种独特的加锁方式,但“同步”这个术语还包括volatile类型的变量,显式锁(Explicit Lock)以及原子变量。
无状态对象一定是线程安全的。不可变对象一定是线程安全的。
(当满足以下条件时,对象才是不可变的:
1.对象创建以后其状态就不能修改;2.对象的所有域都是final类型;3.对象是正确创建的,在对象创建过程期间,this引用没有逸出。)
要保持状态的一致性,就需要在单个原子操作内更新所有相关的状态变量。
由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。
并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护,以实现其原子性操作。
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序(代码编写时指定的顺序)进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读写操作的线程都必须在同一个锁上同步。
加锁机制既可以确保可见性又可以确保原子性{ 把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和可见性(visibility)},而volatile变量只能确保可见性。Atomic类可见性和原子性皆有。
Java语言以及其核心库提供了一些机制来帮助维护线程封闭性(ThreadConfinement),例如局部变量和ThreadLocal类,但即便如此,程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。
正如“除非需要更高的可见性,否则应将所有的域都声明为私有化”是一个良好的编程习惯,“除非需要某个域都是可变的,否则应将其声明为final域”也是一个良好的编程习惯。