第十章 线程和分布式系统
本章关注复杂软件系统的构造。 本章关注复杂软件系统的构造。 这里的“复杂”包括三方面: 这里的“复杂”包括三方面: (1)多线程序 (2)分布式程序 (3) GUI 程序html
Outline
- 并发编程
- Shared memory
- Message passing
- 进程和线程
- 线程的建立和启动,runable
- 时间分片、交错执行、竞争条件
- 线程的休眠、中断
- 线程安全的四种策略
- 约束(Confinement)
- 不变性
- 使用线程安全的数据类型
- 同步与锁
- 死锁
- 以注释的形式撰写线程安全策略
Notes
## 并发编程
【并发(concurrency)】java
- 定义:指的是多线程场景下对共享资源的争夺运行
- 并发的应用背景:
- 网络上的多台计算机
- 一台计算机上的多个应用
- 一个CPU上的多核处理器
- 为何要有并发:
- 摩尔定律失效、“核”变得愈来愈多
- 为了充分利用多核和多处理器须要将程序转化为并行执行
- 并发编程的两种模式:
- 共享内存:在内存中读写共享数据
- 信息传递(Message Passing):经过channel交换消息
【共享内存】android
- 共享内存这种方式比较常见,咱们常常会设置一个共享变量,而后多个线程去操做同一个共享变量。从而达到线程通信的目的。
- 例子:
- 两个处理器,共享内存
- 同一台机器上的两个程序,共享文件系统
- 同一个Java程序内的两个线程,共享Java对象

【信息传递】程序员
- 消息传递方式采起的是线程之间的直接通讯,不一样的线程之间经过显式的发送消息来达到交互目的
- 接收方将收到的消息造成队列逐一处理,消息发送者继续发送(异步方式)
- 消息传递机制也没法解决竞争条件问题
- 仍然存在消息传递时间上的交错
- 例子:
- 网络上的两台计算机,经过网络链接通信
- 浏览器和Web服务器,A请求页面,B发送页面数据给A
- 即时通信软件的客户端和服务器
- 同一台计算机上的两个程序,经过管道链接进行通信

