[软件构造笔记] 第七章 并发和分布式编程

并发(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
加锁原则
  1. 非必要,不要加锁,影响性能;多线程共享的mutable对象都要加锁
  2. 尽量小范围加锁
  3. 要考虑lock的对象,避免lock到Class上
  4. 在ADT中记录threadsafe的设计决策,要优先使用前三种方法解决问题
    ADT
  5. 加锁不代表安全其他线程可以不获取同一个锁而修改数据!所以要充分考虑哪些地方互斥,要对所有互斥的地方加同一个锁
  6. 客户端可能也要用数据对象的锁,将多个操作变为原子操作。
happens-before 关系

happens-before

死锁(Deadlock)

由于锁可以嵌套,可能出现线程间锁的依赖循环。
解决方案:

  1. 对需要获取的锁进行排序,保证所有代码按同一顺序获取该锁
    缺点:需要预知所有的锁
  2. 粗粒锁。用唯一的全局变量或Class锁。
    缺点:会严重降低性能。

wait,notify,notifyAll

有时,某线程必须等待其他线程改变条件才能继续执行,不得不用while保护块实现,这将浪费CPU资源。
三方法都得在synchronized代码块中执行,并且都用锁.wait()/notify()/nodifyAll()方法调用
wait: 释放
当前锁
(不是所有锁),让当前线程进入该对象的等待队列
notify:唤醒该锁的等待队列中的一个线程,并继续执行原线程
notifyAll:唤醒该锁的等待队列中的所有线程,并继续执行原线程

总结

并发程序设计:

  1. 要在设计的时候保证安全,避免竞争、冲突,并证明线程安全
  2. 避免死锁,并证明
  3. 公平性,线程通过操作系统调度,但可以设置优先级

并发实现:

  1. 库数据结构要么不使用同步(单线程),要么使用监视模式。
  2. 可变数据类型必须使用细粒度锁或线程私有
  3. 搜索通常使用不可变类型
  4. 用细粒度锁获得高性能,用锁排序处理死锁问题
  5. 数据库使用事物避免争用锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值