1 多线程介绍
多线程是现代编程领域不可绕开的话题,合理的运行多线程能有效降低系统的开发与维护成本,同时显著提升系统性能,这一点在复杂系统中表现得尤为明显;一般来说,多线程的好处包括:
- 充分利用多核系统资源
- 简化系统抽象模型;对于一个复杂系统来说,使用单一线程处理所有任务会导致系统及其复杂,而将多个任务进行合理分类并置于各自互不干扰的独立线程中进行处理,无疑是一种更合理的组织方式
- 更方便的异步事件处理;如对于一个需应对多客户端的服务端来说,单线程处理所有客户端请求必然会导致效率底下,业务耦合,而多线程处理多客户端请求则能有效解决该问题
- 提升系统性能;合理的组织,充分的利用系统资源,必然带来性能的提升
万事有利有弊,多线程的好处虽多,但想合理的使用好多线程,却并不容易,多线程编程主要需要注意以下几个问题:
- 如何保证线程安全
- 如何避免线程间同步导致的程序执行中断;主要有死锁,活锁,饥饿锁
- 如何避免不合理的多线程应用导致的性能损耗;因为多线程本身也会耗用系统资源,如线程间调度(执行,暂停)导致的线程运行上下文切换,保存与恢复线程运行上下文,如果涉及到多线程之间的数据共享,那么不可避免的需要用到数据同步以保证线程安全,而数据同步也需要消耗系统资源
2 基础知识
2.1 线程安全
所谓线程安全指的是:一个数据(基本类型数据,对象类型数据)在不需要其他的同步或协调机制的情况下被多个线程同时使用且能够持续保证正确的特性;这里面最重要的就是正确这个词,所谓正确指的就是这个数据能永远如你的逻辑预期
2.2 锁
内在锁(intrinsic lock):所有的java对象皆可作为synchronized
声明的锁对象,如:
synchronized (lock) {
// Access or modify shared state guarded by lock
}
or
public synchronized void doSomething() {
//doSomething
}
这种所有java对象锁所共有的锁特性称为内在锁或监听锁,内在锁同一时间最多只能被一个对象锁持有
可重入锁(ReentrantLock):可重入指的是,每个线程可多次获得同一个锁;可重入的特性很重要,如果一段逻辑在执行的时候在获得一个锁后,其后续逻辑又执行了一次获取该锁的操作,此时若锁不是可重入的,那么就会造成死锁;内在锁也具备可重入性
2.3 共享数据
使用synchronized可以保证同一代码块同一时间只有一个线程进入,而共享数据讨论的是如何保证数据在被多个线程同时访问的情况下还能保持正确性
2.3.1 数据的可见性(Visibility)
可见性指的是同一个对象或数据被多个线程访问的时候,每个线程看到的都是这个对象的最新值,或者说一个线程对数据的变动能被其他线程感知;最简单粗暴的保证可见性的方式就是使用synchronized关键字;在讨论可见性时,有一种情况往往被忽视,即:64位数字(double或long)型变量,对于其他的变量,即便在多线程情况下,虽然读到的不是最新的值,但至少还是一个完整的值(即便不是最新的),但是对于64位数字,JVM对其的读写会分为两个32位操作,可能一个线程对其写操作只完成了一半(第一个32位操作),另外一个线程就执行完了读操作,那么得出的是一个完全错误的值
2.3.2 volatile
锁可以保证原子性与可见性,而volatile只能保证可见性,一般在同时满足下列三个条件的情况下方可使用volatile
- 对变量的写不依赖变量当前的值;典型应用场景如自增操作
a++
- 该变量没有包含在具有其他变量的不变式中;这一条有点绕,通过代码示例讲解更易理解:
@NotThreadSafe
public class NumberRange {
private volatile int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
如果初始状态是(0, 5),同一时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3),显然这两个操作交叉存入的值是不符合条件的,但是两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3);所以这种情况下还是需要通过synchronized
保证程序正确
- 考虑应用场景,确实不需要对变量进行加锁
2.3.3 数据的Publication与Escape
Publication很容易理解,即将一个数据或对象以某种形式暴露到其当前域之外,其暴露形式包括:暴露引用,通过方法返回,作为方法参数等;而如果一个对象在不应该被Publication的时候被Publication了,则称为Escape
2.3.4 线程约束(thread confinement)
访问共享的可变的数据一般使用同步机制保证数据准确;如果数据只能被一个线程访问,那么就不需要同步机制,这种场景就叫线程约束;实现线程约束主要有两种形式,分别为stack Confinement和ThreadLocal,stack Confinement指的是通过代码块的调用栈将变量限制于单个线程内,如:
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
// animals confined to method, don't let them escape!
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
if (candidate == null || !candidate.isPotentialMate(a))
candidate = a;
else {
ark.load(new AnimalPair(candidate, a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
这段代码里的numPairs ,作为一个基本类型,也不会有引用通过方法返回暴露出去,它只会存在于当前堆栈中,那么它就是thread Confinement;而ThreadLocal
属于更正规的thread confinement
形式,每个线程都提供了一个单独的数据存储对象,而ThreadLocal
机制在于为数据生成一份单独的基于该线程的copy并存于该存储对象内,这样该数据在该线程下的任何变动都不会影响到其他线程
2.3.5 对象的不可变性
如果对象同时具备下列三个条件,则其具有不变性
- 对象被创建后,它的状态不可变 -- 这里的状态指代的是一些业务关键值
- 它的所有属性都是final的
- 它的应用在构建的时候未Escape掉
一个具有不可变性的对象一定是线程安全的,不可变性的一个变种称为形式上的不可变对象,即:如果一个对象不是完全的不可变,只是在publication后不可变,那么这个对象称为形式上的不可变,如:某个数据在生成后即不会有任何逻辑会对其进行更改
如何publication一个对象取决于对象的可变性如何:
- 对于不可变的对象,可以放心publication
- 对于效果上的不可变对象,需要保证publication安全,即不会发生Escape
- 而对于可变对象,不但要保证publication安全,还要保证在使用的时候线程安全
2.4 多线程下对象的组装
设计一个线程安全的类主要需要考虑下列三点:
- 确定哪些变量(属性)与类的状态相关
- 确定这些变量有哪些可能的取值
- 建立一套多线程机制访问这些变量
对象的线程安全经常需与对象所代表的业务一起考虑,如对象的状态变更是否遵循特定规则(如:必须在当前值的基础上+1)
一般情况下,可通过下列四种机制保证对象线程安全:
- 单线程约束(Thread‐confined):所谓单线程约束意思是一个对象只属于一个线程,不能被其它线程访问,既然不存在多线程的访问,那么该对象也就不存在线程安全的问题
- 只读式共享(Shared read‐only):如果一个对象可以被多个线程访问,但是却不能被多个线程修改,则代表该对象属于只读式共享,同时该对象也是线程安全的
- 线程安全/内部同步 (Shared thread‐safe):即一个对象在其内部已经提供了同步机制已保证对该对象的访问个操作是线程安全的
- Guarded: 所谓Guarded对象意思是对该对象的访问必须获得特定的锁,如被另外一个线程安全的对象封装的对象可称为Guarded对象;更进一步,被另外一个Guarded对象封装的对象也是Guarded对象(有点绕),既然该对象被线程安全的对象Guarded住,那么无疑该对象也是线程安全的
最简单的保证对象线程安全的方式就是instance Confinement,即通过synchronized将对对象状态的访问封装起来,另外常见的线程安全封装方式为管程以及代理模式(通过一个线程安全的类访问另外一个线程不安全的类,以使其线程安全)
2.5 构建模块
Collections.synchronizedXxx
包含了一系列线程安全的方法,它对外暴露的每一个public
方法都有synchronized
加持;但是很多时候,在使用容器的时候仅仅对单个方法进行synchronized
不能满足实际需求,如:
//list.get与list.remove分别线程安全
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
这段代码中一个线程执行getLast
并认为当前的lastIndex
为10,而待其执行至list.get时,另一个线程执行remove
将最后一个元素删除掉,即便get
与remove
都是线程安全的,整体功能上还是会出错,虽然可以在业务层增加互斥控制解决该问题,但是去带来了另外一个性能的问题,Concurrent Collections可以有效解决该问题,还是那句话,有利必有弊,Concurrent Collections也有一些缺点,如ConcurrentHashMap
就不能保证在多线程情况下size
,isEmpty
等方法的返回值绝对准确,但是在多线程情况下,主要的操作都是get, put, containsKey 和 remove等,所以一般情况下都可放心大胆使用ConcurrentHashMap
线程的阻塞与打断:任何抛出InterruptedException
的方法都是阻塞方法,面对InterruptedException
,有两种处理方式:
- 继续丢给外层堆栈进行处理
- 捕捉并自行处理
有一点需要注意,如果当前代码处于一个单独的Runnable
中,那么你应该自行处理该InterruptedException
,因为此种情况直接抛出异常会导致异常悄无声息的消失掉,没有任何机制会去处理
2.6 同步器
同步器是一种基于各线程相关状态以协调其执行顺序的对象;在Java库中,有很多同步器类:
- latch
latch
可以阻塞多个线程直到达到一个terminal state,它就像一扇门,在没有达到terminal state时,门是关着的,直到状态触发,门将会打开,且永远打开,不能再关上,即terminal state不会再改变(所以叫terminal state) - FutureTask
FutureTask
可以启动一个任务,等到执行完后返回结果,如:
public class Preloader {
private final FutureTask<ProductInfo> future =
new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
public ProductInfo call() throws DataLoadException {
return loadProductInfo();
}
});
private final Thread thread = new Thread(future);
public void start() { thread.start(); }
public ProductInfo get() throws DataLoadException, InterruptedException {
try {
return future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof DataLoadException)
throw (DataLoadException) cause;
else
throw launderThrowable(cause);
}
}
}
- Semaphores
通过Semaphores
可控制同一时间内可以有几个线程同时访问特定资源 - Barriers
Barriers
与latch
类似,也是阻塞一系列线程直到某一事件发生,它们的主要不同点在于latch
等待的是一个事件,而Barriers等待所有线程都到达一个Barriers point(barrier.await()
),方会触发执行后续代码