软件构造复习笔记-Part1 第十章

进程和线程

进程

  • 定义:正在运行程序的一个实例,拥有自己私有专用的内存空间
    • 可抽象为虚拟计算机,拥有独立的执行环境和完整的资源
    • 进程间通常不共享内存,不能访问其他进程的内存和对象,需要特殊机制
    • 进程通信采用消息传递方式,即标准I/O流,为了实现进程间通信,大多数操作系统都支持“进程间通信(IPC)资源”,如管道和socket

线程

  • 定义:正在运行程序的一个执行路径(一个进程可对应多个线程)
    • 线程有自己的堆栈和局部变量,但是多个线程共享内存空间
    • 可抽象为一个虚拟处理器,有时也称为轻量级进程
    • 线程存在于进程内,与进程中的其他线程共享相同的资源
    • 采用内存共享机制通信,需要特殊处理才能实现消息传递和私有内存
    • 线程需要同步:在改变对象时要保持lock状态
    • 清理线程是不安全的

1. 进程是负责整个程序的运行,而线程是程序中具体的某个独立功能的运行。
2. 一个进程中至少应该有一个线程。
3. 主线程可以创建其他的线程。

线程的创建和启动,runable

  1. 继承Thread类型
    1. 方法:用Thread类实现了Runnable接口,但它其中的run方法什么都没做,所以用一个类做Thread的子类,提供它自己实现的run方法。用Thread.start()来开始一个新的线程。
    2. 创建:MethodThread m = new MethodThread()
    3. 启动: m.start()
    4. 步骤:
      • 定义一个类MethodThread 继承于java.lang.Thread类
      • 在类MethodThread 中覆盖Thread类中的run()方法
      • 在run()方法中编写需要执行的操作:run()方法里的代码,线程执行体
      • 在main方法(线程)中,创建线程对象,并启动线程
    5. 例子:
    public class MethodThread extends Thread{
    	public void run() {
    		System.out.println("Hello from a thread!");
    	}
    	public static void main(String[] args) {
    		MethodThread m = new MethodThread();
    		m.start();
    	}
    }
    
  2. 实现Runable接口
    1. 创建:Thread t = new Thread();
    2. 调用:t.strat()
    3. 步骤:
      • 定义一个类A实现于java.lang.Runnable接口,注意A类不是线程类】
      • 在A类中覆盖Runnable接口中的run方法
      • 在run方法中编写需要执行的操作:run方法里的,线程执行体
      • 在main方法(线程)中,创建线程对象,并启动线程
    public class MethodRunable implements Runnable{
    	public void run() {
    		System.out.println("Hello from a thread!");
    	}
    	public static void main(String[] args) {
    		MethodThread m = new MethodThread();
    		m.start();
    	}
    }
    
  3. 惯用法:用一个匿名的Runnable启动一个线程,避免创建命名类
    new Thread(new Runable() {
    	public void run(){
    		System.out.println("Hello from a thread!");
    	}
    }).start();
    
  • 实现Runnable接口相比继承Thread类有如下好处:
    • 避免点继承的局限,一个类可以继承多个接口;
    • 适合于资源的共享
  • 创建并运行一个线程所犯的常见错误是调用线程的 run()方法而非 start()方法,如下所示:
Thread newThread = new Thread(MyRunnable());
newThread.run();  //should be start();

起初并不会感觉到有什么不妥,因为 run()方法的确如你所愿的被调用了。但是,事实上,run()方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让创建的新线程执行 run()方法,必须调用新线程的 start 方法。

时间分片、交错执行、竞争条件

时间分片
  • 在某时刻,一个运行核心上只有一个线程可以运行
    • 通过时间分片,再多个线程/进程之间共享处理器
  • 当线程数多于处理器数量时,并发性通过时间片来模拟,处理器切换处理不同的线程
  • 通过时间分片,在多个进程/线程之间共享处理器。(时间分片是由OS自动调度的)
交错执行

时间片的使用不可预知且非确定,线程可能随时暂停或恢复
交错执行示例

竞争条件

  竞争是发生在线程交错的基础上的。当多个线程对同一对象进行读写访问时,就可能会导致竞争的问题。程序中可能出现的一种问题就是,读写数据发生了不同步。
  程序的正确性(后置条件和不变量的满足)取决于并发计算A和B中事件的相对时间,例如A和B都需要从银行中取走全部存款,A取走时B可能不知道,导致了没有足够的存款,产生竞争。
  程序运行时有一种情况,就是程序如果要正确运行,必须保证A线程在B线程之前完成(正确性意味着程序运行满足其规约)。当发生这种情况时,就可以说A与B发生竞争关系。

线程的休眠、中断

休眠(Thread.sleep)-让当前线程暂停指定时间的执行
  • 在线程中允许一个线程进行暂时的休眠,直接使用Thread.sleep()方法即可。
    • 将某个线程休眠,意味着其他线程得到更多的执行机会
    • 进入休眠的线程不会失去对现有monitor或锁的所有权
public static void sleep(long milis,int nanos) throws InterruptedException

示例:

for (int i = 0; i < n; i++){
	Thread.sleep(1000);
	System.out.println("Hello!");
}
Thread.join-确保当前线程能够执行完毕

示例:

