操作系统和并发的爱恨纠葛(三)

简介: 我一直没有急于写并发的原因是我参不透操作系统,如今,我已经把操作系统刷了一遍,这次试着写一些并发,看看能不能写清楚,卑微小编在线求鼓励...... 我打算采取操作系统和并发同时结合讲起来的方式。

活跃性问题


多线程还会带来活跃性问题,如何定义活跃性问题呢?活跃性问题关注的是 「某件事情是否会发生」

「如果一组线程中的每个线程都在等待一个事件,而这个事件只能由该组中的另一个线程触发,这种情况会导致死锁」

简单一点来表述一下,就是每个线程都在等待其他线程释放资源,而其他资源也在等待每个线程释放资源,这样没有线程抢先释放自己的资源,这种情况会产生死锁,所有线程都会无限的等待下去。

换句话说,死锁线程集合中的每个线程都在等待另一个死锁线程占有的资源。但是由于所有线程都不能运行,它们之中任何一个资源都无法释放资源,所以没有一个线程可以被唤醒。

如果说死锁很痴情的话,那么活锁用一则成语来表示就是 弄巧成拙

某些情况下,当线程意识到它不能获取所需要的下一个锁时,就会尝试礼貌的释放已经获得的锁,然后等待非常短的时间再次尝试获取。可以想像一下这个场景:当两个人在狭路相逢的时候,都想给对方让路,相同的步调会导致双方都无法前进。

现在假想有一对并行的线程用到了两个资源。它们分别尝试获取另一个锁失败后,两个线程都会释放自己持有的锁,再次进行尝试,这个过程会一直进行重复。很明显,这个过程中没有线程阻塞,但是线程仍然不会向下执行,这种状况我们称之为 活锁(livelock)

如果我们期望的事情一直不会发生,就会产生活跃性问题,比如单线程中的无限循环

while(true){...}
for(;;){}

在多线程中,比如 aThread 和 bThread 都需要某种资源,aThread 一直占用资源不释放,bThread 一直得不到执行,就会造成活跃性问题,bThread 线程会产生饥饿,我们后面会说。


性能问题


与活跃性问题密切相关的是 性能 问题,如果说活跃性问题关注的是最终的结果,那么性能问题关注的就是造成结果的过程,性能问题有很多方面:比如服务时间过长,吞吐率过低,资源消耗过高,在多线程中这样的问题同样存在。

在多线程中,有一个非常重要的性能因素那就是我们上面提到的 线程切换,也称为 上下文切换(Context Switch),这种操作开销很大。

在计算机世界中,老外都喜欢用 context 上下文这个词,这个词涵盖的内容很多,包括上下文切换的资源,寄存器的状态、程序计数器等。context switch 一般指的就是这些上下文切换的资源、寄存器状态、程序计数器的变化等。

在上下文切换中,会保存和恢复上下文,丢失局部性,把大量的时间消耗在线程切换上而不是线程运行上。

为什么线程切换会开销如此之大呢?线程间的切换会涉及到以下几个步骤

22.png

将 CPU 从一个线程切换到另一线程涉及挂起当前线程,保存其状态,例如寄存器,然后恢复到要切换的线程的状态,加载新的程序计数器,此时线程切换实际上就已经完成了;此时,CPU 不在执行线程切换代码,进而执行新的和线程关联的代码。


引起线程切换的几种方式


线程间的切换一般是操作系统层面需要考虑的问题,那么引起线程上下文切换有哪几种方式呢?或者说线程切换有哪几种诱因呢?主要有下面几种引起上下文切换的方式

  • 当前正在执行的任务完成,系统的 CPU 正常调度下一个需要运行的线程
  • 当前正在执行的任务遇到 I/O 等阻塞操作,线程调度器挂起此任务,继续调度下一个任务。
  • 多个任务并发抢占锁资源,当前任务没有获得锁资源,被线程调度器挂起,继续调度下一个任务。
  • 用户的代码挂起当前任务,比如线程执行 sleep 方法,让出CPU。
  • 使用硬件中断的方式引起上下文切换


线程安全性


在 Java 中,要实现线程安全性,必须要正确的使用线程和锁,但是这些只是满足线程安全的一种方式,要编写正确无误的线程安全的代码,其核心就是对状态访问操作进行管理。最重要的就是最 共享(Shared)的 和 可变(Mutable)的状态。只有共享和可变的变量才会出现问题,私有变量不会出现问题,参考程序计数器

