7-1并发

1什么是并发编程?
并发,即同时发生多个计算
例如一个网络中的多个计算机,一台计算机上多个应用程序,网站处理多个用户,云计算;GUI前台和后台必须同时进行

为什么需要并发?这是由于近几年单核的计算速度不再按照摩尔定律提升了,为了运算的更快,必须将一个计算分成并发的部分

§共享内存:并发模块通过在内存中读写共享对象进行交互。
§消息传递:并发模块通过通信信道相互发送消息进行交互。模块发送消息,每个模块的传入消息排队等待处理。

§共享内存模型示例:
–A和B可以是同一台计算机中的两个处理器(或处理器核心),共享相同的物理内存。
–A和B可能是在同一台计算机上运行的两个程序,它们与可以读写的文件共享一个公共文件系统。
–A和B可能是同一Java程序中的两个线程,共享同一Java对象。

§消息传递示例:
–A和B可能是网络中的两台计算机,通过网络连接进行通信。
–A和B可能是web浏览器和web服务器–A打开与B的连接并请求网页,B将网页数据发送回A。-A和B可能是即时消息客户端和服务器。
–A和B可能是在同一台计算机上运行的两个程序,其输入和输出通过管道连接,如ls | greptyped输入到命令提示符中。

2 进程,线程,时间切片
并发模块本身有两种不同的类型:进程和线程,两种基本的执行单元。
–进程是正在运行的程序的实例,它与同一台计算机上的其他进程隔离。特别是,它在机器内存中有自己的私有部分
–线程是运行程序中的控制点。把它看作是正在运行的程序中的一个位置,再加上导致这个位置的方法调用堆栈(这样线程在到达返回语句时可以返回堆栈)。

(1)进程
是一个虚拟计算机(一个独立的执行环境,具有一套完整的、私有的基本运行时资源,特别是内存)。
–它让程序感觉自己拥有了整个机器–就像一台新的计算机已经被创建,有了新的内存,就为了运行这个程序。
与通过网络连接的计算机一样,进程之间通常不共享内存。
–一个进程根本无法访问另一个进程的内存或对象。
–相反,一个新的进程会自动为消息传递做好准备,因为它是用标准的输入和输出流创建的,通过消息进行协作

§进程通常被视为程序或应用程序的同义词。
–然而,用户所看到的单个应用程序实际上可能是一组协作进程。
§为了促进进程之间的通信,大多数操作系统支持进程间通信(IPC)资源,如管道和套接字。操作系统IPC(管道/插座)
-IPC不仅用于同一系统上的进程之间的通信,还用于不同系统上的进程之间的通信。
§Java虚拟机的大多数实现作为单个进程运行。但是Java应用程序可以使用ProcessBuilder对象创建额外的进程。

(2)线程
线程和多线程编程
§正如一个进程代表一个虚拟计算机,线程抽象代表一个虚拟处理器,线程有时被称为轻量级进程——使一个新线程模拟使一个新处理器在由该进程代表的虚拟计算机内。
–这个新的虚拟处理器运行相同的程序,并与进程中的其他线程共享相同的资源(内存、打开的文件等),即“进程中存在线程”。§线程自动准备共享内存,因为线程共享进程中的所有内存。
–需要特别努力才能获得对单个线程私有的“线程本地”内存。(线程堆栈如何?)
–还需要通过创建和使用队列数据结构来显式地设置消息传递。
在这里插入图片描述
为什么使用线程?
§面对阻塞活动时的性能
—考虑web服务器的性能
—多处理器上的性能§干净地处理自然并发性
§在Java线程中是一个现实
—Java线程中自然并发的干净处理是一个现实
—示例:垃圾收集器在自己的线程中运行
§为了利用我们的多核处理器,我们必须编写多线程代码

(3) 在Java中启动线程
多线程执行是Java平台的一个基本特性。
–每个应用程序至少有一个线程。
–从应用程序程序员的角度来看,只需从一个称为主线程的线程开始。此线程能够创建其他线程。
§创建线程的两种方法:
–(很少使用)子类化线程。
–从Runnable接口构造Thread对象

§Thread子类——Threadclass本身实现了Runnable,尽管它的run方法什么也不做。应用程序可以对线程进行子类划分,提供自己的run()实现。

§提供一个Runnableobject——Runnableinterface定义了一个方法run(),用来包含在线程中执行的代码。
–Runnableobject被传递给Threadconstructor。

有时候会使用匿名Runnable启动线程

在这里插入图片描述
通过lambda表达式创建线程
在这里插入图片描述
§始终使用Runnable的实例创建新的Thread(),并在线程上调用start()来启动它。