并发模型 |
通讯机制 |
同步机制 |
共享内存 |
线程之间共享程序的公共状态,线程之间经过写-读内存中的公共状态来隐式进行通讯。编程 |
同步是显式进行的。程序员必须显式指定某个方法或某段代码须要在线程之间互斥执行。api |
消息传递 |
线程之间没有公共状态,线程之间必须经过明确的发送消息来显式进行通讯。浏览器 |
因为消息的发送必须在消息的接收以前,所以同步是隐式进行的。安全 |
- 进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
- 程序运行时在内存中分配本身独立的运行空间
- 进程拥有整台计算机的资源
- 多进程之间不共享内存
- 进程之间经过消息传递进行协做
- 通常来讲,进程==程序==应用(但一个应用中可能包含多个进程)
- OS支持的IPC机制(pipe/socket)支持进程间通讯(IPC不只是本机的多个进程之间, 也能够是不一样机器的多个进程之间)
- JVM一般运行单一进程,但也能够建立新的进程。
- 线程:它是位于进程中,负责当前进程中的某个具有独立运行资格的空间。
- 线程有本身的堆栈和局部变量,可是多个线程共享内存空间
- 进程=虚拟机;线程=虚拟CPU
- 程序共享、资源共享,都隶属于进程
- 很难得到线程私有的内存空间
- 线程须要同步:在改变对象时要保持lock状态
- 清理线程是不安全的
- 进程是负责整个程序的运行,而线程是程序中具体的某个独立功能的运行。
- 一个进程中至少应该有一个线程。
- 主线程能够建立其余的线程。
## 线程的建立和启动,runable
【方式1:继承Thread类】服务器
- 方法:用Thread类实现了Runnable接口,但它其中的run方法什么都没作,因此用一个类作Thread的子类,提供它本身实现的run方法。用Thread.start()来开始一个新的线程。
- 建立:A类 a = new A类();
- 启动: a.start();
- 步骤:
- 定义一个类A继承于java.lang.Thread类.
- 在A类中覆盖Thread类中的run方法.
- 咱们在run方法中编写须要执行的操做:run方法里的代码,线程执行体.
- 在main方法(线程)中,建立线程对象,并启动线程.
- 栗子:
1 //1):定义一个类A继承于java.lang.Thread类.
2 class MusicThread extends Thread{
3 //2):在A类中覆盖Thread类中的run方法.
4 public void run() {
5 //3):在run方法中编写须要执行的操做
6 for(int i = 0; i < 50; i ++){
7 System.out.println("播放音乐"+i);
8 }
9 }
10 }
11
12 public class ExtendsThreadDemo {
13 public static void main(String[] args) {
14
15 for(int j = 0; j < 50; j ++){
16 System.out.println("运行游戏"+j);
17 if(j == 10){
18 //4):在main方法(线程)中,建立线程对象,并启动线程.
19 MusicThread music = new MusicThread();
20 music.start();
21 }
22 }
23 }
25 }
【方式2:实现Runable接口】网络
- 建立:Thread t = new Thread(new A());
- 调用:t.start();
- 步骤:
- 定义一个类A实现于java.lang.Runnable接口,注意A类不是线程类.
- 在A类中覆盖Runnable接口中的run方法.
- 咱们在run方法中编写须要执行的操做:run方法里的,线程执行体.
- 在main方法(线程)中,建立线程对象,并启动线程.
1 //1):定义一个类A实现于java.lang.Runnable接口,注意A类不是线程类.
2 class MusicImplements implements Runnable{
3 //2):在A类中覆盖Runnable接口中的run方法.
4 public void run() {
5 //3):在run方法中编写须要执行的操做
6 for(int i = 0; i < 50; i ++){
7 System.out.println("播放音乐"+i);
8 }
9
10 }
11 }
12
13 public class ImplementsRunnableDemo {
14 public static void main(String[] args) {
15 for(int j = 0; j < 50; j ++){
16 System.out.println("运行游戏"+j);
17 if(j == 10){
18 //4):在main方法(线程)中,建立线程对象,并启动线程
19 MusicImplements mi = new MusicImplements();
20 Thread t = new Thread(mi);
21 t.start();
22 }
23 }
24 }
- 实现Runnable接口相比继承Thread类有以下好处:
- 避免点继承的局限,一个类能够继承多个接口。
- 适合于资源的共享
- 建立并运行一个线程所犯的常见错误是调用线程的 run()方法而非 start()方法,以下所示:
Thread newThread = new Thread(MyRunnable());
newThread.run(); //should be start();
起初并不会感受到有什么不妥,由于 run()方法的确如你所愿的被调用了。可是,事实上,run()方法并不是是由刚建立的新线程所执行的,而是被建立新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让建立的新线程执行 run()方法,必须调用新线程的 start 方法。
【时间分片】
- 虽然有多线程,但只有一个核,每一个时刻只能执行一个线程。
- 即便是多核CPU,进程/线程的数目也每每大于核的数目
- 经过时间分片,在多个进程/线程之间共享处理器。(时间分片是由OS自动调度的)
- 当线程数多于处理器数量时,并发性经过时间片来模拟,处理器切换处理不一样的线程
【交错执行】
顾名思义,就是说在线程运行的过程当中,多个线程同时运行相互交错。并且,因为线程运行通常不是连续的,那么就会致使线程间的交错。能够说,全部线程安全问题的本质都是线程交错的问题。
【竞争条件】
竞争是发生在线程交错的基础上的。当多个线程对同一对象进行读写访问时,就可能会致使竞争的问题。程序中可能出现的一种问题就是,读写数据发生了不一样步。例如,我要用一个数据,在该数据修改还没写回内存中时就读取出来了,那么就会致使程序出现问题。
程序运行时有一种状况,就是程序若是要正确运行,必须保证A线程在B线程以前完成(正确性意味着程序运行知足其规约)。当发生这种状况时,就能够说A与B发生竞争关系。
- 计算机运行过程当中,并发、无序、大量的进程在使用有限、独占、不可抢占的资源,因为进程无限,资源有限,产生矛盾,这种矛盾称为竞争(Race)。
- 因为两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能由于时间上推动的前后缘由而出现问题,这叫作竞争条件(Race Condition)。
- 竞争条件分为两类:
-Mutex(互斥):两个或多个进程彼此之间没有内在的制约关系,可是因为要抢占使用某个临界资源(不能被多个进程同时使用的资源,如打印机,变量)而产生制约关系。
-Synchronization(同步):两个或多个进程彼此之间存在内在的制约关系(前一个进程执行完,其余的进程才能执行),如严格轮转法。
- 解决互斥方法:
Busy Waiting(忙等待):等着可是不停的检查测试,不睡觉,知道能进行为止
Sleep and Wakeup(睡眠与唤醒):引入Semapgore(信号量,包含整数和等待队列,为进程睡觉而设置),唤醒由其余进程引起。
- 临界区(Critical Region):
- 一段访问临界资源的代码。
- 为了不出现竞争条件,进入临界区要遵循四条原则:
- 任何两个进程不能同时进入访问同一临界资源的临界区
- 进程的个数,CPU个数性能等都是无序的,随机的
- 临界区以外的进程不得阻塞其余进程进入临界区
- 任何进程都不该被长期阻塞在临界区以外
- 解决互斥的方法:
• 禁用中断 Disabling interrupts
• 锁变量 Lock variables (no)
• 严格轮转 Strict alternation (no)
• Peterson’s solution (yes)
• The TSL instruction (yes)
【Thread.sleep】
- 在线程中容许一个线程进行暂时的休眠,直接使用Thread.sleep()方法便可。
- 将某个线程休眠,意味着其余线程获得更多的执行机会
- 进入休眠的线程不会失去对现有monitor或锁的全部权
- sleep定义格式:
public static void sleep(long milis,int nanos)
throws InterruptedException
首先,static,说明能够由Thread类名称调用,其次throws表示若是有异常要在调用此方法到处理异常。
因此sleep()方法要有InterruptedException 异常处理,并且sleep()调用方法一般为Thread.sleep(500) ;形式。

【Thread.interrupt】
- 一个线程能够被另外一个线程中断其操做的状态,使用 interrupt()方法完成。
- 经过线程的实例来调用interrupt()函数,向线程发出中断信号
- t.interrupt():在其余线程里向t发出中断信号
- t.isInterrupted():检查t是否已在中断状态中
- 当某个线程被中断后,通常来讲应中止 其run()中的执行,取决于程序员在run()中处理
- 通常来讲,线 程在收到中断信号时应该中断,直接终止
- 可是,线程收到其余线程发出来的中断信号,并不意味着必定要“中止”
- 实例:

package Thread1;
class MyThread implements Runnable{ // 实现Runnable接口
public void run(){ // 覆写run()方法
System.out.println("一、进入run()方法") ;
try{
Thread.sleep(10000) ; // 线程休眠10秒
System.out.println("二、已经完成了休眠") ;
}catch(InterruptedException e){
System.out.println("三、休眠被终止") ;
return ; // 返回调用处
}
System.out.println("四、run()方法正常结束") ;
}
};
public class demo1{
public static void main(String args[]){
MyThread mt = new MyThread() ; // 实例化Runnable子类对象
Thread t = new Thread(mt,"线程"); // 实例化Thread对象
t.start() ; // 启动线程
try{
Thread.sleep(2000) ; // 线程休眠2秒
}catch(InterruptedException e){
System.out.println("三、休眠被终止") ;
}
t.interrupt() ; // 中断线程执行
}
};
运行结果:
## 线程安全的四个策略
- 线程安全的定义:ADT或方法在多线程中要执行正确,即不管如何执行,不准调度者作额外的协做,都能知足正确性
- 四种线程安全的策略:
- Confinement 限制数据共享
- Immutability 共享不可变数据
- Threadsafe data type 共享线程安全的可 变数据
- Synchronization 同步机制共享共享线程 不安全的可变数据,对外即为线程安全的ADT.
【Confinement 限制数据共享】
- 核心思想:线程之间不共享mutable数据类型
- 将可变数据限制在单一线程内部,避免竞争
- 不容许任何县城直接读写该数据
- 在多线程环境中,取消全局变量,尽可能避免使用不安全的静态变量。
- 限制数据共享主要是在线程内部使用局部变量,由于局部变量在每一个函数的栈内,每一个函数都有本身的栈结构,互不影响,这样局部变量之间也互不影响。
- 若是局部变量是一个指向对象的引用,那么就须要检查该对象是否被限制住,若是没有被限制住(便可以被其余线程所访问),那么就没有限制住数据,所以也就不能用这种方法来保证线程安全
- 栗子:
public class Factorial {
/**
* Computes n! and prints it on standard output.
* @param n must be >= 0
*/
private static void computeFact(final int n) {
BigInteger result = new BigInteger("1");
for (int i = 1; i <= n; ++i) {
System.out.println("working on fact " + n);
result = result.multiply(new BigInteger(String.valueOf(i)));
}
System.out.println("fact(" + n + ") = " + result);
}
public static void main(String[] args) {
new Thread(new Runnable() { // create a thread using an
public void run() { // anonymous Runnable
computeFact(99);
}
}).start();
computeFact(100);
}
}
解释:主函数开启了两个线程,调用的是相同函数。由于线程共享局部变量的类型,但每一个函数调用有不一样的栈,所以有不一样的i,n,result。因为每一个函数都有本身的局部变量,那么每一个函数就能够独立运行,更新它们本身的函数值,线程之间不影响结果。
【Immutability 共享不可变数据】
不可变数据类型,指那些在整个程序运行过程当中,指向内存的引用是一直不变的,一般使用final来修饰。不可变数据类型一般来说是线程安全的,但也可能发生意外。
可是,程序在运行过程当中,有时为了优化程序结构,默默地将这个引用更改了。此时,客户端程序员是不知道它被更改了,对于客户端而言,这个引用仍是不可变的,但其实已经被悄悄更改了。这时就会发生一些线程安全问题。
解决方案就是给这些不可变数据类型再增长一些限制:
- 全部的方法和属性都是私有的。
- 不提供可变的方法,即不对外开放能够更改内部属性的方法。
- 没有数据的泄露,即返回值而不是引用。
- 不在其中存储可变数据对象。
这样就能够保证线程的安全了。
【Threadsafe data type(共享线程安全的可变数据)】
- 方法:若是必需要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。(在JDK中的类,文档中明确指明了是否threadsafe)
- 通常来讲,JDK同时提供两个相同功能的类,一个是threadsafe,另外一个不是。缘由:threadsafe的类通常性能上受影响。
- List、Set、Map这些集合类都是线程不安全的,Java API为这些集合类提供了进一步的decorator
private static Map<Integer,Boolean> cache = Collections.synchronizedMap(new HashMap<>());
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
- ***在使用synchronizedMap(hashMap)以后,不要再把参数hashMap共享给其余线程,不要保留别名,必定要完全销毁.(能够用private static Map cache =Collections.synchronizedMap(new HashMap<>());的方式实例化集合类)
- 即便在线程安全的集合类上,使用iterator也 是不安全的:
List<Type> c = Collections.synchronizedList(new
ArrayList<Type>());
synchronized(c) { // to be introduced later (the 4-th threadsafe way)
for (Type e : c)
foo(e);
}
- 须要注意用java提供的包装类包装集合后,只是将集合的每一个操做都当作了原子操做,也就保证了每一个操做内部的正确性,可是在两个操做之间不能保证集合类不被修改,所以须要用lock机制,例如

若是在isEmpty和get中间,将元素移除,也就产生了竞争。
前三种策略的核心思想:避免共享 --> 即便共享,也只能读/不可写(immutable) -->即便可写(mutable),共享的可写数据应本身具有在多线程之间协调的能力,即“使用线程安全的mutable ADT”
【Synchronization 同步与锁】
- 为何要同步
- 同步方法
- 同步代码块
- 使用锁机制,得到对数据的独家mutation权,其余线程被阻塞,不得访问
- Lock是Java语言提供的内嵌机制,每一个object都有相关联的lock
- 任何共享的mutable变量/对象必须被lock所保护
- 涉及到多个mutable变量的时候,它们必须被同一个lock所保护
## 死锁
- 定义:两个或多个线程相互等待对方释放锁,则会出现死锁现象。
- java虚拟机没有检测,也没有采用措施来处理死锁状况,因此多线程编程是应该采起措施避免死锁的出现。一旦出现死锁,整个程序即不会发生任何异常,也不会给出任何提示,只是全部线程都处于堵塞状态。
- 造成死锁的条件:
- 互斥条件:线程使用的资源必须至少有一个是不能共享的(至少有锁);
- 请求与保持条件:至少有一个线程必须持有一个资源而且正在等待获取一个当前被其它线程持有的资源(至少两个线程持有不一样锁,又在等待对方持有锁);
- 非剥夺条件:分配资源不能从相应的线程中被强制剥夺(不能强行获取被其余线程持有锁);
- 循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程(线程A等线程B;线程B等线程C;...;线程N等线程A。如此造成环路)。
- 防止死锁的方法:
- 加锁顺序:当多个线程须要相同的一些锁,可是按照不一样的顺序加锁,死锁就很容易发生。若是能确保全部的线程都是按照相同的顺序得到锁,那么死锁就不会发生。这种方式是一种有效的死锁预防机制。可是,这种方式须要你事先知道全部可能会用到的锁,但总有些时候是没法预知的

-
- 使用粗粒度的锁,用单个锁来监控多个对象
- 对整个社交网 络设置 一个锁 ,而且对其任何组成部分的全部操做都在该锁上进行同步。
- 例如:全部的Wizards都属于一个Castle, 可以使用 castle 实例的锁
缺点:性能损失大;
- 若是用一个锁保护大量的可变数据,那么久放弃了同时访问这些数据的能力;
- 在最糟糕的状况下,程序可能基本上是顺序执行的,丧失了并发性

-
- 加锁时限:在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程当中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功得到全部须要的锁,则会进行回退并释放全部已经得到的锁。
- 用 jstack 等工具进行死锁检测
## 以注释的形式撰写线程安全策略
- 在代码中以注释的形式添加说明:该ADT采起了什么设计决策来保证线程安全
- 阐述如何使rep线程安全;
- 写入表示不变性的说明中,以便代码维护者知道你是如何为类设计线程安全性的。
- 须要对安全性进行这种仔细的论证,阐述使用了哪一种技术,使用threadsafe data types, or synchronization时,须要论证全部对数据的访问都是具备原子性的
- 栗子:



- 字符串是不可变的而且是线程安全的; 可是指向该字符串的rep,特别是文本变量,并非不可变的;
- 文本不是最终变量,由于咱们须要数据类型来支持插入和删除操做;
- 所以读取和写入文本变量自己不是线程安全的。