对象的状态可以理解为存储在实例变量或者静态变量中的数据,共享意味着某个变量可以被多个线程同时访问、可变意味着变量在生命周期内会发生变化。一个变量是否是线程安全的,取决于它是否被多个线程访问。要使变量能够被安全访问,必须通过同步机制来对变量进行修饰。

如果不采用同步机制的话,那么就要避免多线程对共享变量的访问,主要有下面两种方式

  • 不要在多线程之间共享变量
  • 将共享变量置为不可变的

我们说了这么多次线程安全性,那么什么是线程安全性呢?


什么是线程安全性


根据上面的探讨,我们可以得出一个简单的定义:「当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的」

单线程就是一个线程数量为 1 的多线程,单线程一定是线程安全的。读取某个变量的值不会产生安全性问题,因为不管读取多少次,这个变量的值都不会被修改。


原子性


我们上面提到了原子性的概念,你可以把原子性操作想象成为一个不可分割 的整体,它的结果只有两种,要么全部执行,要么全部回滚。你可以把原子性认为是 婚姻关系 的一种,男人和女人只会产生两种结果,好好的说散就散,一般男人的一生都可以把他看成是原子性的一种,当然我们不排除时间管理(线程切换)的个例,我们知道线程切换必然会伴随着安全性问题,男人要出去浪也会造成两种结果,这两种结果分别对应安全性的两个结果:线程安全(好好的)和线程不安全(说散就散)。


竞态条件


有了上面的线程切换的功底,那么竞态条件也就好定义了,它指的就是「两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性时,这种就被称为竞态条件(race condition)」 ,线程切换是导致竞态条件出现的诱导因素,我们通过一个示例来说明,来看一段代码

public class RaceCondition {
  private Signleton single = null;
  public Signleton newSingleton(){
    if(single == null){
      single = new Signleton();
    }
    return single;
  }
}

在上面的代码中,涉及到一个竞态条件,那就是判断 single 的时候,如果 single 判断为空,此时发生了线程切换,另外一个线程执行,判断 single 的时候,也是空,执行 new 操作,然后线程切换回之前的线程,再执行 new 操作,那么内存中就会有两个 Singleton 对象。


加锁机制


在 Java 中,有很多种方式来对共享和可变的资源进行加锁和保护。Java 提供一种内置的机制对资源进行保护:synchronized 关键字,它有三种保护机制

  • 对方法进行加锁,确保多个线程中只有一个线程执行方法;
  • 对某个对象实例(在我们上面的探讨中,变量可以使用对象来替换)进行加锁,确保多个线程中只有一个线程对对象实例进行访问;
  • 对类对象进行加锁,确保多个线程只有一个线程能够访问类中的资源。

synchronized 关键字对资源进行保护的代码块俗称 同步代码块(Synchronized Block),例如

  synchronized(lock){
 // 线程安全的代码
}

每个 Java 对象都可以用做一个实现同步的锁,这些锁被称为 内置锁(Instrinsic Lock)或者 监视器锁(Monitor Lock)。线程在进入同步代码之前会自动获得锁,并且在退出同步代码时自动释放锁,而无论是通过正常执行路径退出还是通过异常路径退出,获得内置锁的唯一途径就是进入这个由锁保护的同步代码块或方法。

synchronized 的另一种隐含的语义就是 互斥,互斥意味着独占,最多只有一个线程持有锁,当线程 A 尝试获得一个由线程 B 持有的锁时,线程 A 必须等待或者阻塞,直到线程 B 释放这个锁,如果线程 B 不释放锁的话,那么线程 A 将会一直等待下去。

线程 A 获得线程 B 持有的锁时,线程 A 必须等待或者阻塞,但是获取锁的线程 B 可以重入,重入的意思可以用一段代码表示

