并发(Concurrency)
概念
并发:同一时间执行多个计算。即多个任务的执行时间有交叉。
并发编程的两种模型:共享内存、消息传递。
并发模块的两种类型:进程、线程。
- 进程:运行的程序,独立空间,进程间内存不共享。(相当于虚拟机)
- 线程:属于进程,同一进程的多个线程共享内存,堆栈独立。线程要注意用锁实现同步。(相当于虚拟CPU)
时间分片:每个核同一时间只能执行一个线程,通过将核的时间分片,实现多线程的并发处理。
交叉与竞争:
- 交叉: 多并发线程可能对共享内存交叉访问(Interleaving),使数据结果错误。需注意,单条指令!=原子操作(修改多要经过取值、运算、赋值等过程,可能有交叉)。
x = x+1
// step 1: load x
// step 2: caculate ans = x+1
// step 3: save x = ans
- 竞争:多线程时间发生的相对顺序无法确定,存在竞争。
即使采用消息传递(将多请求传入一个队列)也不能解决竞争问题。(如T1、T2检查文件存在,T1删除文件,T2对文件写入,抛出异常)
线程编写
线程创建
通常,构造Thread只需创造匿名内部类,重写Runnable接口的run方法即可。
Thread的run方法不会创建新进程,而start方法会创建新进程并执行run方法。
public class TaskA {
public static void main(String[] args) {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("A work");
}
});
A.start();
System.out.println("main work");
}
}
多线程管理
Thread.sleep(millis): 线程休眠millis毫秒(不会释放锁);若收到中断信号,抛出InterruptedException。
t.interrupt(): 向线程t发出中断信号。收到中断信号后,线程不会立即中断,需要手动检查或通过sleep、join等函数等待中断并抛出异常。
t.isInterrupted():t是否收到了中断信号
t.interrupted(): t是否收到了中断信号,并清空中断标志位
t.join():让线程t保持执行,直到结束或收到中断信号,会抛出InterruptedException。
线程安全
四方法:
- 限制数据共享
- 共享不可变数据
- 共享线程安全的可变数据
- 同步机制:通过锁的机制共享线程不安全的可变数据,变并行为串行
限制数据共享(Confinement)
核心思想:线程间不共享mutable数据类型
小心其他线程通过引用访问其他线程的数据,要避免全局变量
共享不可变数据(Immutability)
使用不可变数据类型、不可变引用,避免线程间的race condition。
强化不可变性:
- 无mutator,所有fiels要private final,无表示泄漏,无有益变化(如平衡数据结构)
共享线程安全的数据(threadsafe data types)
多线程安全:StringBuffer,synchronizedSet(但用迭代器不安全)
通过Decorator模式,使每个操作为原子操作。单个原子操作线程安全。
缺点:多个操作,仍不安全(线程间多个操作可能交叉,如查询存在后被其他线程删除,再删除就出错)
不要暴露线程安全数据的底层数据结构。
锁和同步(Synchronization and Locks)
锁的基本操作
acquire: 获得,如果一个线程尝试获取另一个线程正拥有的锁,他将阻塞,直到锁被另一个线程释放
release:释放,放弃锁的拥有权
锁定
每个对象都有关联的lock,锁定后该部分变成原子操作,不会被线程干扰
要互斥,必须使用统一个lock
sychronized(A){
if(!A.empty()){
A.pop();
}
}
三种锁
Monitor pattern:在所有类方法中用**synchronized(this)**上锁,包括observer,如toString(),length()
sychronized:
作用对象 | 锁对象 |
---|---|
成员方法 | this |
静态方法 | Class |
代码块 | obj |
加锁原则
- 非必要,不要加锁,影响性能;多线程共享的mutable对象都要加锁
- 尽量小范围加锁
- 要考虑lock的对象,避免lock到Class上
- 要在ADT中记录threadsafe的设计决策,要优先使用前三种方法解决问题
- 加锁不代表安全,其他线程可以不获取同一个锁而修改数据!所以要充分考虑哪些地方互斥,要对所有互斥的地方加同一个锁。
- 客户端可能也要用数据对象的锁,将多个操作变为原子操作。
happens-before 关系
死锁(Deadlock)
由于锁可以嵌套,可能出现线程间锁的依赖循环。
解决方案:
- 对需要获取的锁进行排序,保证所有代码按同一顺序获取该锁
缺点:需要预知所有的锁 - 粗粒锁。用唯一的全局变量或Class锁。
缺点:会严重降低性能。
wait,notify,notifyAll
有时,某线程必须等待其他线程改变条件才能继续执行,不得不用while保护块实现,这将浪费CPU资源。
三方法都得在synchronized代码块中执行,并且都用锁.wait()/notify()/nodifyAll()方法调用
wait: 释放当前锁(不是所有锁),让当前线程进入该对象的等待队列
notify:唤醒该锁的等待队列中的一个线程,并继续执行原线程
notifyAll:唤醒该锁的等待队列中的所有线程,并继续执行原线程
总结
并发程序设计:
- 要在设计的时候保证安全,避免竞争、冲突,并证明线程安全
- 避免死锁,并证明
- 公平性,线程通过操作系统调度,但可以设置优先级
并发实现:
- 库数据结构要么不使用同步(单线程),要么使用监视模式。
- 可变数据类型必须使用细粒度锁或线程私有
- 搜索通常使用不可变类型
- 用细粒度锁获得高性能,用锁排序处理死锁问题
- 数据库使用事物避免争用锁