通常当我们实现一个接口时,我们通过声明一个类来实现。例如,给定Java API中的接口比较器:我们可以在这里插入图片描述
我们可以声明:
在这里插入图片描述
对于可排序集合,如果没有指定 比较器,会采用所保存对象实现的compareTo方法(需要实现 Comparable接口)

我们可以使用已有的比较器,也可以在比较的时候创建新的比较器

§匿名类声明一个实现接口的未命名类,并立即创建该类的唯一实例。与上面的代码相比:
§匿名类的优势
–如果我们在这段代码中只使用比较器,那么我们通过使用匿名类缩小了它的范围。对于命名类,任何其他代码都可以开始使用并依赖于StringLengthComparator。
–不再需要在其他地方搜索比较器的细节
§缺点:如果我们不止一次需要相同的比较器,我们可能会尝试复制并粘贴匿名类。指定的类是DRY的。
–如果匿名类的实现很长,它会中断周围的代码,使其更难理解。命名的类作为模块块被分离出来。
§因此匿名类适合于方法的短期一次性实现。

3 交错和竞争
(1)时间切片
§在具有单一执行核心的计算机系统中,在任何给定时刻,只有一个线程实际执行。
–单个内核的处理时间通过一个叫做时间切片的操作系统功能在进程和线程之间共享。
今天的计算机系统要有多个处理器或多个执行的处理器核心。所以,如何在计算机中有多个只有一个或两个处理器的并发线程?
–当线程多于处理器时,并发性通过时间切片模拟,这意味着处理器在线程之间切换。

§三个线程T1、T2和T3可能在具有两个实际处理器的计算机上进行时间切片。
–首先一个处理器运行线程T1,另一个处理器运行线程T2,然后第二个处理器切换到运行线程T3。
–线程T2只是暂停,直到它的下一个时间片在同一个处理器或另一个处理器上。
§在大多数系统中,时间切片是不可预测和不确定的,这意味着线程可以随时暂停或恢复。在这里插入图片描述
(2)共享内存示例
§线程之间的共享内存可能会导致细微的错误!!!
§示例:银行有使用共享内存模型的提款机,因此所有提款机都可以在内存中读写相同的帐户对象。在这里插入图片描述
§将银行简化为一个账户,在余额变量中存储一美元余额,并且有两个操作简单地增加或删除一美元:
在这里插入图片描述
§客户使用提款机进行如下交易:
在这里插入图片描述
§每笔交易只是一美元存款,然后是一美元提款,因此应保持账户余额不变。–每台机器处理一系列存款/取款交易。
§在一天结束时,无论有多少台提款机在运行,或者我们处理了多少交易,我们都应该预计账户余额仍然为0。
–但是如果我们运行这段代码,我们经常会发现一天结束时的余额不是0。如果同时运行多个cashMachine()调用(例如,在同一台计算机上的不同处理器上),那么一天结束时余额可能不会为零。为什么呢?
在这里插入图片描述
假设两台取款机,A和B,同时在处理一笔存款。
§下面是deposit()步骤通常如何分解为低级处理器指令:
§当A和B同时运行时,这些低级指令相互交错。
在这里插入图片描述
(3)竞争条件
§余额现在是1–A的美元丢失了!
–A和B都同时读取余额,分别计算最终余额,然后争先恐后地存储新的余额
–这没有考虑到对方的存款。
§竞争条件:程序的正确性(后条件和不变量的满足)取决于并发计算A和B中事件的相对时间。当这种情况发生时,我们说“A与B处于竞争状态”-一些事件的交错可能是可以的,因为它们与单个事件一致,非当前进程会产生,但其他交错会产生错误的答案
——违反后置条件或不变量。

调整代码没有帮助
§所有这些版本的银行账户代码都表现出相同的竞争条件!
§你不能仅仅从Java代码中看出处理器将如何执行它。你不知道原子操作是什么。
–它不是原子的,仅仅因为它是一行Java代码。
–它不会只触及balance一次,因为balance标识符只在一行中出现一次。
§Java编译器不会承诺它将从代码中生成什么低级操作。
–典型的现代Java编译器为这三个版本生成完全相同的代码!
在这里插入图片描述
§银行账户余额的竞争条件可以用不同处理器上顺序操作的不同交错来解释。
§但事实上,当使用多个变量和多个处理器时,甚至不能指望这些变量的变化以相同的顺序出现

问题在于,现代的编译器和处理器做了很多事情来提高代码的速度。其中一件事是制作变量的临时副本,并在更快的存储器(处理器寄存器或处理器缓存)中准备好,并在最终将它们存储回内存中的正式位置之前临时使用它们。§存储的顺序可能与代码中操作的变量不同。

关键点:不能通过查看 一个表达式来判断它是否在竞争条件下是安全的。竞争条件又称线程干扰

