HIT软件构造课程复习笔记(八)

7.1 并发

并发编程

并发:多个计算同时发生。它可以指网络上的多台计算机,也可以指一台计算机上的多个应用,也可以指一个CPU上的多核处理器。使用并发的原因在于摩尔定律失效,“核”变得越来越多,为了充分利用多核和多处理器, 需要将程序转化为并行执行

共享内存:在内存中读写共享数据。例如两个处理器共享内存、同一台机器上的两个程序共享文件系统、同一个Java程序内的两个线程共享Java对象

消息传递:通过channel交换消息。例如网络上的两台计算机通过网络连接通讯、浏览器和Web服务器,A请求页面,B发送页面数据给A、即时通讯软件的客户端和服务器、同一台计算机上的两个程序通过管道连接进行通讯

进程、线程

进程:拥有整台计算机的资源,多进程之间不共享内存,进程之间通过消 息传递进行协作,一般来说,进程==程序 ==应用,但一个应用中可能包含多个进程。OS支持的IPC机制(pipe/socket)支持进程间通信, JVM通常运行单一进程,但也可以创建新的进程。

进程=虚拟机;线程=虚拟CPU,程序共享、资源共享,都隶属于进程

线程:共享内存,很难获得线程私有的内存空间,通过创建消息队列在线程之间进行消息传递

线程的创建:每个应用至少有一个线程(主线程),可以创建其他的线程。创建的一种方式是从Thread类派生子类,另一种是从Runnable接口构造Thread对象
派生子类:继承Thread类并重写run方法,之后调用线程对象的start方法
实现接口:实现Runnable接口重写run方法,(new Thread(new HelloRunnable())).start();即可启动线程,也可以用匿名类实现:
new Thread(new Runnable() {
public void run() {
System.out.println(“Hello”);
}
}).start();

交错和竞争条件

时间分片:虽然有多线程, 但只有一个核,每个时刻只能执行一个线程,通过时间分片,在多个进程/线程之间共享处理器。(即使是多核CPU,进程/线程的数目也往往大于核的数目)注意:时间分片是由OS自动调度的

竞争条件:程序的正确性(后置条件和不变量的满足)取决于并发计算A和B事件的相对时间。当这种情况发生时,我们说“A在与B存在竞争”。光调整代码是不够的,因为单行、单条语句都未必是原子的,是否是原子的,由JVM确定。

线程休眠:Thread.sleep(time);将某个线程休眠,意味着其他线程得到更多的执行机会,进入休眠的线程不会失去对现有monitor或锁的所有权。

线程中断:向线程发出中断信号调用interrupt()方法,检查线程是否被中断调用isInterrupted(),当某个线程被中断后,一般来说应停止其run()中的执行,取决于程序员在run()中处理。一般来说,线程在收到中断信号时应该中断,直接终止,但也有例外。通过sleep()检测中断信号,抛出异常,停止 线程。

join方法可以让当前线程保持执行,直到其执行结束。(一般不需要这种显式指定线程执行次序)执行该操作时,也会检测其他线程发来的中断信号。

7.2 线程安全

什么是线程安全

线程安全:ADT或方法在多线程中要执行正确。不违反spec、保持RI,与多少处理器、 OS如何调度线程,均无关,不需要在spec中强制要求client满足某种“线程安全”的义务

措施:限制数据共享,共享不可变数据,共享线程安全的可变数据,同步机制:通过锁的机制共享线程不安全的可变数据,变并行为串行

策略

限制

Confinement:

核心思想:线程之间不共享mutable数据类型。将可变数据限制在单一线程内部,避免竞争,不允许任何线程直接读写该数据

全局静态变量不会被线程自动限制,如果程序里有,需要限制只有一个线程可以使用它,最后去掉它

如果一个ADT的rep中包含mutable的属性且多线程之间对其进行mutator操作,那么就很难使用confinement策略来确保该ADT是线程安全的

不变性

使用不可变数据类型和不可变引用,避免多线程之间的race condition。(使用final限定)不可变数据通常是线程安全的,如果ADT中使用了beneficent mutation,必须要通过“加锁”机制来保证线程安全。

线程安全的不可变类:无变值器方法,所有属性用private和final修饰,无表示暴露,无beneficent mutation

使用线程安全类

如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。 在JDK中的类,文档中明确指明了是否threadsafe,一般来说,JDK同时提供两个相同功能的类,一个是threadsafe,另一个不是。(原因在于threadsafe的类一般性能上受影响)

集合类都是线程不安全的, Java API提供了进一步的decorator,对它们的每一个操作调用,都以原子方式执行,不会与其他操作interleaving,写法如下:
Collections.synchronizedXXX(new XXX);
在使用该封装对象之后,不要再把参数共享给其他线程,不要保留别名,一定要彻底销毁

即使在线程安全的集合类上,使用iterator也是不安全的(除非加锁)即使是线程安全的collection类,仍可能产生竞争,执行其上某个操作是threadsafe的,但如果多个操作放在一起,仍旧不安全

如何撰写线程安全策略

在代码中以注释的形式增加说明:该ADT采取了什么设计决策来保证线程安全?使用了上文的哪种策略?对数据的访问是否为原子的,不存在交错?除非你知道线程访问的所有数据,否则Confinement无法彻底保证线程安全(除非是在ADT内部创建的线程,可以清楚得知访问数据有哪些)

7.3 锁和同步

同步

线程安全不应依赖于偶然,程序员来负责多线程之间对mutable数据的共享操作,通过“同步” 策略,避免多线程同时访问数据;使用锁机制,获得对数据的独家修改权,其他线程被阻塞,不得访问。

synchronized (lock) {}
Lock是Java语言提供的内嵌机制。每个object都有相关联的lock,拥有lock的线程可独占式的执行该部分代码,保护了共享数据。注意:要互斥,必须使用同一个lock进行保护,可以用ADT自己做lock

Monitor模式:ADT所有方法都是互斥访问(如果用synchronized关键字,构造函数不需要添加)

锁也是不可以随意加的,因为同步机制给性能带来极大影响,除非必要,否则不要用。Java中很多mutable的类型都不是threadsafe就是这个原因。尽可能减小lock的范围,避免在方法spec中加synchronized,而是在方法代码内部更加精细的区分哪些代码行可能有threadsafe风险,为其加锁。

任何共享的mutable变量/对象必须被lock所保护,涉及到多个mutable变量的时候,它们必须被同一个lock所保护。monitor pattern中,ADT所有方法都被同一个synchronized(this)所保护

死锁

死锁:多个线程竞争lock,相互等待对方释放lock
解决方案:
锁排序:对需要同时获得的锁进行排序,并确保所有代码按照该顺序获得锁。但该方式没有模块化,且在获取第一个锁之前,代码可能很难或不可能确切地知道它需要哪些锁,可能需要做一些计算才能算出来。
粗锁:使用一个锁来保护多个对象实例,甚至一个程序的整个子系统

wait()、notify() 和notifyAll()

保护块:这样的块首先循环判定一个条件,该条件必须为真才能继续执行,这将带来极大的性能浪费。

o.wait(): 释放o的锁,加入o的等待队列并等待。该操作使o所处的当前线程进入阻塞/等待状态,直到其他线程调用该对象的notify()操作

o.notify(): 唤醒o的等待队列中的一个线程(随机)

o.notifyAll(): 唤醒o的等待队列中的所有线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值