软件构造 7-1 Concurrency

7.1 并发

一. 并发性程序

  并行同一 CPU 上的并发执行,并发是指多个任务在时间上有重叠,这 n 个任务称为并发地执行,其概念与处理器等概念、数目无关
在这里插入图片描述
  并发程序的通信,共有以下两种方式:

  • 共享内存:在程序使用的内存中读写共享数据,只适合用于线程,不能用于进程。
  1. 两个处理器,共享内存
  2. 同一台机器上的两个程序,共享文件系统
  3. 同一个 Java 程序内的两个线程,共享 Java 对象
    在这里插入图片描述
  • 消息传递:通过 channel 交换消息。
  1. 网络上的两台计算机,通过网络连接通讯
  2. 浏览器和 Web 服务器, A 请求页面, B 发送页面数据给 A
  3. 即时通讯软件的客户端和服务器
  4. 同一台计算机上的两个程序,通过管道连接进行通讯
    在这里插入图片描述

二. 进程,线程,时间切片

  并发模块的类型:

  • 进程:私有空间,彼此隔离
  • 线程:程序内部的控制机制

1. 进程 Process

  进程拥有整台计算机的资源,但多进程不共享内存,进程之间通过消息传递进行协作。


  一般来说,进程==程序==应用,但一个应用中可能包含多个进程。

  • OS 支持的 IPC 机制 (pipe/socket)支持进程间(不仅是本机的多个进程之间,也可以是不同机器的多个进程之间)通信。

  JVM 通常运行单一进程,但也可以
创建新的进程。

2. 线程 Thread

  进程=虚拟机;线程=虚拟 CPU

  • 程序共享、资源共享,都隶属于进程

  进程间可共享内存

  • 很难获得线程私有的内存空间 (how about thread)
  • 通过创建消息队列在线程之间进行消息传递(即两种方式通信:消息传递、共享内存)

  使用多线程使得程序运行速度更快。

3. Java 创建 Thread

  首先明确:

  • 每个应用至少有一个线程
  • 主线程可以创建其他的线程

  Java 中创建 Thread 的方法:

  • (少用)从 Thread 类派生子类
  • (一般使用)从 Runnable 接口构造 Thread 对象

  第一种 Thread 创建方式:继承,重写 run() 方法,使用 start() 方法开始线程。

//public class Thread extends Object implements Runnable
public class HelloThread extends Thread {
	public void run() {
		System.out.println("Hello from a thread!");
	}
	public static void main(String args[]) {
		HelloThread p = new HelloThread();
		p.start();
	}
	//----------启动该线程的两个方式
	public static void main(String args[]) {
		(new HelloThread()).start();
	}
}

  但 Thread 中大多数方法无需使用,并且会形成复杂的继承树。


  第二种方法,继承 Runnable 接口,重写 run() 方法,将对象作为 Thread 的参数传递到 Thread 构造方法中,使用 start() 方法启动线程:

public class HelloRunnable implements Runnable {
	public void run() {
		System.out.println("Hello from a thread!");
	}
	public static void main(String args[]) {
		(new Thread(new HelloRunnable())).start();
	}
}

  可以用匿名类的方式简要编写为

new Thread(new Runnable() {
	public void run() {
		System.out.println("Hello");
	}
}).start();

  其执行流程:

  1. 创建 Runnable 对象
  2. 创建 Thread 对象
  3. 调用 Thread.start()
  4. 线程开始运行
  5. 打印 Hello
  6. 线程结束运行

  Runnable 接口实际上为:

public interface Runnable {
	void run();
}

  注:main 也是线程之一,于是如下程序共 3 个线程:

public class Moirai {
	public static void main(String[] args) {
		Thread clotho = new Thread(new Runnable() {
			public void run() { System.out.println("spinning"); };
		});
		clotho.start();
		new Thread(new Runnable() {
			public void run() { System.out.println("measuring"); };
		}).start();
		new Thread(new Runnable() {
			public void run() {	System.out.println ("cutting"); };
		});
	}
}

三. 交错和竞争

1. 时间分片

  虽然有多线程,但只有一个核,每个时刻只能执行一个线程。

  • 通过时间分片,在多个进程/线程之间共享处理器
    在这里插入图片描述
      即使是多核 CPU ,进程/线程的数目也往往大于核的数目
  • 时间分片
    在这里插入图片描述
      无论单核还是多核,时间分片均由 OS 自动调度的。有可能造成程序在不同时间执行结果不同。

2. Thread 之间的共享内存

  例如银行 ATM ,类似于共享内存:
在这里插入图片描述
  简化其操作,模拟共享内存:

// suppose all the cash machines share a single bank account
private static int balabce = 0;

private static void deposit() {
	balance = balance + 1;
}
private static void withdraw() {
	balance = balance - 1;
}

