写点什么

并发与多线程之线程安全篇

  • 2022 年 3 月 26 日
  • 本文字数:2413 字

    阅读完需:约 8 分钟

并发是指某个时间段内,多个任务交替执行的能力。 CPU 把可执行时间均匀地分成若干份,每个进程执行一段时间后,记录当前的工作状态,释放当前的执行资源并进入等待状态,让其他进程抢占 CPU 资源。并行是指同时处理多任务的能力。目前, CPU 已经发展为多核,可以同时执行多个互不依赖的指令及执行块。

​ 并发和并行的目标都是尽可能快执行完所有的任务,两者区别核心在于进程是否同时执行,并发环境有着以下几个特点:

  1. 并发程序之间有相互制约的关系。

  2. 并发程序的执行过程是断断续续的。

  3. 当并发设置合理并且 CPU 拥有足够的处理能力时,并发会提高程序的运行效率。

线程安全

我们都知道,进程是操作系统进行资源分配的独立单位,而线程是 CPU 调度和分派的基本单位,为了更充分地利用 CPU 资源,一般都会使用多线程进行处理。多线程的作用是提高任务的平均执行速度

​ 线程可以拥有自己的操作栈、程序计数器、局部变量表等资源,它与同一进程内的其他线程共享该进程的所有资源。线程在生命周期内存在多种状态,分别为 NEW(新建状态)RUNNABLE(就绪状态)RUNNING(运行状态)BLOCKED(堵塞状态)DEAD(死亡状态) 五种状态。线程状态图如下

NEW-新建状态

​ New 是线程被创建且未启动的状态。线程被创建的方式有三种:第一种继承 Thread 类,第二种实现 Runnable 接口,第三种实现 Callable 接口。由于 Java 单继承限制,所以推荐第二种,实现 Runnable 接口可以使编程更加灵活,对外暴露的细节比较少,开发者只需专注于具体的实现,即 run 方法的实现。第三种方式是实现 Callable 接口,代码如下:

@FunctionalInterfacepublic interface Callable<V> {	    //	Computes a result, or throws an exception if unable to do so.    //	Returns:computed result    //	Throws:Exception – if unable to compute a result    V call() throws Exception;}
复制代码

从注释中看出,当出现无法计算的结果就会抛出异常,并且 Callable 接口是存在返回值的。这是 Callable 跟 Runnable 的本质区别。

RUNNABLE-就绪状态

​ Runnable 是调用 start() 之后并且在运行之前的状态。线程的 start 方法不能多次调用,否则将抛出 IllegalThreadStateException 异常。

RUNNING-运行状态

​ Running 是 run() 正在执行时线程的状态。线程可能会由于某些原因而退出 RUNNING ,如时间、异常、锁、调度等。

BLOCKED-堵塞状态

Blocked 是线程已经发生堵塞的状态,具体发生堵塞的有以下几种情况:

  • 同步堵塞:锁被其他线程占用。

  • 主动堵塞:调用 Thread 的某些方法,主动让出 CPU 执行权,比如 sleep()、join() 等。

  • 等待堵塞:执行了 wait() 。

DEAD-死亡状态

Dead 是线程已经执行完 run 方法,或因异常错误导致退出的状态,该状态无法逆转,即无法回到就绪状态。

​ 在计算机的线程处理过程当中,因为每个线程轮流占用 CPU 的计算资源,可能会出现某个线程尚未执行完就不得不中断的情况,容易导致线程不安全。例如,在服务端某个高并发业务共享某用户数据,首先 A 线程执行用户的查询任务,但是查询出来的数据还未返回就退出 CPU 时间片;然后后面进来的 B 线程抢占了 CPU 资源并覆盖了该用户数据,最后 A 线程重新执行,将 B 线程修改过后的数据返回给前端,导致页面出现数据异常。所以,为了保证线程安全,在多个线程并发地竞争共享资源时,通常采用同步机制协调各个线程的执行,确保得到正确的结果。

线程安全问题只在多线程环境下出现,单线程串行执行并不会存在此问题。为了保证高并发场景下的线程安全,可以从以下四个维度来探讨:

(1)数据单线程内可见。单线程总是安全的。通过限制数据仅在单线程内可见,可以避免数据被其他线程篡改。最典型的就是线程局部变量,它存储在独立虚拟机栈帧的局部变量表中,与其他线程毫无瓜葛。ThreadLocal 就是采用这种方式来实现线程安全的。

(2)只读对象。只读对象总是安全的。它的特性是允许复制、拒绝写入。最典型的只读对象有 String、Integer 等。一个对象想要拒绝任何写入,必须要满足以下条件:使用 final 关键字修饰类,避免被继承;使用 private final 关键字避免属性被中途修改;没有任何更新方法;返回值不能为可变对象。

(3)线程安全类。某些线程安全类的内部有非常明确的线程安全机制。比如 StringBuffer 就是一个线程安全类,它采用 synchronized 关键字来修饰相关方法。

(4)同步与锁机制。如果想要对某个对象进行并发更新操作,但又不属于上述三类,需要开发者在代码中自定义实现相关的安全同步机制。

线程安全的核心理念就是“要不只读,要不加锁”。JDK 提供的并发包,主要分成以下几个类族:

(1)线程同步类。这些类使线程间的协调更加容易,支持了更加丰富的线程协调场景,逐步淘汰了使用 Object 的 wait() 和 notify() 进行同步的方式。主要代表有 CountDownLatch、Semaphore、CyclicBarrier 等。

(2)并发集合类。集合并发操作的要求是执行速度快,提取数据准。最典型的莫过于 ConcurrentHashMap ,经过不断的优化,有刚开始的分段式锁到后来的 CAS ,不断的提高并发性能。除此之外,还有 ConcurrentSkipListMap 、 CopyOnWriteArrayList 、BlockingQueue 等。

(3)线程管理类。虽然 Thread 和 ThreadLocal 在 JDK1.0 就已经引入,但是真正把 Thread 的作用发挥到极致的是线程池。根据实际场景的需要,提供了多种创建线程池的快捷方式,如使用 Executors 静态工厂或者使用 ThreadPoolExecutors 等。另外,通过 ScheduledExecutorService 来执行定时任务。

(4)锁相关类。锁以 Lock 接口为核心,派生出一些实际场景中进行互斥操作的锁相关类。最有名的是 ReentrantLock 。

想学习 C++工程化、高性能及分布式、深入浅出。性能调优、TCP,协程,Nginx 源码分析 Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,Linux 内核,P2P,K8S,Docker,TCP/IP,协程,DPDK 学习资料视频获取点击:C++架构师学习资料

C++后台开发视频链接:C/C++Linux服务器开发高级架构师/Linux后台架构师​


用户头像

Linux服务器开发qun720209036,欢迎来交流 2020.11.26 加入

专注C/C++ Linux后台服务器开发。

评论

发布
暂无评论
并发与多线程之线程安全篇_线程_Linux服务器开发_InfoQ写作平台