第十章--第一节:并发与线程安全

第十章:并行与分布式程序设计

第一节:并发与线程安全


问题一:并行程序设计两种模式

    1.共享内存

  • 两个处理器,共享内存
  • 同一台机器上的两个程序,共享文件系统
  • 同一个Java程序内的两个线程,共享Java对象
  • 第十章--第一节:并发与线程安全_第1张图片

    2.消息传递

  • 网络上的两台计算机,通过网络连接通讯
  • 浏览器和Web服务器,A请求页面,B发送页面数据给A
  • 即时通讯软件的客户端和服务器
  • 同一台计算机上的两个程序,通过管道连接进行通讯

问题二:进程、线程、时间切片

    1.进程:私有空间,彼此隔离

  • 拥有整台计算机的资源
  • 多进程之间不共享内存
  • 进程之间通过消息传递进行协作
  • 一般来说,进程==程序==应用
  • 但一个应用中可能包含多个进程
  • JVM通常运行单一进程,但也可以创建新的进程。

    2.线程:程序内部的控制机制

  • 进程=虚拟机;线程=虚拟CPU
  • 程序共享、资源共享,都隶属于进程
  • 很难获得线程私有的内存空间

    3.线程与进程的区别

第十章--第一节:并发与线程安全_第2张图片

问题四:线程的开发

  • 每个应用至少有一个线程
  • 主线程,可以创建其他的线程

    创建线程的第一种方法:

    1.从Thread类派生子类:(Seldom used) Subclassing Thread

    方法:用Thread类实现了Runnable接口,但它其中的run方法什么都没做,所以用一个类做Thread的子类,提供它自己实现的run方法。用Thread.start()来开始一个新的线程。

第十章--第一节:并发与线程安全_第3张图片

    创建线程的第二种方法:

    2.用一个类来实现Runnable接口,实现run的方法

第十章--第一节:并发与线程安全_第4张图片

    3.使用匿名内部类的方式创建一个线程

第十章--第一节:并发与线程安全_第5张图片

问题三:交错和竞争(Interleaving and Race Condition)

    1.时间分片(Time slicing)

场景:①虽然有多线程,但只有一个核,每个时刻只能执行一个线程。②即使是多核CPU,进程/线程的数目也往往大于核的数目

方法:通过时间分片,在多个进程/线程之间共享处理器。(时间分片是由OS自动调度的)

    2.竞争条件(Race Condition)


第十章--第一节:并发与线程安全_第6张图片

注意:单行、单条语句都未必是原子的。(是否为原子,有JVM决定)

*例:下列程序并发的执行时,可能会出现什么结果?

第十章--第一节:并发与线程安全_第7张图片

(答案:5、6、10、15、30、)

问题四:干扰线程自动交织的若干操作

    1.线程的休眠操作

    用法:Thread.sleep(time)(time的单位为毫秒)

    实例:

    第十章--第一节:并发与线程安全_第8张图片

  • 将某个线程休眠,意味着其他线程得到更多的执行机会
  • 进入休眠的线程不会失去对现有monitor或锁的所有权

    2.线程的中断

    用法:Thread.interrupt()

    ①通过线程的实例来调用interrupt()函数,向线程发出中断信号

    ②在其他线程里向t发出中断信号 t.interrupt()

    ③检查线程t是否被中断 t.isInterrupted()

  • 当某个线程被中断后,一般来说应停止其run()中的执行,取决于程序员在run()中处理
  • 一般来说,线程在收到中断信号时应该中断,直接终止;但是,线程收到其他线程发来的中断信号,并不意味着一定要“停止”…

实例:用sleep()接收外界传来的中断信号

第十章--第一节:并发与线程安全_第9张图片

实例:用函数来接收外部传来的中断信号

第十章--第一节:并发与线程安全_第10张图片

    3.线程的放弃

    方法:Thread.yield()

    作用:使用该方法,线程告知调度器:我可以放弃CPU的占用权,从而可能引起调度器唤醒其他线程。(尽量避免在代码中使用)

    实例:

第十章--第一节:并发与线程安全_第11张图片

    4.线程的加入

    方法:Thread.join()

    作用:让当前线程保持执行,直到其执行结束(一般不需要这种显式指定线程执行次序)

    实例:

第十章--第一节:并发与线程安全_第12张图片

问题五:线程的安全

    含义:ADT或方法在多线程中要执行正确

    四种线程安全的方式:

  1.     限制数据共享
  2.     共享不可变数据
  3.     共享线程安全的可变数据
  4.     同步机制共享共享线程不安全的可变数据,对外即为线程安全的ADT