public class JoinExample2 {
	public static void main(String[] args) {
		Thread th1 = new Thread(new MyClass2(), "th1");
		Thread th2 = new Thread(new MyClass2(), "th2");
		Thread th3 = new Thread(new MyClass2(), "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) {}  
	}
}
Thread.wait-释放锁,让线程进入等待,直到调用notify()
Thread.interrupt
  • 一个线程可以被另一个线程中断其操作的状态,使用 interrupt()方法完成
    • 通过线程的实例来调用interrupt()函数,向线程发出中断信号
    • t.interrupt():在其他线程里向t发出中断信号
    • t.isInterrupted():检查t是否已在中断状态中
  • 每个线程都有中断状态,初始为false
  • 若该线程在执行低级可中断阻塞方法,取消阻塞,抛出中断异常,否则设置中断状态为true
  • 当某个线程被中断后,一般来说应停止 其run()中的执行,取决于程序员在run()中处理
    • 一般来说,线程在收到中断信号时应该中断,直接终止
  • 在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情
  • 当一个线程中断另一个线程时,被中断的线程不一定要立即停止,只需在愿意并且方便的时候停止,为安全地构造可取消活动提供了更大的灵活性
  • 如果活动在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态,中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止
    示例:
    中断示例

线程安全的四个策略

定义:无论如何执行,不许调度者做额外的协作,都能满足正确性

Confinement:限制可变变量的共享
  • 核心思想:线程之间不共享mutable数据类型
    • 将数据限制在单个线程中,避免线程在可变数据上进行竞争
    • 不允许任何线程直接读写该数据
  • 在多线程环境中,取消全局变量,尽量避免使用不安全的静态变量。
  • 局部变量保存在线程栈中,每个调用都有自己的变量副本,如果是对象的引用,则要确保不能引用任何其他线程可访问的对象。
  • 限制数据共享主要是在线程内部使用局部变量,因为局部变量在每个函数的栈内,每个函数都有自己的栈结构,互不影响,这样局部变量之间也互不影响。
  • 如果局部变量是一个指向对象的引用,那么就需要检查该对象是否被限制住,如果没有被限制住(即可以被其他线程所访问),那么就没有限制住数据,因此也就不能用这种方法来保证线程安全。
Immutability:用不可变的共享变量

不可变数据类型,指那些在整个程序运行过程中,指向内存的引用是一直不变的,通常使用final来修饰。不可变数据类型通常来讲是线程安全的,但也可能发生意外。

  • 解决了因为共享可变数据造成的竞争,可以安全地从多个线程访问final。
  • 这种安全性只适用于变量本身,仍需确保变量指向的对象是不可变的。
  • 如果类型的对象在整个生命周期中表示相同的抽象值,则类型不可变。
  • 若改变对用户不可见,且对应抽象值不变,允许对rep进行更改。
  • 不变性的强定义
    • 没有改变数据的操作
    • 所有字段均为private和final
    • 没有表示暴露
    • 表示中的任何可变对象都不能变化
    • 不存储传递给构造函数的外部可变对象的引用
    • 避免在方法返回值中包含对可变对象的引用
Threadsafe data type:将共享数据封装在线程安全的数据类型中

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

  • 如List、Set、Map等数据结构是不安全的,线程安全提供了Connections的类型,可确保方法是原子的(动作的内部操作不会同其他操作交叉,不会产生部分完成的情况),例:private static Map<Integer,Boolean> cache = Collections.synchronizedMap(new HashMap<>());
  • 统一采用包装类的形式,确保抛弃对底层非线程安全容器类的引用
  • 即使在线程安全的集合类上,使用iterator也 是不安全的,在迭代collection的时候需获取集合的锁
  • 原子操作仍然不足以完全避免竞争,如检查集合是否为空的时候,另一个线程可能提前取走了元素
  • 包装的实现是将所有的实际工作委托给指定的容器,但在容器的基础上添加额外的功能

前三种策略的核心思想:避免共享 --> 即使共享,也只能读/不可写(immutable) -->即使可写(mutable),共享的可写数据应自己具备在多线程之间协调的能力,即“使用线程安全的mutable ADT”

Synchronization:使用同步来防止线程同时访问变量
  • 锁是一种实现同步的抽象,某一时刻最多允许一个线程拥有锁
  • 锁的两种操作
    • 获取锁的所有权:如过锁被其他线程拥有,将进入阻塞状态,等待锁释放后再同其他线程竞争
    • 释放锁的所有权
  • 使用锁可以确保锁的所有者始终查看最新的数据
  • 锁只能确保与其他请求获取相同对象锁的线程互斥访问,若其他线程采用不同的锁,则同步失效
  • 当线程调用同步方法时,它会自动获取该方法对象的内部锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,也会释放锁
  • 同一对象上的同步方法的两次调用不会有交错现象

死锁

  • 由于使用锁需要线程等待,可能会陷入两个线程正在等待对方,陷入永远阻塞的情况
  • 死锁可能涉及两个以上的模块,线程间的依赖关系环是出现死锁的信号
  • 防止死锁方法
    • 对需要同时获取的锁进行排序,并确保所有代码按照该顺序获取锁定,例:
      在这里插入图片描述
    • 用单个粗粒度的锁监控多个对象实例(但性能损失大)

以注释的形式撰写线程安全策略

  • 需要对安全性进行这种仔细的论证,阐述使用了哪种技术,使用threadsafe data types, or synchronization时,需要论证所有对数据的访问都是具有原子性的
    示例:
    在这里插入图片描述
    反例:
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值