android线程安全ppt,线程进程安全.ppt

253b171540df25e1b84436cbe50dfc72.gif线程进程安全.ppt

第三章 线程 进程安全 进程和线程是两个范围不同的概念 进程是程序在计算机上的一次执行活动 运行一个程序 相当于启动了一个进程 进程是操作系统进行资源分配的单位 通俗地讲 是一个正在执行的程序 线程是进程中的一个实体 是被系统独立调度和分派的基本单位 它可与同属一个进程的其它线程共享进程所拥有的全部资源 一个线程可以创建和撤消另一个线程 同一进程中的多个线程之间可以并发执行 比如 一个在线播放软件 在播放歌曲的同时还可以进行下载 就可认为这两件工作由不同的线程完成 线程和进程的开发和相关操作 在程序设计中具有重要地位 线程和进程的安全和系统的安全息息相关 对于不够熟练的程序员来说 很容易出现安全隐患 而这些安全问题又具有不间断发生 难于调试等特点 一般说来 线程的安全性主要来源于其运行的并发性和对资源的共享性 进程的安全性主要在应用级别 在于其对系统的威胁性 不过对于系统软件的开发者 进程安全的考虑需要更加深入 本章主要针对线程和进程开发过程中的安全问题进行讲述 首先基于面向对象语言 讲解线程的的基本机制 然后讲解线程操作过程中的几个重要的安全问题 线程同步安全 线程协作安全 线程死锁 线程控制 最后讲解进程安全 3 1线程机制 3 1 1为什么需要线程 由于Java在线程操作方面具有较好的面向对象特性 也具有一定的代表性本章基于Java语言进行讲解 实际上 多线程最直观的说法是 让应用程序看起来好像同时能做好几件事情 为了表达这个问题 我们用一个案例来说明 比如 需要在控制台上每隔1秒钟打印一个欢迎信息 代码如下所示 publicclassP03 01 publicstaticvoidmain String args while true System out println Welcome try Thread sleep 1000 catch Exceptionex System out println 其他工作 代码行1 该程序似乎没有什么问题 运行时 Welcome 也能不断打印 但是 我们发现 打印函数中的while循环是个死循环 也就是说 这个死循环不运行完毕 程序将不能作其他事情 比如 程序中的代码行1永远也无法运行 这就给程序的功能形成了巨大的阻碍 在实际应用开发的过程中 经常会出现一个程序看起来同时作好几件事情的情况 如 程序进行一个用时较长的计算 希望该计算进行的时候 程序还可以做其他事情 程序进行一个用时较长的计算 希望该计算进行的时候 程序还可以做其他事情 软件要能够接受多个客户的请求 而让客户感觉不出等待 媒体播放器在播放歌曲的同时也能下载电影 财务软件在后台进行财务汇总的同时还能接受终端的请求 等等 在这些情况下 多线程就能够起到巨大的作用 线程和进程的关系很紧密 进程和线程是两个不同的概念 但是进程的范围大于线程 通俗地说 进程就是一个程序 线程是这个程序能够同时做的各件事情 比如 媒体播放机运行时就是一个进程 而媒体播放机同时做的下载文件和播放歌曲 就是两个线程 以上代码如果用线程来进行开发 在Java语言里面 就可以用如P03 02 java的方式 其他语言类似 运行 就会发现 此时 打印欢迎信息 和 其他工作 就 同时 做了 3 1 2线程机制和生命周期 每个程序至少自动拥有一个线程 称为主线程 当程序加载到内存时 启动主线程 从上节的程序可以看出 代码行 实际上相当于实例化一个新的线程对象 并运行该线程中的run 函数 该线程的运行并不影响主线程向下执行 这是为什么呢 这是由于多线程的机制实际上相当于CPU交替分配给不同的代码段来运行 也就是说 某一个时间片 某线程运行 下一个时间片 另一个线程运行 各个线程都有抢占CPU的权利 至于决定哪个线程抢占 是操作系统需要考虑的事情 由于时间片的轮转非常快 用户感觉不出各个线程抢占CPU的过程 看起来好像计算机在 同时 做好几件事情 WelcomeThreadwt newWelcomeThread wt start 一个线程有从创建 运行到消亡的过程 称为线程的生命周期 用线程的状态 state 表明线程处在生命周期的哪个阶段 线程有创建 可运行 运行中 阻塞 死亡五种状态 通过线程的控制与调度可使线程在这几种状态间转化 这五种状态详细描述如下 1 创建状态 使用new运算符创建一个线程后 该线程仅仅是一个空对象 系统没有分配资源 2 可运行状态 使用start 方法启动一个线程后 系统分配了资源 使该线程处于可运行状态 Runnable 3 运行中状态 占有CPU 执行线程的run 方法 4 阻塞状态 运行的线程因某种原因停止继续运行 5 死亡状态 线程结束 线程的安全隐患可能出现在各个状态 一般说来 线程的安全性来源于两个方面 1 多个线程之间可能会共享进程的内存资源 2 CPU的某个时间片分配给哪个线程使用 默认情况下无法由用户控制 多线程的安全问题比较复杂 解决方法繁多 在这里我们阐述几个比较典型的安全问题 3 2线程同步安全 3 2 1线程同步 默认情况下 线程都是独立的 而且异步执行 线程中包含了运行时所需要的数据或方法 而不需要外部的资源或方法 也不必关心其它线程的状态或行为 但是在多个线程在运行时共享数据的情况下 就需考虑其他线程的状态和行为 否则就不能保证程序的运行结果的正确性 在某些项目中 经常会出现线程同步的问题 即 多个线程在访问同一资源时 会出现安全问题 本节基于一个简单的案例 针对线程的同步问题进行阐述 所谓同步 就是在发出一个功能调用时 在没有得到结果之前 该调用就不返回 同时其它线程也不能调用这个方法 通俗地讲 一个线程是否能够抢占CPU 必须考虑另一个线程中的某种条件 而不能随便让操作系统按照默认方式分配CPU 如果条件不具备 就应该等待另一个线程运行 直到条件具备 3 2 2案例分析 给出一个案例 有若干张飞机票 2个线程去卖它们 要求没有票时能够提示 没有票了 以最后剩下3张票为例 首先用传统方法来编写这段代码 代码如P03 03 java所示 运行 控制台打印如下 这段程序貌似没有问题 但是它是很不安全的 并且这种不安全性很难发现 会给项目后期维护带来巨大的代价 观察程序中的代码行1处的注释 当只剩下一张票时 线程1卖出了最后一张票 接着要运行ticketNum 但在ticketNum 还没来得及运行的时候 线程2有可能抢占CPU 来判断当前有无票可卖 此时 由于线程1还没有将ticketNum 当然票数还是1 线程2判断还可以买票 这样 最后一张票卖出了两次 当然 上面的程序中 没有给线程2以买票的机会 实际上票都由线程1卖出 我们看不出其中的问题 为了让大家看清这个问题 我们模拟线程1和线程2交替卖票的情况 将P03 03 java的代码改为P03 04 java 该代码中 增加了一行 程序休眠1000毫秒 让另一个线程来抢占CPU 运行 控制台打印如下 最后一张票被卖出两次 系统不可靠 更为严重的是 该问题的出现很具有随机性 比如 有些项目在实验室运行阶段没有问题 因为哪个线程抢占CPU 是由操作系统决定的 用户并没有权利干涉 也无法预测 所以 项目可能在商业运行阶段出现了问题 等到维护人员去查问题的时候 由于问题出现的随机性 问题可能就不出现了 这种工作往往给维护带来了巨大的代价 以上案例是多个线程消费有限资源的情况 该情况下还有很多其他案例 如 多个线程 向有限空间写数据时 线程1写完数据 空间满了 但没来得及告诉系统 此时另一个线程抢占CPU 也来写 不知道空间已满 造成溢出 3 2 3解决方案 怎样解决这个问题 很简单 就是让一个线程卖票时其他线程不能抢占CPU 根据定义 实际上相当于要实现线程的同步 通俗地讲 可以给共享资源 在本例中为票 加一把锁 这把锁只有一把钥匙 哪个线程获取了这把钥匙 才有权利访问该共享资源 有一种比较直观的方法 可以在共享资源 如 票 每一个对象内部都增加一个新成员 标识 票 是否正在被卖中 其他线程访问时 必须检查这个标识 如果这个标识确定票正在被卖中 线程不能抢占CPU 这种设计理论上当然也是可行 但由于线程同步的情况并不是很普遍 仅仅为了这种小概率事件 在所有对象内部都开辟另一个成员空间 带来极大的空间浪费 增加了编程难度 所以 一般不采用这种方法 现代的编程语言的设计思路都是把同步标识加在代码段上 确切的说 是把同步标识放在 访问共享资源 如卖票 的代码段 上 不同语言中 同步代码段的实现模型类似 只是表达方式有些不同 这里以Java语言为例 在Java语言中 synchronized关键字可以解决这个问题 整个语法形式表现为 注意 synchronized后的 同步锁对象 必须是可以被各个线程共享的 如this 某个全局标量等 不能是一个局部变量 其原理为 当某一线程运行同步代码段时 在 同步锁对象 上置一标记 运行完这段代码 标记消除 其他线程要想抢占CPU运行这段代码 必须在 同步锁对象 上先检查该标记 只有标记处于消除状态 才能抢占CPU 在上面的例子中 this是一个 同步锁对象 synchronized 同步锁对象 访问共享资源 需要同步的代码段 因此 在上面的案例中 可以将将卖票的代码用synchronized代码块包围起来 同步锁对象 取this 如代码P03 05 java所示 运行 可以得到如下效果 这说明程序运行完全正常 从以上代码可以看出 该方法的本质是将需要独占CPU的代码用synchronized this 包围起来 如前所述 一个线程进入这段代码之后 就在this上加了一个标记 直到该线程将这段代码运行完毕 才释放这个标记 如果其他线程想要抢占CPU 先要检查this上是否有这个标记 若有 就必须等待 但是可以看出 该代码实际上运行较慢 因为一个线程的运行 必须等待另一个线程将同步代码段运行完毕 因此 从性能上讲 线程同步是非常耗费资源的一种操作 我们要尽量控制线程同步的代码段范围 理论上说 同步的代码段范围越小 段数越少越好 因此在某些情况下 推荐将小的同步代码段合并为大的同步代码段 实际上 在Java内 还可以直接把synchronized关键字直接加在函数的定义上 这也是一种可以推荐的方法 不过 值得一提的是 如果不能确定整个函数都需要同步 那就要尽量避免直接把synchronized加在函数定义上的做法 如前所述 要控制同步粒度 同步的代码段越小越好 synchronized控制的范围越小越好 否则造成不必要的系统开销 所以 在实际开发的过程中 要十分小心 因为过多的线程等待可能造成系统性能的下降 甚至造成死锁 3 3线程协作安全 3 3 1线程协作 有些情况下 多个线程合作完成一件事情的几个步骤 此时线程之间实现了协作 如一个工作需要若干个步骤 各个步骤都比较耗时 不能因为它们的运行 影响程序的运行效果 最好的方法就是将各步用线程实现 但是 由于线程随时都有可能抢占CPU 可能在前面一个步骤没有完成时 后面的步骤线程就已经运行 该安全隐患造成系统得不到正确结果 3 3 2案例分析 给出一个案例 线程1负责完成一个复杂运算 比较耗时 线程2负责得到结果 并将结果进行下一步处理 如 某个科学计算系统中 线程1负责计算1 1000各个数字的和 暂且认为它非常耗时 线程2负责得到这个结果并且写入数据库 读者首先想到的是将耗时的计算放入线程 这是正确的想法 首先用传统线程方法来编写这段代码 代码如P03 06 java所示 该程序貌似没有问题 也能够打印正确结果 但是和上一节的例子一样 它也是很不安全的 这种不安全性也很难发现 也会给项目后期维护带来巨大的代价 该程序的安全隐患在哪里呢 观察cal 函数中的代码 当线程th1运行后 线程th2运行 此时 线程th2随时可能抢占CPU 而不一定要等线程th1运行完毕 当然 在上面的例子中 可能因为线程th1运行较快 th2在它运行的过程中没有抢占CPU 碰巧 得到了正确结果 但是如果让线程th2抢占CPU 这样 系统可能得不到正确结果 为了解释这个问题 将P03 06 java的代码改为P03 07 java 该代码中 增加了一行 程序休眠1毫秒 让另一个线程来抢占CPU 运行 控制台打印如下 很显然 这个结果不是我们所需要的 那为什么sum得到的结果为1呢 很明显 线程th1的start函数运行时 相当于让求和过程以多线程形式运行 在它 休眠 之际 th2就抢占了CPU 在求和还没开始做或只完成一部分时就打印sum 导致得到不正常结果 3 3 3解决方案 怎样解决 显而易见 方法是 在运行一个线程时 命令其他线程等待该线程运行完毕 才能抢占CPU进行运行 对于该问题 不同语言解决方法类似 以Java语言为例 在Java语言中 线程的join 方法可以解决这个问题 见代码P03 08 java运行正常 实际上 该程序相当于摒弃了 线程就是为了程序看起来同时做好几件事情 的思想 将并发程序又变成了顺序的 如果线程th1没有运行完毕的话 程序会在th join 处堵塞 如果cal 函数耗时较长 程序将一直等待 一般的方法是 可以将该工作放在另一个线程中 这样 既不会堵塞主程序 又能够保证数据安全性 见代码P03 09 java 3 4线程死锁安全 3 4 1线程死锁 死锁 DeadLock 是指两个或两个以上的线程在执行过程中 因争夺资源而造成的一种互相等待的现象 此时称系统处于死锁状态 这些永远在互相等待的线程称为死锁线程 产生死锁的四个必要条件是 互斥条件 资源每次只能被一个线程使用 如前面的 线程同步代码段 就是只能被一个线程使用的典型资源 请求与保持条件 一个线程请求资源 但因为某种原因 该资源无法分配给它 于是该线程阻塞 此时 它对已获得的资源保持不放 不剥夺条件 进程已获得的资源 在未使用完之前 不管其是否阻塞 无法强行剥夺 循环等待条件 若干进程之间互相等待 形成一种头尾相接的循环等待资源关系 这四个条件是死锁的必要条件 只要系统发生死锁 这些条件必然成立 而只要上述条件之一不满足 就不会发生死锁 3 4 2案例分析 以Java语言为例 死锁一般来源于代码段的同步 当一段同步代码被某线程运行时 其他线程可能进入堵塞状态 无法抢占CPU 而刚好在该线程中 访问了某个对象 此时 除非同步锁定被解除 否则其他线程就不能访问那个对象 这可以称为 线程正在等待一个对象资源 如果出现一种极端情况 一个线程等候另一个对象 而另一个对象又在等候下一个对象 以此类推 这个 等候链 如果进入封闭状态 也就是说 最后那个对象等候的是第一个对象 此时 所有线程都会陷入无休止的相互等待状态 造成死锁 尽管这种情况并非经常出现 但一旦碰到 程序的调试将变得异常艰难 在这里给出一个死锁的案例 如代码P03 10 java 这段程序也貌似没有问题 但是和上一节的例子一样 它也是很不安全的 这种不安全性也很难发现 观察run 函数中的代码 当th1运行后 进入代码段1 锁定了S1 如果此时th2运行 抢占CPU 进入代码段3 锁定S2 那么th1就无法运行代码段2 但是又没有释放S1 此时 th2也就不能运行代码段4 造成互相等待 为了模拟这个过程 我们在程序中增加让其休眠的代码 好让另一个线程来抢占CPU 将P03 10 java的代码改为P03 11 java该代码中 增加了一行 程序休眠1000毫秒 让另一个线程来抢占CPU 运行 控制台打印如下 两个线程陷入无休止的等待 其原因是 线程th1进入代码段1后 线程2抢占CPU 锁定了S2 而线程th1对S1的锁定又没有解除 造成线程th2无法运行下去 当然 由于线程th2锁定了S2 线程th1也无法运行下去 死锁是一个很重要的问题 它能导致整个应用程序慢慢终止 尤其是当开发人员不熟悉如何分析死锁环境的时候 还很难被分离和修复 3 4 3解决方案 就语言本身来说 尚未直接提供防止死锁的帮助措施 需要我们通过谨慎的设计来避免 一般情况下 我们主要是针对死锁产生的四个必要条件来进行破坏 用以避免和预防死锁 在系统设计 线程开发等方面 注意如何不让这四个必要条件成立 如何确定资源的合理分配算法 避免线程永久占据系统资源 以Java为例 Java并不提供对死锁的检测机制 但可以通过javathreaddump来进行判断 一般情况下 当死锁发生时 Java虚拟机处于挂起状态 threaddump可以给出静态稳定的信息 从操作系统上观察 虚拟机的CPU占用率为零 这时可以收集threaddump 查找 waitingonitorentry 的线程 如果大量thread都在等待给同一个地址上锁 说明很可能死锁发生了 解决死锁没有简单的方法 这是因为线程产生死锁都各有各的原因 而且往往具有很高的负载 从技术上讲 可以用如下方法来进行死锁排除 可以撤消陷于死锁的全部线程 可以逐个撤消陷于死锁的进程 直到死锁不存在 从陷于死锁的线程中逐个强迫放弃所占用的资源 直至死锁消失 提示 关于死锁的检测与解除 有很多重要算法 如资源分配算法 银行家算法等 在操作系统的一些参考资料中应该可以进行足够了解 很多软件产品内置了死锁解决策略 可做参考 如 数据库死锁 一个连接占用了另一个连接所需的数据库锁 它可能阻塞另一个连接 如果两个或两个以上的连接相互阻塞 产生死锁 该情况下 一般会强制销毁一个连接 通常是使用最少的连接 并回滚其事务 这将释放所有与已经结束的事务相关联的锁 至少允许其他连接中有一个可以获取它们正在被阻塞的锁 资源池耗尽死锁 资源池太小 而每个线程需要的资源超过了池中的可用资源 产生死锁 此时可以增加连接池的大小或者重构代码 以便单个线程不需要同时使用很多资源 3 5线程控制安全 3 5 1安全隐患 线程控制主要是对线程生命周期的一些操作 如暂停 继续 消亡等 本节以Java语言为例 讲解线程控制中的一些安全问题 Java中提供了对线程生命周期进行控制的函数 stop 停止线程 suspend 暂停线程的运行 resume 继续线程的运行 destroy 让线程销毁 等等 线程生命周期中的安全问题主要体现在 线程暂停或者终止时 可能对某些资源的锁并没有释放 它所保持的任何资源都会保持锁定状态 线程暂停之后 我们无法预计它什么时候会继续 一般和用户操作有关 如果对某个资源的锁长期被保持 其他线程在任何时候都无法再次访问该资源 极有可能造成死锁 针对这个问题 为减少出现死锁的可能 Java1 2中 将Thread的stop suspend resume 以及destroy 方法定义为 已过时 方法 不再推荐使用 3 5 2案例分析 如前所述 线程的暂停和继续 早期采用suspend 和resume 方法 但是容易发生死锁 以线程暂停为例 调用suspend 的时候 目标线程会停下来 但却仍然持有在这之前获得的锁定 此时 其他任何线程都不能访问锁定的资源 除非被 挂起 的线程恢复运行 如果它们想恢复目标线程 同时又试图使用任何一个锁定的资源 就会造成死锁 下面给出一个案例 来说明这个问题 屏幕上不断打印欢迎信息 点击按钮 打印工作暂停 再点击 继续打印 传统代码如P03 12 java如果点击 暂停 按钮 则暂停打印 再点击 继续打印 如上所述 该代码实际上在事件响应中用suspend 和resume 来控制线程的暂停和继续 是不安全的 3 5 3解决方案 解决该问题 常见的方法有如下几种 1 当需要暂停时 干脆让线程的run 方法结束运行以释放资源 实际上就是让该线程永久结束 继续时 新开辟一个线程继续工作 怎样让run 方法结束呢 一般可用一个标志告诉线程什么时候通过退出自己的run 方法来中止自己的执行 2 将线程暂停或继续 不使用suspend 和resume 可在Thread类中置入一个标志 指出线程应该活动还是挂起 若标志指出线程应该挂起 便用wait 命其进入等待状态 若标志指出线程应当恢复 则用一个notify 重新启动线程 3 不推荐使用stop 来终止阻塞的线程 而应换用由Thread提供的interrupt 方法 以便中止并退出堵塞的代码 3 6进程安全 3 6 1进程概述 进程是一个执行中的程序 对每一个进程来说 都有自己独立的一片内存空间和一组系统资源 进程由进程控制块 程序段 数据段三部分组成 在进程概念中 每一个进程的内部数据和状态都是完全独立的 一个进程可以包含若干线程 线程可以帮助应用程序同时做几件事 比如一个线程向磁盘写入文件 另一个则接收用户的按键操作并及时做出反应 互相不干扰 进程也有运行 阻塞 就绪三种状态 并随一定条件而相互转化 3 6 2进程安全问题 由于进程的独立性 从应用的角度讲 进程安全比线程安全更受重视 一般针对已有的进程进行安全方面的控制 如 在系统安全中发现并清除病毒进程 在网络应用中 优化守护进程或端口扫描进程 等等 不过 从开发者 编程 的角度 进程的安全所需要考虑的问题和线程类似 但由于线程能够共享进程的资源 所以线程安全一般考虑的问题比进程安全多 不过 对于开发多个进程能够运行的系统软件 如操作系统 进程的安全就应该重点考虑了 一般情况下 此时考虑的问题和线程安全类似 因为在这种软件中 各个进程在使用系统有限的资源 和线程安全中考虑的问题类似 在此不再叙述 小结 本章主要针对线程和进程开发过程中的安全问题进行讲述 从开发者角度 首先讲解了线程的的基本机制 然后讲解线程操作过程中几个重要的安全问题 线程同步安全 线程协作安全 线程死锁 线程控制安全等 最后讲解了进程安全 练习 1 将P03 05 java中的同步代码写在一个同步函数中 2 线程1首先用Pen写字 然后用Pencil写字 线程2首先用Pencil写字 然后用Pen写字 编写一个因为线程1等待Pencil 线程2等待Pen而造成死锁的例子 并提出解决方法 3 两个线程 向空间有限的数组中写数据 写一段代码 具有数组溢出的安全隐患 并提出解决方案 4 界面上有个小红球 要求能够慢慢掉下来然后弹起来 为了逼真 当球在比较上方的时候 球比较大 球落下时 慢慢变小 在界面右下角有一个 暂停 按钮 可以让动画暂停 动画暂停之后又可以点击按钮让动画继续运行 5 举例说明进程安全和线程安全问题所考虑的问题的不同之处 6 Oracle数据库中 多个用户访问同一数据 可能会造成死锁 请你设计一个案例 进行测试 并观察其解决方法 7 很多语言中提供了定时器 Timer 让某个功能定时执行 该功能也可以用线程实现 比较两种方法的优劣 8 编写一个程序 每隔一秒 界面上方落下来一个字母 字母落到界面底端 消失 这里怎样控制线程的开始和结束 9 飞机射击游戏中线程是怎样应用的 10 将P03 11 java代码中同步段改为同步方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值