多线程的王国:Java并发探险 - 上篇

588 阅读8分钟

了解进程与线程

进程 是程序的基本执行实体,是线程的容器。例如:Java 程序启动执行之后变成了进程。
线程 是操作系统能够进行运算调度的最小单位。它被包含在进程之中一个进程中可以并发多个线程,每条线程并行执行不同的任务,线程共享进程中的系统资源。

并发 是多个程序在一段重叠的时间段中开始、运行与结束,但这些程序并没有在任何一个时刻同时在执行。
并行 是意味着在同一个时刻,存在两个以上任务在同时运行。

并行是要求更严格的并发,同时性要求更高。

探索进程与线程模型

进程是线程的容器,设计的初衷为了解决资源分配的问题。

图片.png

在创建一个进程时会有一个主线程同时存在。
目前的操作系统是把计算资源(CPU)分配给了线程。进程是不会直接对接计算资源,进程对接的是内存、文件、用户权限、操作系统的命名空间。

内核线程与用户线程

到这里进程和线程已介绍完毕,开始深处的挖掘。

图片.png

内核空间 进程和硬件的沟通桥梁,存在应用和硬件中间。内核的优先级和权限非常高,是能看到所有的内存,必须给它一个单独的空间。
应用空间 Java 程序是应用进程,运行在用户空间。
内核级线程与用户级线程 内核级线程由内核调度,用户级线程由应用自己调度。

思考一个问题:Java 线程线程模型,是由内核线程调用还是用户级线程调用?

图片.png

Java 老版本,Java 程序运行在虚拟机上,进程创建时会创建一条主线程,主线程是由内控线程调度,Java 的主线程是内核级,其它的线程是用户线程,操作系统是不会调用用户级线程,操作系统把 CPU 的执行权限给了主线程用户线程共享主线程的时间。

图片.png

现在的版本 Java 创建任何线程,都是由内核调度实现并发并行。由M个内核线程去响应N个用户级线程执行。

线程常见状态

图片.png

  1. 初始(NEW) 新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE) Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)
  3. 阻塞(BLOCKED) 表示线程阻塞于锁。
  4. 等待(WAITING) 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING) 该状态不同于 WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED) 表示该线程已经执行完毕。

Thread.join 线程进入 WAITING 状态。
Thread.sleep 线程进入 TIMED_WAITING 状态。
网络请求线程进入 BLOCKED 状态。

线程切换

Context Switch(上下文切换)切换是 CPU 的上下文。CPU 的上下文就是寄存器和程序计数器

图片.png

线程A 调用Thread.join、Thread.sleep或者网络请求造成 线程A 中断,操作系统保存当前线程寄存器。OS 调用 线程B,OS 恢复 B 寄存器。

对操作系统而言 JVM 是一种应用,对 Java 程序来说是真实的机器。JVM 作为进程向操作系统申请内存资源文件资源,计算资源分配给了线程。

原子操作

操作不可分,这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(上下文切换)原子操作很好理解。

那么思考 i++ 是不是原子操作?

而 i++ 不是原子操作而是 3 个原子操作组合而成的,读取 i 的值、计算 i+1、写入新的值。
既然是组合原子操作就会面临竞争条件(竞争灾难)问题。

竞争条件

造成竞争条件的原因主要为多个线程并发的执行了非原子操作的操作。
例如:两个线程执行了 i++ 竞争灾难的结果是不确定的。

竞争条件一般发生在临界区发生问共享资源。

图片.png

减少竞争

那么了解了该如何解决,首先要做的是减少竞争合理分化线程任务。

package com.summer;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

