第十章主要讲解了? 进程和线程 ? 线程的创建和启动,runnable ? 时间分片、交错执行、竞争条件 ? 线程的休眠、中断 ? 线程安全的四种策略**** ? Message passing ? 死锁 ? 以注释的形式撰写线程安全策略
并发意味着多个运算同时发生
两种常见的并发模型:共享 内存和消息传递
Shared memory并发程 序通过读写内存中的共享对象交互
两个处理器共享物理内存 两个程序共享文件 两个线程共享对象
Message passing在消息传递模型中,并发模块通过通信信道相互发送消息进行交互。 模块发送消息,并将每个模块的传入消息排队等待处理
络中两台计算机通信 即时消息的客户 端和服务器 通过管道连接两个程序的输入和输出
.并发模块本身主 要分为两种类型:进程和线程
进程是正在运行程序的一个实例,拥有自己私有 专用的内存空间
线程是正在运行程序的一个执行路径(一个进程可对应 多个线程),线程有自己的堆栈和局部变量,但是多个线程共享内存空间
进程可抽象为虚拟计算机, 拥有独立的执行环境和完整的资源
进程间通常不共享内存 进程不能 访问其他进程的内存或对象 需要特殊机制才能实现进程间共享内存
进程通信采用的是消 息传递方式(因采用标准I/O 流)
应用程序实际上可能是一组协作进程 为了实现进程间通信,大多数操作系统都 支持”进程间通信(IPC)资源”,例如pipe和socketava虚拟机本身的大多数实现都是作为单个进程运行的
线程可抽象为一个虚拟处理器,线程有时也称为 轻量级进程
线程与进程中的其他线程共享相同的资源( 内存,打开的文件等),即“线程存在于进程内
进程内的线程共享内存线程需要特殊处理才能实现消息传递和访问私有内存
解决阻塞活动时的性能
每个应用程序至少有一个 线程
站在程序员角度,main线程是开始线程, 可以通过它创建其他的线程
创建Thread类的子类 实现Runnable接口,
Thread类本身实现了Runnable,尽管它的run方法什么都不做。 应用程序可以子类化Thread,提供自己的run()实现。
作为参数传递给 new Thread(…)构造函数
方法2:提供Runnable对象 – Runnable接口定义单个方法run(),用于包含在线程中执行的代码。 Runnable对象被传递给Thread构造函数。
用不可变的共享变量 将共 享数据封装在线程安全的数据类型中 e.使用同步机制来防止线程同时使用变量
. 全局 静态变量不会自动受到线程访问限制 . 如果使用了全局静态变量,则应说明只允 许一个线程使用它们 在多线程 环境中,取消全局变量
一般来说,静态变量对于并行是非常危 险的,它们可能隐藏在似乎没有副作用或改变的无害功能之后
当有 多个线程同时执行cache.put()操作时,map可能会遭到破坏
Strategy 2: Immutability
.使用不可变的引用和数据类型
不可变解决了 因为共享可变数据造成的竞争,并简单地通过使共享数据不可变来解决它
final变量不允许再赋 值,所以声明为final的变量可以安全地从多个线程访问。
因为这种安全性 只适用于变量本身,仍然必须确保变量指向的对象是不可变的。
类型是不可变的: 如果类型的对象在其 整个生命周期中始终表示相同的抽象值
n.但实际 上,允许对rep进行改变,只要这些改变对客户是不可见的,并且对应的抽 象值不变,例如有益的变化
但是对于并发编程,这种隐藏的变化是不安全 的, 使用有益的变化的不可变数据类型必须使用锁使自己线程安全
类声明为final 更复杂的方法是使构造函数为私有,并使用工厂方法 构造实例。 如果成员变量包含对可变对象的引用,不要允许更改 这些对象
不要共享对可变对象的引用 不存储传递给构造函数的外部可变对象的引用 避免在方法返回值中包含对可变对象的引用不提供修改可变对象的方法
Strategy 3: Using Threadsafe Data Types
将共享的可 变数据存储在线程安全的数据类型中。
Java类库中线程安全的数据类型,会显 式地声明
因为线程安全的数据类型性能较差,所以Java对一些可变数据类型提 供两种形式供选择:线程安全的和线程不安全的。
StringBuffer 和 StringBuilder都是可变数据类型,功能基本相同 ? StringBuffer是线程安全的,StringBuilder不是 ? StringBuilder性能更好,推荐在单线程程序中使用
ArrayList , HashMap , and HashSet , cannot be used safely from more than one thread. 这些不是线程安全的类型
Java提供了线程安全 的Collections类型版本。
确保方法是原子的
原子方法:动作的内部操作不会同其他操作交叉, 不会产生部分完成的情况
包装的实现是将所有的实际工作委托给指定的容器,但在容器 的基础上添加额外的功能。
同步包装将自动同步(线程安全) 添加到指定集合
解决方案将是在需要迭代collection 时获取它的锁
Strategy 4: Locks and Synchronization
Synchronization: 防止多个线程同时访问共享数据
并发模块彼此间采用同步的方式共享内存,以解决竞争带来的问题
锁是一种抽象,某时刻最多只允许一个线程拥有锁
获取锁的拥有权,如果锁被其他线程拥有,将进入阻塞状态,等待锁释放后, 再同其他线程竞争获取锁的拥有权 锁拥有者释放锁的拥有权
锁机制可以确保锁的拥有者始终查看最新的数据,避免了 reordering问题
阻塞一般意味着线程等待(不再 继续工作),直到一个事件发生(其他线程释放锁)
java将锁定机制作为内置语言特性提供
每个类及其所有对象实例都有一个锁***
Object类也有锁,经常用于显示地定义锁
同步语句必须指定提供内部锁的对象 同步区域提供了互斥功能: 一次只能有一个线程处于由给定对 象的锁保护的同步区域中 锁定时,遵循顺序执行模式
锁用于保护共享数据变量,确 保原子性
在线程t中,会阻止其他线程进入保护块中,直到语句 块中代码执行完 锁只能确保与其他请求获取相同对象锁的线程 互斥访问
拥有对象的锁会自动阻止其他线程访问同步区域
锁只能确保与其他请求获取相同对象锁的线程互斥访问,如 果其他线程没有使用synchronized(obj)或者利用了不同的锁,则同 步会失效,
在编写类的方法时,最方便的锁是对象实例本身,即this。 作为一种简单的方法,我们可以通过将对sync的所有访问包装在synchronized(this)中来保护类的整个代表。使用这个变量作为锁,保护所有的众议员
. 在 Monitor pattern中,某时刻只能有一 个线程在类的一个实例
如果将关键字synchronized添加到方法签名中,那么Java就像在方法体周围编写synchronized(this)一样。 public synchronizedvoid increment(){c ++;}
当线程调用同步方法时,它会自 动获取该方法所在对象的内部锁,并在方法返回时释放它。即使返回是由未 捕获的异常引起的,也会释放锁。
同一对象上的同步方法的两次调用不会有交叉现 象
当一个线程在执行一个对象的同步方法时,所有其他线程如果调 用同一对象的同步方法块,则会挂起执行,直到第一个线程针对此对象的操 作完成
当一个同步方法退出时,它会自动建立一个与之后调用同 一个对象的同步方法的happens-before关系,这保证对象状态的更改 对所有线程都是可见的。
happens-before relationship
前 一个事件的结果可以被后续的事件获取(即使出于优化的目的,实际运 行中并不是按照指定顺序执行)
在Java中,采用happened-before机制,保证了语句A对内存的 写入对语句B是可见的,也就是在B开始读数据之前,A已经完成了数 据的写入
由于静态方法与类关联,而不是对象,此时线 程获取与该类关联的Class对象的内部锁
. 对类的静态字段的访问由 与该类的任何实例的锁截然不同的锁来控制
同步方法不用指定锁,但同 步语句/块必须指定锁
锁规则是确保同步代码是线程安全的策略
每个共享的可变变量必须由某个锁保护
如果一个不变量涉及多个 共享的可变变量(甚至可能在不同的对象中),那么涉及的所有变量都必 须由相同的锁保护
使用volatile变量可以降低内存一致性错误的风险
每次使用此类变量时都到主存中进行读取,而且当成员变量 发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两 个不同的线程总是看到某个成员变量的同一个值。避免了reordering带 来的问题。
volatile 不能提供必须的原子 特性,只能在有限的一些情形 下使用 volatile变量替代锁: 对变量的写操作不依赖于当前 值,变量的有效值独立于任何 程序的状态,包括变量的当前 状态。
. 同步方法意味着正在获取一个锁,而不考虑它是哪个锁,或者 它是否是保护你将要执行的共享数据访问的正确锁。
findReplace是一个静态方法,如果声明为synchronized方法后,将使用 其所在类的锁,将对所有对象生效。这样导致一次只有一个线程可以调用 findReplace,即使操作不同的buffer也不行。性能损
并发应用程序的及时执行能力被称为活跃度
由于使用锁需要线程等待(当另一个线程 持有锁时),因此可能会陷入两个线程都在等待对方的情况 – 此时都 无法继续
死锁描述了两个或更多线程 永远被阻塞的情况,都在等待对方
死锁可能涉及两个以上的模块: 线程间的依赖关系环是出现死锁的信
防止死锁的一种方法是对需要同时获 取的锁进行排序,并确保所有代码按照该顺序获取锁定 solution 1: lock ordering
2: coarse-grained locking
粗粒度的锁,用单个锁来监控多个对 象实例
s.饥饿 描述了线程无法获得对共享资源的访问,而无法取得进展的情况
当共享资源由“贪婪”线程导致长时间不 可用时,会发生这种情况。
如果一个线程的行为也是对 另一个线程的行为的响应,则可能导致活锁。
与死锁一样,活锁线程无法取得进一步进展
线程并未被阻止 – 他们只 是忙于响应对方恢复工作。
Thread.sleep(time): 让当前线程暂停指定时 间的执行,期间不参与CPU的调度,不释放所拥有的监视器资源(锁)
Join()方法用于保持当前正在运行的线程的执行,直到该线程死 亡(执行完毕),才能继续执行后续线程
让一 个线程等待另一个线程结束。(可能需要前面线程的输出结果作为输入)
防护块:在语句块开始放 一个轮询条件(一直检测),当条件为真才能执行后续的语句。
o.wait(): release lock on o, enter o‘s wait queue and wait 释放拥有对象o 锁的线程的拥有权,使线程进入等待状态
– o.notify(): wake up one thread in o‘s wait queue 唤醒对象o锁的等待队 列上的单个线程 – o.notifyAll(): wake up all threads in o‘s wait queue唤醒对象o锁的等待 队列上的所有线程
执行wait()后,当前线程会等待,直到其他线程调用此 对象的notify( ) 方法或 notifyAll( ) 方法
调用wait()时,当前线程需要拥有对象的锁。执行 wait()后,线程释放锁并等待。当被唤醒后,重新参与锁所有权的竞 争,成功后从之前开始wait的点继续执行
虚假唤醒:一个线程可以从挂起状态变为运行状态(被唤醒),即使该 线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被 中断,或者等待超时。
? 为了避免虚假唤醒,则采用循环的方法测试被唤醒的条件是否满足, 不满足则继续等待
其他线程获得锁,并调 用notify方法。
t. 被唤醒的资源会重新参与到锁的竞争中,获取后, 从wait的地方继续执行
阻塞方法不同于一般的要运行较长时间的方法
.一般方法的完成只取决于它所要 做的事情,以及是否有足够多可用的计算资源
而阻塞方法的完成还取决于一 些外部的事件,例如计时器到期,I/O 完成,或者另一个线程的动作( 释放一个锁,设置一个标志,或者将一个任务放在一个工作队列中
一般方法在它们的工作完成 后即可结束,而阻塞方法较难于预测,因为它们取决于外部事
每个线程 都有一个与之相关联的 Boolean 属性,用于表示线程的中断状态,中断状 态初始时为 false
当另一个线程通过调用 Thread.interrupt() 中断一个线程时,会出现以下两种情况之一:
如果被中断线程在执行一个低级可中断阻塞方法,例如 Thread.sleep()、 Thread.join() 或Object.wait(),那么它将取消阻塞并抛出 InterruptedException
s. 否则, interrupt() 只是设置线程的中断状态,通知该线程有其他线程想终止它,让它 自己决定是否终止
中断状态可以通过 Thread.isInterrupted() 来读取,还可以通过一个名为 Thread.interrupted() 的操作读取和清除
中断是一 种协作机制。当一个线程中断另一个线程时,被中断的线程不一定要立即 停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方 便的时候停止它正在做的事情。
有些方法,例 如 Thread.sleep(),很认真地对待这样的请求(立即响应),但每个方法不 是一定要对中断作出响应。对于中断请求,不阻塞但是仍然要花较长时间 执行的方法可以轮询中断状态,并在被中断的时候提前返回。 也可以随 意忽略中断请求,但是这样做的话会影响响应。
.中断是一 种协作机制。当一个线程中断另一个线程时,被中断的线程不一定要立即 停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方 便的时候停止它正在做的事情。
有些方法,例 如 Thread.sleep(),很认真地对待这样的请求(立即响应),但每个方法不 是一定要对中断作出响应。对于中断请求,不阻塞但是仍然要花较长时间 执行的方法可以轮询中断状态,并在被中断的时候提前返回。 也可以随 意忽略中断请求,但是这样做的话会影响响应。
线程安全的数据类型只能保 证其上操作的原子性,不能避免类型间操作的原子性。因此,当存在 类型间关系的时候,仅采用不可变类型和线程安全的数据类型是不够 的
通过通信通道显式交互,而不是共享可变数据
消息传递共享 的信息为不可变的,降低了产生bugs的可能
络中的Client/server模式,采用socket作为抽象
络通信是并发的,需要考 虑线程安全问题
需要设计client和server通讯用的线路协议
. socket的一些阻塞操作使代码编写更容易 ,但有可能造成死锁
客户端发起通信,服务器接 收、处理、回复,重复此过程,客户端断开连接。服务器可同时处理多个 客户端,客户端也可同时连接多个服务器。
客户机和服务器可在不同的计算机上,也 可在一台计算机上。
服务器进程绑定到特定的端口,在该端口上进行侦听。 客户端必须知道服务器正在侦听哪个端口
socket(套接字)是用于发送和/或接收数据的 络 连接中的端点(socket本质是API,对TCP/IP协议的封装
.客户端和服务器通. 队列本身是线程安全的数据类型过 络交换的数据以块的形式发送
络将这些数据块分成数据包,每个 数据包通过 络分别路由传输。另一端,接收器将数据包重新组装成一个字 节流
Message passing模型用于在 客户端和服务器端的进程间通过sockets传递消息
在同一进程的线程间通过message passing传递消 息,比通过锁定机制共享内存更受欢迎
使用同步 队列在线程之间传递消息
. 支持在检索元素时等待队列变为非 空,在存储元素时等待队列中的空间变得可用
多个生产者和消费者共享一个同步的队列,都可对其写入和 读取,需要同步安全机制
定长队列,队列满时put操作会阻塞
在C/S模式下, 可以关闭socket以停止 客户端或者服务器继续侦听消息 一种不推荐的策略:使用特殊的消息提示结束(如0,或 者null),但是魔数或者null都不是推荐的
方案1:重新设计ADT,使其具有结束标志和对应操作
方案2:采用interrupt()
interrupted()检测当前线程 是否被中断,如果被中断,返回 true,并清除中断标志
. 队列本身是线程安全的数据类型
传送的数据是不可变的
. 生产者和消费者内部的数据对外部是不可见的,仅通过消息通讯
. 如果消息是可 变的,则要确保同一时刻只有一个线程能够访问
限制可变消息或通过队列发送的数据,但一次只能 由一个线程访问。 实施时要格外注意。
GUI由视图对象组成,每个视图对象占据屏幕的某个部分,通常是称为其边界框的矩形区域。 ?视图概念在各种UI工具包中有各种各样的名称。 – 在Java Swing中,它们是JComponent对象; – 在HTML中,它们是元素或节点; – 在其他工具包中,它们可能被称为小部件,控件或交互器。 ?视图树:视图排列在包含层次结构中,其中一些视图包含其他视图。 – 典型的容器是窗户,面板和工具栏。 ?视图树不仅仅是一个任意层次结构,而且实际上是空间层次结构:子视图嵌套在其父级边界框内。
听众模式
?在GUI中,我们不直接编写这种方法,因为它不是模块化的 – 它将按钮,列表框和文本框的职责混合在一个地方。 ?相反,GUI利用视图树提供的空间分隔来提供功能分离。 ?鼠标单击和键盘事件分布在视图树周围,具体取决于它们出现的位置。 ?GUI输入事件处理是监听器模式的实例(也称为发布 – 订阅): – 事件源生成离散事件流,这些事件对应于源中的状态转换。 – 一个或多个侦听器注册对事件流的兴趣(订阅),提供在新事件发生时要调用的函数
回调
?actionPerformed监听器是一般设计模式的示例,即回调。 ?回调是客户端提供给模块以供模块调用的函数。 ?这与正常控制流形成对比,在正常控制流中,客户端正在进行所有调用:调用模块提供的功能。 ?通过回调,客户端提供一段代码供实施者调用。
视图树将屏幕组织成嵌套矩形树,用于调度输入事件以及显示输出。
?监听器模式向已注册的侦听器发送事件流(如鼠标或键盘事件或按钮操作事件)。
?Model-View-Controllerpattern分离职责:model = data,view = output,controller = input。
?长时间运行的处理应移至后台线程,但Swing视图树仅限于事件派发线程。 因此,从另一个线程访问Swing对象需要使用事件循环作为消息传递队列,以返回事件派发线程。
AWT – 已过时,但作为Swing的一部分?Swing – 迄今为止使用最广泛的?SWT – 在Eclipse之外很少使用?JavaFX–被称为Swing的替代品?一堆现代( 络和移动)框架 – 例如,Android的
以上为第十章内容,重点理解线程及其相关。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!