(4) 消息传递示例
§现在不仅是提款机模块,账户也是模块。
§模块通过相互发送消息进行交互。–传入的请求放在队列中,一次处理一个请求。
–发件人在等待其请求的答复时不会停止工作。它处理来自自己队列的更多请求。对其请求的答复最终会作为另一条消息返回。
在这里插入图片描述
§不幸的是,信息传递并不能消除竞争状况的可能性。
–假设每个帐户都支持获取余额和取款操作,并有相应的消息。–在提款机A和B的两个用户都试图从同一个账户中提取一美元。
–他们首先检查余额,以确保提取的金额永远不会超过账户所持有的金额,因为透支会引发巨额银行罚款。
问题又是交错,但这次是交错发送到银行账户的邮件,而不是A和B执行的指令。
§如果账户以美元开头,那么什么样的信息交错会使A和B认为他们都可以提取一美元,从而透支账户?在这里插入图片描述

(5) 并发性很难测试和调试
使用测试很难发现竞争状况。
–即使测试发现了一个bug,也很难将它定位到导致它的程序部分。
----为什么?
§并发错误的再现性非常差。
–很难让它们以同样的方式发生两次。
–指令或消息的交错取决于受环境强烈影响的事件的相对时间。
–延迟是由其他正在运行的程序、其他网络流量、操作系统调度决策、处理器时钟速度变化等引起的
–每次运行包含竞争条件的程序时,您可能会得到不同的行为。
heisenbug是一种软件缺陷,当人们试图研究它时,它似乎会消失或改变其行为。
顺序编程中几乎所有的错误都是bohrbugs

§当你试图用printlnor调试器查看海森bugs时,它甚至可能消失!
–原因是打印和调试要比其他操作慢得多,通常要慢100-1000倍,因此它们会极大地改变操作和交织的时间。
§因此将一个简单的printstatement插入到cashMachine()中: …突然之间,平衡值总是0,正如所希望的那样,错误似乎消失了。但它只是隐藏了,没有真正修复。

(6) 干扰线程自动交织的一些操作
§暂停执行线程Thread.sleep(time): :使当前线程在指定的时间段内暂停执行。
–这是一种有效的方法,使处理器时间可用于同一台计算机上运行的其他线程或其他应用程序。
–线程睡眠不会丢失当前线程获得的任何监视器或锁。

Thread.interrupt()
§线程通过调用线程对象上的中断来发送中断,以便使用interrupt()方法中断线程。
§Interruption是一种协作机制。当一个线程中断另一个线程时,被中断的线程不一定立即停止正在执行的操作。相反,打断是一种礼貌地要求另一个线程在它方便的时候停止它正在做的事情的方式。
§一些方法,像Thread.sleep(),Thread.join() 和 Object.wait()认真对待此请求,,但其他方法不是一定要对中断作出响 应,可以不予理会。

当另一个线程通过调 用 t.interrupt() 中断一个线程时,会出现以下两种情况之一: 如果被中断线程在执行一个低级可中断阻塞方法,例如 Thread.sleep()、 Thread.join() 或Object.wait(),那么它将响应终端,抛出 InterruptedException异常,程序捕获该异常后,可以做中断后的处理
– 否则, interrupt() 只是设置线程的中断状态,通知该线程有其他线程想终止它,让它 自己决定是否终止
在这里插入图片描述
每个线程都有一个与之相关联的 Boolean 属性,用于表示线程 的中断状态,中断状态初始时为 false
– t.interrupt() 执行后,t的中断状态设置为true
– t.isInterrupted() 检查t是否已在中断状态中,只是查询,不改变状态
– Thread.interrupted() 检查当前线程是否已在中断状态中,并重置状态 为false
– 注意:interrupt()和isInterrupted()是实例方法,interrupted()是 类方法

Thread.join()
§join()方法用于保持当前正在运行的线程的执行,直到指定的线程停止(完成执行)。
–在正常情况下,我们通常有多个线程,线程调度程序会调度线程,这并不保证线程的执行顺序。
–通过使用join()方法,我们可以使一个线程等待另一个线程。

§并发性:多个计算同时运行,
§共享内存和消息传递范例,
§进程和线程
–进程就像一个虚拟计算机;线程就像一个虚拟处理器,当结果的正确性(后置条件和不变量)取决于事件的相对定时时,多个线程共享相同的可变变量,而不协调它们正在做的事情。
–这是不安全的,因为程序的正确性可能取决于低级操作的时间安排。

§这些想法与我们优秀软件的关键特性联系在一起,主要表现在坏的方面。
§并发是必要的,但它会导致严重的正确性问题:
–安全错误。并发性错误是一些最难发现和修复的错误,需要仔细设计才能避免。
–易于理解。预测并发代码如何与其他并发代码交叉是程序员很难做到的。最好是以这样一种方式设计代码,程序员根本不必考虑交错。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值