public class Retreent {
  public synchronized void doSomething(){
    doSomethingElse();
    System.out.println("doSomething......");
  }
  public synchronized void doSomethingElse(){
    System.out.println("doSomethingElse......");
}

获取 doSomething() 方法锁的线程可以执行 doSomethingElse() 方法,执行完毕后可以重新执行 doSomething() 方法中的内容。锁重入也支持子类和父类之间的重入,具体的我们后面会进行介绍。

volatile 是一种轻量级的 synchronized,也就是一种轻量级的加锁方式,volatile 通过保证共享变量的可见性来从侧面对对象进行加锁。可见性的意思就是当一个线程修改一个共享变量时,另外一个线程能够 看见 这个修改的值。volatile 的执行成本要比 synchronized 低很多,因为 volatile 不会引起线程的上下文切换。

23.png

关于 volatile 的具体实现,我们后面会说。

我们还可以使用原子类 来保证线程安全,原子类其实就是 rt.jar 下面以 atomic 开头的类

24.png

除此之外,我们还可以使用 java.util.concurrent 工具包下的线程安全的集合类来确保线程安全,具体的实现类和其原理我们后面会说。

相关文章
|
5月前
|
供应链 安全 数据处理
操作系统高级议题:并发控制与进程互斥技术
操作系统高级议题:并发控制与进程互斥技术
101 0
|
4月前
|
负载均衡 算法 Linux
操作系统的演化之旅:从单任务到多任务再到现代并发处理
【7月更文挑战第28天】在数字世界的心脏,操作系统(OS)是支撑一切软件运行的基础。本文将带领读者穿梭于操作系统的发展史,揭示从简单单任务处理到复杂多任务和现代并发处理技术的演进过程。我们将探讨如何通过这些技术提高计算机资源的利用效率,并分析它们对现代软件开发实践的影响。
48 5
|
2月前
|
Web App开发 Linux iOS开发
操作系统的演变:从单任务到多核并发
在数字时代的浪潮中,操作系统作为计算机硬件与应用程序之间的桥梁,其发展历史充满了创新与变革。本文将带领读者穿越时空,探索操作系统如何从简单的单任务处理演化为今天能够高效管理多核处理器的复杂系统。我们将一窥各个时代下操作系统的设计哲学,以及它们是如何影响现代计算的方方面面。加入我们的旅程,一起见证技术的力量如何在每次迭代中重塑世界。
40 7
|
3月前
|
人工智能 监控 虚拟化
操作系统的演变:从单任务到多任务,再到并发和分布式
随着计算技术的发展,操作系统经历了从简单的单任务处理到复杂的多任务、并发处理,再到现代的分布式系统的转变。本文将探索这一演变过程中的关键里程碑,以及它们如何塑造我们今天使用的计算机系统的架构和性能。
|
4月前
|
算法 安全 Linux
操作系统的演变之旅:从单任务到多任务和并发处理
【7月更文挑战第30天】操作系统作为计算机硬件与软件之间的桥梁,其发展历程充满了创新与变革。本文将通过一次虚拟的时光旅行,探索操作系统从最初的单任务处理到现代多任务和并发处理的转变。我们将一窥操作系统如何适应计算需求的演进,并讨论这一过程中出现的关键概念和技术。
34 3
|
4月前
|
人工智能 分布式计算 物联网
操作系统的演变:从单任务到多任务再到并发和分布式
在数字时代的浪潮中,操作系统作为计算机硬件与应用程序之间的桥梁,其发展史是一部技术革新与需求演进的史诗。本文将带领读者穿梭于操作系统的时空隧道,从早期简单而原始的单任务系统出发,一路见证它如何逐步进化为支持多任务、并发执行乃至分布式计算的复杂系统。我们将一探究竟,是什么推动了这些转变,它们又是如何影响我们日常的技术实践与生活的。
62 1
|
4月前
|
Web App开发 物联网 Unix
操作系统的演变:从单任务到多任务再到并发与分布式
本文旨在探讨操作系统的发展历程,着重分析其从处理单一任务的原始阶段,经历多任务处理能力的增强,直至支持并发计算和分布式架构的现代转型。我们将追溯关键时间节点,审视技术创新如何塑造了今日操作系统的复杂性与多样性,并预测未来可能的发展趋势。
|
3月前
|
物联网 Unix Linux
操作系统的演变:从单任务到多任务再到现代并发
操作系统作为计算机的核心软件,其设计和架构的演变反映了计算需求和技术的进步。本文将带领读者穿越时间线,探索操作系统从最初的单任务处理,发展到多任务处理,直至当代复杂的并发和分布式处理系统的历程。我们将一窥各个时代下操作系统的设计哲学、关键技术以及它们如何塑造了今日的数字世界。
55 0
|
5月前
|
机器学习/深度学习 人工智能 算法
现代操作系统的演进与未来发展:从单任务到多任务并发
在计算机技术飞速发展的今天,操作系统作为计算机系统的核心,其功能和性能也在不断演进。本文将探讨操作系统从单任务到多任务并发的转变过程,分析其背后的技术驱动因素,并展望未来操作系统的发展方向。我们将深入剖析多任务操作系统的设计挑战、解决方案及其在现代计算环境中的应用,旨在为读者提供一个全面的理解。
|
5月前
|
算法 Java Linux
线程的魔法:揭开现代操作系统并发执行的面纱
线程的魔法:揭开现代操作系统并发执行的面纱
55 0