// each ATM does a bunch of transactions that
// modify balance, but leave it unchanged afterward
public static void cashMachine() {
	new Thread(new Runnable() {
		public void run() {
			for (int i = 0; i < TRANSACTION_PER_MACHINE; ++i)
				deposit();  // put a dollar in
				withdraw();  //take it back out
			}
		}
	}).start();
}

  按理,上述程序余额结果应该为 0 。但在很多情况下结果可能不为 0
  例如我们将 deposit() 划分为三个阶段

  1. get balance (balance = 0)
  2. add 1
  3. write back the result (balance = 1)

  例如其结果正确为 2
在这里插入图片描述
  而其结果不正确,为 1
在这里插入图片描述
  这种执行方式称为 Interleaving (交错)。与时间等等因素有关。根本原因是切片具有不确定性。然而程序员无法决定,这是由操作系统等等决定的。

3. 竞争条件

  如上例
在这里插入图片描述
  AB 称为 Race condition (竞争)或线程干扰 (Thread Interference)。这是由于单行、单条语句不一定是原子的,是否是原子的,对 Java 来说,是由 JVM 决定的


  线程的执行顺序与代码顺序无关

4. 消息传递

  多个线程通过多个消息队列传递消息。
  以银行 ATM 为例:
在这里插入图片描述
  消息传递机制也无法解决竞争问题,这是因为消息队列的每个操作不一定是原子的,可能转换为多个消息。如:
在这里插入图片描述

5. 线程难以调试与测试

  很难测试和调试因为竞争条件导致的 bug 。因为 interleaving 的存在,导致很难复现 bug

  bug 共两种:

  • Heisenbugs :随机性 bug,有时出现有事不出现
  • Bohrbugs :确定性 bug ,有输入会引起错误,则该输入一定会引起错误

  增添 print 语句可能导致 bug 消失,这是因为 print 语句执行时间长(普通语句的 100 - 1000 倍),改变了时间切片方式。

6. 利用某些方法调用来主动影响线程之间的 interleaving 关系

6.1 Thread.sleep()

  Thread.sleep() 是一种j静态的线程的休眠方法。

  • 将某个线程休眠,意味着其他进程得到更多的执行机会
  • 进入休眠的线程不会失去对现有 monitor 或锁的所有权
for (int i = 0; i < n ; i++) {
	//Pause for 4 seconds
	Thread.sleep(4000);
	//Print a message
	System.out.println(msg[i]);
}
6.2 Thread.interrupt()

  Thread.interrupt() 向进程发出中断信号

  • t.interrupt() 在其他线程里向 t 发出中断信号(出于安全性等方面的考虑,该操作实质上是打上标识符,然后程序是否中断即“停止”是由该线程自己决定,见后面)

  检查线程是否被中断

  • t.isInterrupted() 检查 t 是否已在中断状态中,但不对中断状态位修改
  • t.interrupted() 检查 t 是否已在中断状态中,并对中断状态位复位

  当某个线程被中断后,一般来说应停止其 run() 中的执行,取决于程序员在 run() 中处理

  • 一般来说,线程在收到中断信号时应该中断,直接终止,但出于安全性等方面的考虑,该操作实质上是打上标识符,然后程序是否中断即“停止”是由该线程自己决定

class Task implements Runnable{
	private double d = 0.0;
	public void run() {
		try{
			while (true) {
				for (int i = 0; i < 900000; i++)
					d = d + (Math.PI + Math.E) / d;
				Thread.sleep(500);
			}
		} catch(InterruptedException e) {return;}
	}
	//...
	Thread t = new Thread(new Task());
	t.start();
	Thread.sleep(100); //当前线程休眠
	t.interrupt(); //试图中断 t 线程
}

  上一程序 sleep() 检测标识位。sleep()interrupt() 相互配合。

class Task implements Runnable{
	private double d = 0.0;
	public void run() {
		try {
			//检查线程是否收到中断信号
			while (!Thread.interrupted()) {
				Thread.sleep(20);
				for (int i = 0; i < 900000; i++)
					d = d + (Math.PI + Math.E) / d;
			}
		} catch (InterruptedException e) {
			System.out.println("Exiting by Exception");
		}
	}
}

  上述程序有两种方式退出线程的执行。实质上上述程序 sleep() 操作去掉也可以终止运行。
  也可以不使用 sleep() 方式,如下:

if (Thread.interrupted()) {
	throw new InterruptedException;
}
6.3 Thread.join()

  让当前线程保持执行,直到其执行结束。

  • 一般不需要这种显式指定线程执行次序

  指定次序方法如下:

public class JoinExample {
	public static void main(String[] args)
		Thread th1 = new Thread(new MyClass (), "th1");
		Thread th2 = new Thread(new MyClass (), "th2");
		Thread th3 = new Thread(new MyClass (), "th3");
		th1.start();
		try {
			th1.join();
		} catch (InterruptedException ie) {}
		th2.start();
		try {
			th2.join();
		} catch (InterruptedException ie) {}
		th3.start();
		try {
			th3.join();
		} catch (InterruptedException ie) {}
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值