# 【NO.440】并发与多线程之线程安全篇

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

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

1. 并发程序之间有相互制约的关系。
2. 并发程序的执行过程是断断续续的。
3. 当并发设置合理并且 CPU 拥有足够的处理能力时，并发会提高程序的运行效率。

## 1.**线程安全**

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

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

![img](https://linuxcpp.0voice.com/zb_users/upload/2022/12/13/20221213160114_21905.jpg)

## 2.**NEW-新建状态**

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

```
@FunctionalInterface
public 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 的本质区别。

## **3.RUNNABLE-就绪状态**

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

## **4.RUNNING-运行状态**

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

## **5.BLOCKED-堵塞状态**

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

- 同步堵塞：锁被其他线程占用。
- 主动堵塞：调用 Thread 的某些方法，主动让出 CPU 执行权，比如 sleep()、join() 等。
- 等待堵塞：执行了wait() 。

## 6.**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 。

原文链接：https://zhuanlan.zhihu.com/p/488001949

作者：[Hu先生的Linux](https://www.zhihu.com/people/huhu520-10)