问题六:线程安全的实现策略

    1.策略一:约束(Confinement)

    核心思想:线程之间不共享mutable数据类型

    方法:

    ①将可变数据限制在单一线程内部,避免竞争

    ②不允许任何线程直接读写该数据

    示例:

    第十章--第一节:并发与线程安全_第13张图片

(避免使用全局变量)

第十章--第一节:并发与线程安全_第14张图片

    2.策略二:不变性

    方法:使用不可变数据类型和不可变引用,避免多线程之间的race condition。

  • 不可变数据通常是线程安全的
  • 如果ADT中使用了beneficent mutation(有益不变量),必须要通过“加锁”机制来保证线程安全

    对线程安全中不变性的定义:

  1.     没有mutator的方法
  2.     所有的区域都是private和final的
  3.     没有表示外泄
  4.     No mutation whatsoever of mutable objects in the rep – not even beneficent mutation

    3.策略三:使用线程安全的数据类型

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

  • List,Set,Map等集合类都是线程不安全的。
  • Java API为这些集合类提供了进一步的decorator

  • 第十章--第一节:并发与线程安全_第15张图片
  • ***在使用synchronizedMap(hashMap)之后,不要再把参数hashMap共享给其他线程,不要保留别名,一定要彻底销毁.(可以用private static Map cache =Collections.synchronizedMap(new HashMap<>());的方式实例化集合类)
  • 即使在线程安全的集合类上,使用iterator也是不安全的。(解决办法用lock机制,第十章--第一节:并发与线程安全_第16张图片)。
  • ****需要注意用java提供的包装类包装集合后,只是将集合的每个操作都看成了原子操作,也就保证了每个操作内部的正确性,但是在两个操作之间不能保证集合类不被修改,因此需要用lock机制,例如第十章--第一节:并发与线程安全_第17张图片如果在isEmpty和get中间,将元素移除,也就产生了竞争。

例:判断下列变量是不是线程安全的

第十章--第一节:并发与线程安全_第18张图片

第十章--第一节:并发与线程安全_第19张图片

(buildingName在任何情况下都是安全的;companyNames则不一定)。

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

    4.策略四:Locks and Synchronization

    方法:Synchronization 同步和锁,程序员来负责多线程之间对mutable数据的共享操作,通过“同步”策略,避免多线程同时访问数据。

  • 使用锁机制,获得对数据的独家mutation权,其他线程被阻塞,不得访问
  • Lock是Java语言提供的内嵌机制,每个object都有相关联的lock
  • 注意:对可变数据类型的访问线程要互斥,必须使用同一个lock进行保护
  • 当你要锁住一个类的时候,用ADT自己做lock是最方便的(synchronized(this).)(Monitor模式:ADT所有方法都是互斥访问
  • synchronized 等价于 synchronized(this)。synchronized 作为关键字加入到方法的签名中。锁住的是这个类的实例化对象,当调用有synchronized 标签的方法时,别的线程不能用this对象。(this指的是当前调用的类的对象)
  • 对synchronized的方法,多个线程执行它时不允许interleave,也就是说“按原子的串行方式执行”。
  • synchronized 与 synchronized(this)的区别
  • 第十章--第一节:并发与线程安全_第20张图片
  • 第十章--第一节:并发与线程安全_第21张图片
  • 任何共享的mutable变量/对象必须被lock所保护
  • 涉及到多个mutable变量的时候,它们必须被同一个lock所保护

例:回答下列问题

①:

第十章--第一节:并发与线程安全_第22张图片

第十章--第一节:并发与线程安全_第23张图片

第十章--第一节:并发与线程安全_第24张图片

问题七:应该在哪些地方用synchronized

  • 同步机制给性能带来极大影响
  • When you don’t need synchronization, don’t use it. 除非必要,否则不要用。Java中很多mutable的类型都不是thread safe就是这个原因
  • 尽可能减小lock的范围
  • Synchronized不是灵丹妙药,你的程序需要严格遵守设计原则,先尝试其他办法,实在做不到再考虑lock。
  • 所有关于threadsafe的设计决策也都要在ADT中记录下来。

问题八:死锁(Deadlock)、饥饿(Starvation)、活锁(Livelock)

    1.死锁(Deadlock)

    含义:多个线程竞争lock,相互等待对方释放lock

    模式图:

    第十章--第一节:并发与线程安全_第25张图片

    2.饥饿(Starvation)

    含义:因为其他线程lock时间太长,一个线程长时间无法获取其所需的资源访问权(lock),导致无法往下进行。

    3.活锁(Livelock)

    含义:....


你可能感兴趣的:(软件构造学习)