public class VocabularyCounter {
    public static void main(String[] args) {
        String[] bookPaths = {
                "book1.txt",
                "book2.txt",
                // ... (add paths to 300 books)
        };

        int numThreads = Math.min(Runtime.getRuntime().availableProcessors(), bookPaths.length);
        ExecutorService executor = Executors.newFixedThreadPool(numThreads);
        CompletionService<Map<String, Integer>> completionService = new ExecutorCompletionService<>(executor);

        for (String bookPath : bookPaths) {
            completionService.submit(new WordCountTask(bookPath));
        }

        Map<String, Integer> totalWordCount = new HashMap<>();

        for (int i = 0; i < bookPaths.length; i++) {
            try {
                Future<Map<String, Integer>> future = completionService.take();
                Map<String, Integer> wordCount = future.get();

                // 将本书的字数合并到总字数中
                synchronized (totalWordCount) {
                    for (Map.Entry<String, Integer> entry : wordCount.entrySet()) {
                        totalWordCount.merge(entry.getKey(), entry.getValue(), Integer::sum);
                    }
                }
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        executor.shutdown();

        // 打印或处理 totalWordCount 映射
    }
}

class WordCountTask implements Callable<Map<String, Integer>> {
    private String bookPath;

    public WordCountTask(String bookPath) {
        this.bookPath = bookPath;
    }

    @Override
    public Map<String, Integer> call() throws Exception {
        Map<String, Integer> wordCount = new HashMap<>();

        try (BufferedReader reader = new BufferedReader(new FileReader(bookPath))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String[] words = line.split("\s+");
                for (String word : words) {
                    word = word.toLowerCase().replaceAll("[^a-zA-Z]", "");
                    if (!word.isEmpty()) {
                        wordCount.merge(word, 1, Integer::sum);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return wordCount;
    }
}

实现原子操作 CAS

CAS(Compare-And-Swap) 是 CPU 底层支持的指令。

作用设置一个地址值类似变量赋值,如果想给这个变量赋值,需要知道它原有值的内容。

图片.png

举例:小明用 10000 分别买两个物品第一个是 1000,第二个是 2000,此时小明的余额是多少?如果发生竞争条件小明余额会是 8000 或者 9000。

避免这类事情发生 CAS 操作如下:

package com.summer;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCASExample {
    public static void main(String[] args) {
        AtomicInteger money = new AtomicInteger(10000);
        Thread thread1 = new Thread(() -> {
            int price = 1000;
            int currentMoney;
            do {
                currentMoney = money.get();
            } while (!money.compareAndSet(currentMoney, currentMoney - price));
            System.out.println("Thread 1: Bought item worth 1000, remaining money: " + money.get());
        });

        Thread thread2 = new Thread(() -> {
            int price = 2000;
            int currentMoney;
            do {
                currentMoney = money.get();
            } while (!money.compareAndSet(currentMoney, currentMoney - price));
            System.out.println("Thread 2: Bought item worth 2000, remaining money: " + money.get());
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final money: " + money.get());
    }
}

AtomicIntegercompareAndSet方法的原理。当compareAndSet方法被调用时,它首先检查共享变量的值是否等于期望值。
如果相等,就将共享变量的值更新为新的值,并返回true表示操作成功。
如果不相等,表示其它线程已经修改了共享变量的值,此时返回false表示操作失败。

实际的CAS操作是基于处理器指令的原子性特性来实现的,确保在单个指令周期内执行读取、比较和更新操作,避免了竞态条件。
这使得CAS成为一种高效的无锁同步操作,适用于并发编程中保证变量更新的原子性。

TAS 互斥锁

TAS(Test-And-Set)是一种基本的原子操作,通常用于实现互斥锁(mutex lock)的机制。
用于在多线程环境中保护临界区的访问。TAS操作通过检查并设置一个特定的标志位来实现,以确保只有一个线程可以进入临界区。

boolean flag = false;
while (true) {
    if (!flag) {
        flag = true;
        break; // 进入临界区
    }
}

CAS 解决了竞争条件,但没有解决两个线程同时 i++ 这类问题。
当两个线程读取到 i 的值是 100 时,是无法做到线程1i++ 结果是 101,线程2i++结果是 102,线程2想要执行成功必须在读取 i++ 之前的值是 101。

注意:CAS 还存在一个 ABA 的问题。

JAVA 锁

同步器

同步分为执行同步和数据同步,JAVA 中并发控制就是执行同步,缓存和存储的同步就是数据同步的一种了。

Java 架构下的同步器:

图片.png

Synchronized关键字是依赖于 C 和 C++ 写的Monitor,而其它全部依赖于ASQ。Monitor 存在于 JVM 层。

Synchronized的缺陷:

  • 不够灵活:synchronized关键字是内置的,因此它的灵活性有限。你只能使用它来实现基本的同步需求,无法在更高级别上进行自定义操作。
  • 隐式锁:synchronized使用隐式锁,这意味着锁的获取和释放都是由 JVM 自动管理的,无法手动控制,可能导致一些不可控的情况。
  • 性能问题:synchronized在某些情况下可能会引起性能问题。当多个线程竞争同一个锁时,会导致其它线程被阻塞,从而降低并发性能。

与ReentrantLock的区别:

  • 显示锁:ReentrantLock是显式锁,你可以手动控制锁的获取和释放,从而更加灵活地实现你的同步需求。
  • 公平锁和非公平锁: ReentrantLock可以配置为公平锁或非公平锁。公平锁按照线程请求锁的顺序获得锁,而非公平锁不保证按照顺序。
  • 可中断锁:ReentrantLock允许使用lockInterruptibly()方法来实现可中断的锁获取,这意味着当一个线程在等待锁的时候,可以响应中断。
  • 超时锁:ReentrantLock允许使用tryLock(long time, TimeUnit unit)方法来实现超时锁获取,可以在一段时间内尝试获取锁,如果超过时间则放弃。
  • 更好的性能:在高度竞争的场景下,ReentrantLock的性能可能会优于synchronized,因为它提供了更多的灵活性和更少的上下文切换。

综上所述,ReentrantLock相对于synchronized提供了更多的灵活性和控制,但使用它需要更多的编程工作,并且需要小心处理锁的获取和释放,以避免死锁等问题。在选择使用哪种机制时,你应该根据具体的情况来决定。