【软件构造】第十章 线程和分布式系统

本章关注复杂软件系统的构造。 本章关注复杂软件系统的构造。 这里的“复杂”包括三方面: 这里的“复杂”包括三方面: (1)多线程序 (2)分布式程序 (3) GUI 程序

Outline

  • 并发编程

    • Shared memory
    • Message passing
  • 进程和线程
  • 线程的创建和启动,runable
  • 时间分片、交错执行、竞争条件
  • 线程的休眠、中断
  • 线程安全的四种策略
    • 约束(Confinement)
    • 不变性
    • 使用线程安全的数据类型
    • 同步与锁
  • 死锁
  • 以注释的形式撰写线程安全策略

Notes

## 并发编程

【并发(concurrency)】

  • 定义:指的是多线程场景下对共享资源的争夺运行
  • 并发的应用背景:
    • 网络上的多台计算机
    • 一台计算机上的多个应用
    • 一个CPU上的多核处理器
  • 为什么要有并发:
    • 摩尔定律失效、“核”变得越来越多
    • 为了充分利用多核和多处理器需要将程序转化为并行执行
  • 并发编程的两种模式:
    • 共享内存:在内存中读写共享数据
    • 信息传递(Message Passing):通过channel交换消息

【共享内存】

  • 共享内存这种方式比较常见,我们经常会设置一个共享变量,然后多个线程去操作同一个共享变量。从而达到线程通讯的目的。
  • 例子:
    • 两个处理器,共享内存
    • 同一台机器上的两个程序,共享文件系统
    • 同一个Java程序内的两个线程,共享Java对象

【信息传递】

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

并发模型 通信机制 同步机制
共享内存
线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。


同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。

消息传递
线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。


由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

## 进程和线程

  • 进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。

    • 程序运行时在内存中分配自己独立的运行空间
    • 进程拥有整台计算机的资源
    • 多进程之间不共享内存
    • 进程之间通过消息传递进行协作
    • 一般来说,进程==程序==应用(但一个应用中可能包含多个进程)
    • 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("1、进入run()方法") ;
        try{
                Thread.sleep(10000) ;    // 线程休眠10秒
                System.out.println("2、已经完成了休眠") ;
        }catch(InterruptedException e){
            System.out.println("3、休眠被终止") ;
            return ; // 返回调用处
        }
        System.out.println("4、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("3、休眠被终止") ;
        }
        t.interrupt() ;    // 中断线程执行
    }
};

运行结果:

1、进入run()方法
3、休眠被终止

## 线程安全的四个策略

  • 线程安全的定义: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 同步与锁】

  • 为什么要同步

    • java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查)
    • 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,
    • 从而保证了该变量的唯一性和准确性。
  • 同步方法
    • 即有synchronized关键字修饰的方法。
    • 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。
    • 在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
    • 代码如下:

      public synchronized void save(){} 
    • 注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
  • 同步代码块
    • 在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
    • 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
    • 代码如:

      synchronized(object){    }
    • 注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
  • 使用锁机制,获得对数据的独家mutation权,其他线程被阻塞,不得访问
  • Lock是Java语言提供的内嵌机制,每个object都有相关联的lock
  • 任何共享的mutable变量/对象必须被lock所保护
  • 涉及到多个mutable变量的时候,它们必须被同一个lock所保护

## 死锁

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

原文地址:https://www.cnblogs.com/hithongming/p/9204473.html

时间: 2024-12-09 10:30:42

【软件构造】第十章 线程和分布式系统的相关文章

麻省理工18年春软件构造课程阅读04“代码评审”

本文内容来自MIT_6.031_sp18: Software Construction课程的Readings部分,采用CC BY-SA 4.0协议. 由于我们学校(哈工大)大二软件构造课程的大部分素材取自此,也是推荐的阅读材料之一,于是打算做一些翻译工作,自己学习的同时也能帮到一些懒得看英文的朋友.另外,该课程的阅读资料中有许多练习题,但是没有标准答案,所给出的答案均为译者所写,有错误的地方还请指出. 译者:李秋豪 审校: V1.0 Thu Mar 8 22:58:41 CST 2018 本次课

软件构造 第一章

一.课程简介 1.软件构造课程的目标 ①程序设计与实现能力 ②系统设计与实现能力 ③系统分析与评价能力 ④利用现代软件构造工具的能力 2.传统软件设计步骤 ①讨论需要写的软件并实现②测试代码并改错③重复②.图示如下 3.更好的软件设计 ①未雨绸缪②考虑非功能质量属性③考虑多种设计选择④把设计决策明确写下来 4.设计的目标.原则和模式 设计目标:编程的视野 设计原则:编程的标尺 设计模式:编程的经验 二.软件构造多维视图 1.什么是软件 (1)构成: ·程序Program:UI, Algorith

软件构造期末复习考点总结

[考点 Equals] ==是引用等价性 :而equals()是对象等价性. == 比较的是索引.更准确的说,它测试的是指向相等(referential equality).如果两个索引指向同一块存储区域,那它们就是==的.对于我们之前提到过的快照图来说,==就意味着它们的箭头指向同一个对象. equals()操作比较的是对象的内容,换句话说,它测试的是对象值相等(object equality).e在每一个ADT中,quals操作必须合理定义. 基本数据类型,也称原始数据类型.byte,sho

面向对象软件构造 (Bertrand Meyer 著)

Part A: The Issues 议题 第一章 软件品质 第二章 面向对象的标准 Part B: The Road To Object Orientation 通向面向对象之路 第三章 模块性 第四章 复用性方法 第五章 走进对象技术 第六章 抽象数据类型 Part C: Object-Oriented Techniques 面向对象技术 第七章 静态结构: 类 第八章 运行时结构: 对象 第九章 内存管理 第十章 泛型 第一章 软件品质 1.1 外部和内在的因素 1.2 外部因素介绍 1.

Winform软件,不要在线程里操作UI

对于Winform软件,不要在线程里操作UI,不要相信:StartForm.CheckForIllegalCrossThreadCalls = false; 于是,把所有的代码都改成主线程委托调用的方式 private delegate void SetTextHandle(string id, string value); private void ThreadSetText(string id, string value) { this.Controls.Find(id, true)[0].

【软件构造】第一章 软件构造基础(2)

二.软件构造的质量目标 1. 外部属性(主要):影响用户感受,如外观.速度等 (1)正确性:符合规格范围和计划目标 ·只保证各个层面的正确性(假设调用正确) ·检验与调试 ·防御性编程 ·形式化编程 (2)健壮性:响应规格范围外的异常情况 ·提示错误信息 ·正常退出或降级 (3)可扩展性:提供增加新功能的空间 ·固化需求以规避风险 ·设计简洁.离散化 (4)可复用性:使软件模块能够被其他程序使用 ·模式固化 (5)兼容性:跨平台.跨软件交互 ·使用标准文件格式.数据结构.接口,保持一致性 ·定义

【软件构造】第二章第二节 软件构造的过程、系统和工具

第二章第二节 软件构造的过程.系统和工具 Outline 广义的软件构造过程 编程 静态代码分析 动态代码分析 调试与测试 重构 狭义的软件构造过程 构造系统:经典BUILD场景 构造系统的组件 构造过程和构造描述 Java编译工具 子目标和结构变体 构造工具 Notes ## 广义的软件构造过程 [编程(Coding)] 开发语言:如Java.C.Python 使用IDE(集成开发工具)的优势(组成) 方便编写代码和管理文件(有代码编辑器,代码重构工具.文件和库(Library)管理工具) 能

【软件构造】第六章第一节 可维护性的度量与构造原则

第六章第一节 可维护性的度量与构造原则 本章面向另一个质量指标:可维护性--软件发生变化时,是否可以以很小的代价适应变化? 本节是宏观介绍:(1)什么是软件维护:(2)可维护性如何度量:(3)实现高可维护性的设计原则--很抽象. Outline 软件的维护和演化 可维护性的常见度量指标 聚合度与耦合度 面向对象五大原则SOLID 单一职责原则SRP(Single Responsibility Principle) 开放封闭原则OCP(Open-Close Principle) 里式替换原则LSP

软件构造2

HIT - 软件构造 3章: 基本数据类型:int,long,boolean,double,char,short,byte,float 对象数据类型:classes,interface,arrays,enums,annotations Java是一种静态类型的语言,所有变量的类型在编译是已知的,Java可以静态检查,在像python这样的动态类型语言中,这种检查被推迟到运行时. 静态检查的类型:语法错误,名字错误,参数个数错误,参数类型错误,返回错误 动态检查的类型:非法参数,非法返回值,数组越