并发

1. 什么是线程

在 Java 中,线程是程序中独立执行的一个路径。它允许程序同时执行多个任务,实现并发性。每个线程都有自己的执行流程,可以执行不同的代码段,从而在同一时间内完成多个任务。

举一个简单的例子来说明 Java 中的线程:

假设您有一个简单的程序,需要同时下载多个文件,并在下载完成后将它们合并成一个文件。如果您只使用单线程来下载和合并文件,那么您必须等待一个文件下载完成后才能开始下载下一个文件,然后等待所有文件下载完成后才能开始合并。

但是,如果您使用多线程来完成这个任务,您可以同时启动多个下载任务,每个线程下载一个文件。这样,在一个文件下载的同时,其他线程可以继续下载其他文件。一旦所有文件都下载完成,您可以等待所有线程完成后再开始合并。

下面是一个简化的示例代码,展示了如何使用 Java 的线程来实现这个任务:

public class FileDownloader implements Runnable {
    private String url;
    private String fileName;

    public FileDownloader(String url, String fileName) {
        this.url = url;
        this.fileName = fileName;
    }

    @Override
    public void run() {
        // Simulate file download by sleeping for a while
        System.out.println("Downloading " + fileName + " from " + url);
        try {
            Thread.sleep(2000); // Simulate download time
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(fileName + " downloaded");
    }

    public static void main(String[] args) {
        Thread downloader1 = new Thread(new FileDownloader("http://example.com/file1.txt", "file1.txt"));
        Thread downloader2 = new Thread(new FileDownloader("http://example.com/file2.txt", "file2.txt"));
        Thread downloader3 = new Thread(new FileDownloader("http://example.com/file3.txt", "file3.txt"));

        downloader1.start();
        downloader2.start();
        downloader3.start();

        try {
            downloader1.join();
            downloader2.join();
            downloader3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Merge downloaded files
        System.out.println("All files downloaded and merged.");
    }
}

在这个例子中,我们创建了一个 FileDownloader 类,它实现了 Runnable 接口,表示一个下载任务。然后我们创建了三个线程分别下载三个文件,每个线程对应一个下载任务。使用 start() 方法启动线程,使用 join() 方法等待所有线程完成后再执行合并操作

2. 线程状态

在 Java 中,线程有不同的状态,表示线程在不同的生命周期阶段所处的状态。以下是 Java 线程的一些常见状态:

  1. New(新建状态): 当线程对象被创建但还没有调用 start() 方法时,线程处于新建状态。此时线程还没有分配系统资源,也没有开始执行。
  2. Runnable(就绪状态): 当调用线程的 start() 方法后,线程进入就绪状态。表示线程已经准备好运行,但可能还没有获得执行的机会,等待调度器分配时间片。
  3. Running(运行状态): 在就绪状态中,线程被调度执行后,线程进入运行状态。此时线程正在执行它的任务代码。
  4. Blocked(阻塞状态): 在某些情况下,线程可能会进入阻塞状态,例如等待输入/输出、获取锁、等待其他线程的通知等。在阻塞状态中,线程暂时停止执行,直到满足特定的条件才能继续执行。
  5. Waiting(等待状态): 当线程等待其他线程的通知或特定条件满足时,它可能进入等待状态。例如,使用 wait() 方法等待其他线程调用 notify()notifyAll() 方法来唤醒。
  6. Timed Waiting(计时等待状态): 类似于等待状态,但线程在一段时间后会自动唤醒。例如,使用 sleep() 方法等待一段时间。
  7. Terminated(终止状态): 当线程执行完其任务代码或由于异常而提前终止时,线程进入终止状态。一旦线程终止,它将不再执行。

这些状态构成了线程的生命周期,线程可以在不同状态之间转换。线程调度器负责将线程从一个状态切换到另一个状态,以实现并发执行。请注意,具体的线程状态可能会受到底层操作系统和 JVM 调度器的影响。

2.1 新建线程

线程的生命周期包括多个状态,其中一个是"New"(新建状态)。"New" 状态表示线程对象已经被创建,但尚未调用 start() 方法来启动线程的执行。在"New" 状态下,线程对象已经存在,但系统资源尚未分配给该线程,因此它还没有开始执行任何代码。

以下是关于"New" 状态的一些要点:

  1. 线程创建: 当您使用 new Thread() 创建一个新的线程对象时,线程进入"New" 状态。
  2. 未启动: 在"New" 状态下,线程对象已经存在,但线程的执行尚未开始。要使线程开始执行,您需要调用线程对象的 start() 方法。
  3. 无法重复启动: 一旦线程对象的 start() 方法被调用并线程进入其他状态(如"Runnable" 状态),您不能再次调用 start() 方法。尝试重复启动一个线程会导致 IllegalThreadStateException 异常。

2.2 可运行线程

"可运行"(Runnable)是线程的一种状态。线程的生命周期中的可运行状态表示线程已经准备好运行,并且可以在操作系统的线程调度器中被调度执行。这是线程在等待获取 CPU 时间片并执行代码的状态。

以下是关于"Runnable" 状态的一些要点:

  1. 线程就绪: 当调用线程对象的 start() 方法后,线程进入就绪状态。在就绪状态中,线程已经准备好运行,但还没有被操作系统调度器选中执行。
  2. 可能需要等待: 虽然线程处于可运行状态,但是实际的运行时间取决于操作系统的调度策略。多个可运行线程可能在等待 CPU 时间片,以便执行其任务代码。
  3. 竞争资源: 在多线程环境中,多个可运行线程可能会竞争共享的资源,如内存、文件、锁等。线程同步机制可用于确保多个线程安全地访问共享资源。

2.3 阻塞和等待线程

线程的生命周期中有两种与等待和阻塞相关的状态:阻塞状态(Blocked)和等待状态(Waiting 和 Timed Waiting)。

  1. Blocked(阻塞状态): 当线程在等待获取锁或等待某个条件满足时,它会进入阻塞状态。在阻塞状态下,线程暂时停止执行,直到满足特定的条件才能继续执行。常见的情况包括等待其他线程释放锁、等待输入/输出、等待获取资源等。
  2. Waiting(等待状态): 线程进入等待状态是因为它正在等待其他线程的通知,以便恢复执行。线程可以通过调用 Object 类的 wait() 方法进入等待状态,等待其他线程通过 notify()notifyAll() 方法通知它。线程在等待状态下不会占用 CPU 资源。
  3. Timed Waiting(计时等待状态): 与等待状态类似,线程进入计时等待状态是因为它正在等待一段时间后自动唤醒。线程可以通过调用 Thread 类的 sleep() 方法或使用其他计时等待的方法进入此状态。

这些状态的转换与线程在多线程环境中的交互和资源竞争有关。使用适当的线程同步机制(如锁、条件变量等),可以确保线程在等待和阻塞状态之间正确切换,并协调它们的执行。

下面是一个简单的示例,演示了线程的等待和阻塞状态:

public class ThreadStateExample {
    public static void main(String[] args) {
        Object lock = new Object();

        Thread waitingThread = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Waiting Thread: Waiting...");
                    lock.wait(); // Enter Waiting state
                    System.out.println("Waiting Thread: Resumed");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread blockingThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Blocking Thread: Working...");
                try {
                    Thread.sleep(2000); // Simulate work
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        waitingThread.start();

        // Sleep to allow waitingThread to enter Waiting state
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        blockingThread.start();

        // Sleep to allow blockingThread to enter Blocked state
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized (lock) {
            lock.notify(); // Wake up waitingThread
        }
    }
}

在这个示例中,我们创建了两个线程 waitingThreadblockingThread,并使用一个共享的锁对象 lock 进行同步。waitingThread 在锁上调用 wait() 方法,进入等待状态。blockingThread 在锁上执行一段时间的工作,进入阻塞状态。然后我们通过调用 lock.notify() 方法来唤醒等待中的线程。这个示例演示了等待和阻塞状态的转换。

2.4 终止线程

线程的终止是指线程执行完其任务代码或由于某种原因提前结束执行。线程终止后,它将不再执行任何代码。Java 提供了几种方式来终止线程的执行:

  1. 自然终止: 当线程的 run() 方法中的代码执行完毕,线程将自然终止。这意味着线程的任务已完成,不会再执行其他代码。
  2. 使用 return 语句: 在线程的 run() 方法中,您可以使用 return 语句来提前结束线程的执行。这将导致线程终止并退出。
  3. 使用 Thread.interrupt() 方法: 您可以调用线程的 interrupt() 方法来中断线程的执行。这会将线程的中断标志设置为 true,但实际中断的线程需要在适当的地方检查中断状态并做出响应。例如,可以在线程中的循环中检查中断状态,并在检测到中断时终止循环。
  4. 使用标志变量: 在线程的任务代码中使用标志变量来控制线程的执行。当标志变量为某个值时,线程终止执行。

下面是一个使用 Thread.interrupt() 方法来中断线程执行的示例:

public class InterruptThreadExample {
    public static void main(String[] args) {
        Thread myThread = new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("Thread is running...");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        });

        myThread.start();

        // Sleep to allow the thread to run for a while
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        myThread.interrupt(); // Interrupt the thread
    }
}

在这个示例中,我们创建了一个线程 myThread,它在循环中执行任务代码。线程在循环中检查自身的中断状态,如果中断标志被设置为 true,则线程会捕获 InterruptedException 并终止执行。在主线程中,我们启动 myThread 并等待一段时间后调用 myThread.interrupt() 方法来中断线程的执行。

请注意,线程终止时,它可能需要清理资源或执行其他必要的操作。确保在线程终止时进行适当的清理工作是很重要的。

3. 线程属性

3.1 中断线程

线程的中断是一种机制,允许在某些条件下中止线程的执行。使用 interrupt() 方法可以设置线程的中断标志为 true,但实际中断需要在线程的代码中检查中断状态并采取适当的操作。以下是一些中断线程的方法:

  1. Thread.interrupt() 方法: 调用这个方法会将线程的中断标志设置为 true。线程可以通过 Thread.currentThread().isInterrupted() 方法检查中断状态。
  2. Thread.isInterrupted() 方法: 这个方法用于检查线程的中断状态。
  3. InterruptedException 异常: 如果线程在等待某些操作时(如使用 sleep()wait() 等方法),其他线程调用了它的 interrupt() 方法,就会抛出 InterruptedException 异常。线程可以捕获这个异常来处理中断情况。

以下是一个简单的示例,演示如何中断线程的执行:

public class InterruptThreadExample {
    public static void main(String[] args) {
        Thread myThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Thread is running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted");
                    // Restore interrupted status
                    Thread.currentThread().interrupt();
                }
            }
        });

        myThread.start();

        // Sleep to allow the thread to run for a while
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

      
        myThread.interrupt(); // Interrupt the thread
    }
}

在这个示例中,我们创建了一个线程 myThread,它在循环中执行任务代码。线程在循环中检查自身的中断状态,如果中断标志被设置为 true,则线程会捕获 InterruptedException 并终止执行。在主线程中,我们启动 myThread 并等待一段时间后调用 myThread.interrupt() 方法来中断线程的执行。当线程被中断时,它会捕获 InterruptedException 并执行清理工作。

3.2 守护线程

守护线程(Daemon Threads)是一种特殊类型的线程,它在后台运行,为其他非守护线程提供服务。当所有的非守护线程终止时,守护线程会自动终止,无论它是否执行完毕。

以下是关于守护线程的一些特点和注意事项:

  1. 设置为守护线程: 您可以使用 setDaemon(true) 方法将线程设置为守护线程。该方法必须在线程启动之前调用,否则会抛出 IllegalThreadStateException 异常。
  2. 主线程不是守护线程: 主线程(即 main 方法运行的线程)不是守护线程,它会等待所有非守护线程执行完毕再终止。只有当所有的非守护线程终止时,Java 虚拟机才会终止,不会等待守护线程执行完毕。
  3. 守护线程的应用: 守护线程通常用于执行后台任务,如垃圾回收、内存管理等。一些周期性的任务也可以作为守护线程来运行。

下面是一个简单的示例,演示了如何创建守护线程:

public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("Daemon Thread is running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        daemonThread.setDaemon(true); // Set as daemon thread
        daemonThread.start();

        // Sleep to allow the daemon thread to run for a while
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread is done.");
    }
}

在这个示例中,我们创建了一个守护线程 daemonThread,它在循环中执行任务代码。我们通过调用 setDaemon(true) 方法将线程设置为守护线程。然后我们启动守护线程,并让主线程休眠一段时间。由于 daemonThread 是守护线程,当主线程终止时,守护线程也会随之终止,不会等待它完成循环。

请注意,守护线程应该仅用于执行无关紧要的后台任务,因为它们可能会在任何时候终止,而不会等待任务完成。

3.3 线程名

线程名是指给线程分配的可读性较高的名称,以便在代码中更好地标识和区分不同的线程。线程名对于调试和日志记录非常有用,可以帮助您更好地理解程序中不同线程的行为。

以下是关于线程名的一些内容:

  • 设置线程名: 您可以使用 setName(String name) 方法来设置线程的名称。示例如下:

    Thread thread = new Thread(() -> {
        // Thread's task
    });
    thread.setName("MyThread");
    
  • 获取线程名: 您可以使用 getName() 方法来获取线程的名称。示例如下:

    Thread thread = new Thread(() -> {
        // Thread's task
    });
    String threadName = thread.getName();
    
  • 默认线程名: 如果您不显式地设置线程名,Java 会为线程分配一个默认的名称,如 "Thread-0"、"Thread-1" 等。

  • 显示线程名: 在日志输出、调试信息或其他需要标识线程的场景中,您可以使用线程名来显示线程的标识。

3.4 未捕获异常的处理器

在 Java 中,可以通过设置未捕获异常的处理器(Uncaught Exception Handler)来处理在线程执行期间未被捕获的异常。当线程抛出未捕获的异常时,如果没有显式地捕获这些异常,它们将传递给默认的未捕获异常处理器,也就是 Thread.UncaughtExceptionHandler 接口的实现。

以下是有关未捕获异常处理器的一些内容:

  1. 默认处理器: 如果您没有显式地设置未捕获异常处理器,Java 将使用默认的未捕获异常处理器来处理未捕获的异常。默认处理器会将异常的堆栈跟踪信息输出到标准错误流(System.err)。
  2. 设置处理器: 您可以通过实现 Thread.UncaughtExceptionHandler 接口,并将实现类设置为线程的未捕获异常处理器,来自定义处理未捕获的异常。要设置线程的未捕获异常处理器,可以使用 setUncaughtExceptionHandler() 方法。

下面是一个简单的示例,演示了如何设置未捕获异常的处理器:

public class UncaughtExceptionHandlerExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            throw new RuntimeException("Uncaught Exception Example");
        });

        // Set custom uncaught exception handler
        thread.setUncaughtExceptionHandler(new CustomUncaughtExceptionHandler());

        thread.start();
    }
}

class CustomUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.err.println("Uncaught Exception in thread: " + t.getName());
        System.err.println("Exception details: " + e.getMessage());
    }
}

在这个示例中,我们创建了一个线程 thread,并在线程的任务代码中抛出一个未捕获的异常。然后,我们使用 setUncaughtExceptionHandler() 方法将 CustomUncaughtExceptionHandler 设置为线程的未捕获异常处理器。当线程抛出未捕获的异常时,CustomUncaughtExceptionHandler 将被调用,输出异常的信息。

使用未捕获异常处理器可以帮助您在多线程环境中更好地处理异常情况,特别是在后台线程中可能不易被注意到的情况下。

3.5 线程优先级

线程优先级(Thread Priority)用于指定线程在竞争 CPU 时间片时被调度的优先级。线程优先级是一个整数值,范围从 Thread.MIN_PRIORITY(1)到 Thread.MAX_PRIORITY(10),其中默认优先级为 Thread.NORM_PRIORITY(5)。

以下是有关线程优先级的一些内容:

  1. 设置线程优先级: 您可以使用 setPriority(int priority) 方法来设置线程的优先级。请注意,线程的优先级并不是绝对的,操作系统和 JVM 调度器可能会影响线程的实际执行顺序。
  2. 获取线程优先级: 您可以使用 getPriority() 方法来获取线程的当前优先级。
  3. 竞争 CPU 时间片: 在多线程环境中,优先级较高的线程在竞争 CPU 时间片时有更高的概率被调度执行。但并不是绝对的,操作系统和 JVM 调度器可能会根据具体情况进行调整。
  4. 默认优先级: 新创建的线程继承创建它的父线程的优先级。主线程的默认优先级为 Thread.NORM_PRIORITY

在没有使用操作系统线程的java早期版本中,线程优先级可能很有用。不过现在不要使用线程优先级了

4. 同步

在多线程编程中,竞态条件(Race Condition)是指多个线程在访问和操作共享资源时,由于没有适当的同步机制导致的不确定性和错误的结果。竞态条件可能会导致程序产生意外的行为,包括数据不一致、程序崩溃和死锁等问题。

4.1 竞态条件的一个例子

让我们考虑一个简单的银行转账场景,其中两个线程并发地进行转账操作。如果没有适当的同步机制,就可能会导致竞态条件,从而出现错误的结果。

在以下的示例中,我们将使用两个账户对象和两个线程模拟转账操作。如果没有同步机制,可能会出现一种情况,其中两个线程都读取了相同的账户余额,然后进行了并发的转账操作,导致余额计算错误。

public class RaceConditionTransferExample {
    static class BankAccount {
        private int balance;

        public BankAccount(int initialBalance) {
            this.balance = initialBalance;
        }

        public int getBalance() {
            return balance;
        }

        public void deposit(int amount) {
            balance += amount;
        }

        public void withdraw(int amount) {
            balance -= amount;
        }
    }

    public static void main(String[] args) {
        BankAccount account1 = new BankAccount(1000);
        BankAccount account2 = new BankAccount(1000);

        Runnable transferTask = () -> {
            for (int i = 0; i < 10000; i++) {
                account1.withdraw(1);
                account2.deposit(1);
            }
        };

        Thread thread1 = new Thread(transferTask);
        Thread thread2 = new Thread(transferTask);

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

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

        System.out.println("Final balance of account1: " + account1.getBalance());
        System.out.println("Final balance of account2: " + account2.getBalance());
    }
}

在这个示例中,两个线程并发地进行转账操作,其中一个线程从 account1 中取款并向 account2 存款,另一个线程也在同时进行类似的操作。由于没有适当的同步机制,两个线程可能会读取相同的账户余额,导致余额计算错误。

4.2 竞态条件详解

竞态条件(Race Condition)是在多线程编程中常见的一个问题,它发生在多个线程并发地访问和修改共享资源时,由于缺乏适当的同步机制而导致的不确定性和错误的结果。竞态条件可能会导致程序产生意外的行为,如数据不一致、计算错误、死锁等问题。

竞态条件的发生通常需要满足以下条件:

  1. 共享资源: 多个线程同时访问和修改同一个共享资源,如变量、数据结构、文件等。
  2. 至少一个写操作: 至少有一个线程对共享资源进行了写操作,可能是修改、更新、删除等。
  3. 没有足够的同步: 没有足够的同步机制来保证多个线程之间的有序访问和修改,导致操作的顺序和时机不可预测。

以下是一个更详细的竞态条件示例:

public class RaceConditionExample {
    private static int sharedCounter = 0;

    public static void main(String[] args) {
        Runnable incrementTask = () -> {
            for (int i = 0; i < 1000000; i++) {
                sharedCounter++;
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

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

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

        System.out.println("Final sharedCounter value: " + sharedCounter);
    }
}

在这个示例中,两个线程并发地对共享变量 sharedCounter 进行递增操作。由于没有适当的同步机制,两个线程可能会在不同的时间片内交替执行,导致结果不确定。运行这段代码可能会得到不同的输出,因为两个线程可能会覆盖彼此的更新。

4.3 锁对象

并发编程是指多个线程在同一时间段内执行,可能会访问共享资源或共享数据。由于多线程的执行是并行的,因此可能会导致数据不一致、竞态条件等问题。为了解决这些问题,Java提供了各种机制,其中之一就是锁对象。锁对象是一种同步机制,用于控制多个线程对共享资源的访问。锁对象的核心思想是在任何给定的时间,只有一个线程可以持有锁并访问被保护的资源,其他线程必须等待,直到持有锁的线程释放锁。

  • synchronized关键字: 最简单的同步机制就是使用synchronized关键字。可以将方法或代码块标记为synchronized,以确保在任意时刻只有一个线程可以访问被保护的代码。例如:
public synchronized void synchronizedMethod() {
    // 同步代码块
}
  • ReentrantLock类: ReentrantLock是Java提供的显式锁实现,它允许更细粒度的控制,比synchronized更灵活。使用该锁需要手动获取和释放锁,以及处理异常情况。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

Lock lock = new ReentrantLock();

public void someMethod() {
    lock.lock();
    try {
        // 保护的代码块
    } finally {
        lock.unlock();
    }
}
  • 读写锁(ReadWriteLock): ReadWriteLock允许多个线程同时读取共享资源,但只允许一个线程进行写操作。这对于读多写少的场景可以提高并发性能。常见的实现是ReentrantReadWriteLock
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

public void readMethod() {
    readWriteLock.readLock().lock();
    try {
        // 读取操作
    } finally {
        readWriteLock.readLock().unlock();
    }
}

public void writeMethod() {
    readWriteLock.writeLock().lock();
    try {
        // 写入操作
    } finally {
        readWriteLock.writeLock().unlock();
    }
}

无论使用哪种锁对象,正确地管理锁的获取和释放是至关重要的,以避免死锁、饥饿等问题。选择锁对象类型取决于具体的需求和情况,以及对性能和灵活性的要求。

4.4 条件对象

在Java并发编程中,条件对象(Condition Object)是一种高级同步机制,用于允许线程在某些特定条件下等待或继续执行。条件对象通常与锁对象(如ReentrantLock)一起使用,以实现更精细的线程协调和通信。

条件对象的使用场景通常涉及等待特定条件满足的线程,并在条件满足时通知其他线程。这种机制可以用来解决生产者-消费者问题、线程间通信等情况。

以下是条件对象的基本用法示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean conditionMet = false;

    public void awaitCondition() throws InterruptedException {
        lock.lock();
        try {
            while (!conditionMet) {
                condition.await(); // 等待条件满足
            }
            // 条件满足后执行相应操作
        } finally {
            lock.unlock();
        }
    }

    public void signalCondition() {
        lock.lock();
        try {
            conditionMet = true;
            condition.signalAll(); // 通知等待线程条件已满足
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ConditionExample example = new ConditionExample();

        Thread waitingThread = new Thread(() -> {
            try {
                example.awaitCondition();
                System.out.println("Condition met, waiting thread resumes.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread signalingThread = new Thread(() -> {
            try {
                Thread.sleep(2000);
                example.signalCondition();
                System.out.println("Condition met, signaling thread.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        waitingThread.start();
        signalingThread.start();
    }
}

在这个示例中,一个线程(waitingThread)等待条件满足,而另一个线程(signalingThread)在一段时间后改变条件,并通知等待线程。await()方法会使等待线程进入等待状态,直到其他线程通过signalAll()方法通知条件满足。

需要注意的是,Condition对象需要和锁对象(Lock)配合使用,因此在使用条件对象时,通常会先获取锁,然后使用条件对象进行等待和通知操作。

条件对象提供了更灵活的线程等待和通知机制,可以避免一些传统的wait()notify()机制可能引发的问题,例如丢失通知或过早通知。

4.5 synchronized关键字

每个对象都有一个内部锁,并且这个锁有一个内部条件,这个锁会管理试图进入synchronized方法的线程,这个条件会管理调用了wait的线程。

将静态方法声明为同步也是合法的。如果调用这样一个方法,它会获得关联类对象的内部锁。例如,如果Bank类有一个静态同步方法,调用这个方法时,会锁定Bank.class对象的锁。因此,没有其他线程可以调用Bank类的这个方法和任何其他同步静态方法。

内部锁(Intrinsic Lock)是指通过 synchronized 关键字来实现的锁机制,它有一些限制和局限性,其中包括:

  1. 非阻塞性: 内部锁是一种悲观锁,如果一个线程获得了锁,其他线程在尝试获取锁时会被阻塞,无法立即得到反馈。这可能导致线程在高并发情况下等待时间增加,降低系统的并发性能。
  2. 非可中断性: 使用 synchronized 关键字获取锁的操作是不可中断的,即使线程在等待锁的过程中被中断,它也不能直接退出等待状态,只有在获取到锁之后才能响应中断。
  3. 单一等待集: 内部锁只能维护一个等待队列,这意味着在某些情况下,如果有多个条件需要等待,难以实现精细的线程唤醒和通信。
  4. 可重入性: 内部锁是可重入的,这意味着同一个线程可以多次获取同一个锁。虽然这是一个优点,但也可能导致复杂性增加,需要确保在适当的地方释放锁。

为了克服这些限制,Java 5 引入了显式锁机制,其中最常见的就是 ReentrantLock 类。ReentrantLock 提供了更高度的灵活性,它可以在一些特定情况下更好地满足多线程编程的需求,但也需要更多的代码来进行管理。此外,它还提供了一些扩展功能,如可中断锁获取、公平锁、条件对象等,可以更好地解决内部锁存在的一些问题。

条件对象(Condition)是 ReentrantLock 的一部分,用于在一些特定条件下等待和通知线程。与内部锁的等待和通知机制相比,条件对象提供了更灵活的线程协作和通信方式。通过使用条件对象,可以更精细地控制线程的等待和唤醒,从而避免了内部锁单一等待集的限制。

4.6 同步块

同步块(Synchronized Block)是一种使用synchronized关键字来实现的同步机制,用于控制多个线程对共享资源的并发访问。同步块允许多个线程在同一时间段内按顺序进入被同步的代码块,从而避免了竞态条件和数据不一致的问题。

同步块的基本语法如下:

synchronized (lockObject) {
    // 同步代码块
}

其中,lockObject 是一个对象,用于作为锁。同一时间内,只有一个线程可以获得 lockObject 的锁,并进入同步代码块执行,其他线程必须等待。

以下是一个使用同步块的示例:

public class SynchronizedBlockExample {
    private int count = 0;
    private Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }

    public static void main(String[] args) {
        SynchronizedBlockExample example = new SynchronizedBlockExample();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

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

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

        System.out.println("Count: " + example.getCount());
    }
}

在这个示例中,count 是一个共享资源,lock 对象作为同步锁。increment 方法和 getCount 方法都使用了同步块,以确保线程安全地对 count 进行读写操作。

需要注意的是,锁定的是 lock 对象,而不是被保护的资源本身。这样做是为了避免使用资源本身作为锁可能导致的混淆和错误。

当使用同步块时,选择合适的锁对象非常重要,因为锁对象的选择直接影响到并发程序的正确性和性能。以下是一些关于锁对象的注意事项:

  1. 选择合适的锁对象: 锁对象应该是被保护资源的逻辑关联对象,而不是资源本身。这有助于避免混淆和歧义。通常情况下,应该使用 private final 修饰的对象作为锁,以防止外部访问。
  2. 避免使用字符串和包装类作为锁: 使用字符串或包装类作为锁对象可能会导致性能问题,因为它们是常用的对象,可能在全局范围内被其他部分使用,增加了竞争的可能性。
  3. 避免锁定过大的范围: 同步块应该尽可能小,只锁定必要的代码区域。锁定过大的范围可能会导致性能下降,因为其他线程可能需要等待更长时间才能获取锁。
  4. 避免嵌套同步块: 在同一个线程中嵌套使用多个同步块,特别是嵌套使用相同的锁对象,可能会导致死锁。确保在合适的地方释放锁,避免出现死锁情况。
  5. 避免锁顺序死锁: 当多个线程以不同的顺序获取锁时,可能会导致锁顺序死锁。为了避免这种情况,应该在代码中保持一致的锁获取顺序。
  6. 使用公平锁: 如果有多个线程等待获取锁,并且希望按照等待的先后顺序获取锁,可以考虑使用公平锁,例如 ReentrantLock 的构造函数中设置为 true
  7. 考虑并发性和性能: 锁的粒度和并发性能之间存在权衡。锁定过多的代码区域可能会减少并发性能,但锁的粒度太小可能会导致频繁的锁竞争和上下文切换。根据具体情况,进行适当的权衡。
  8. 使用并发工具: 在一些情况下,可以使用 Java 提供的并发工具,如 ConcurrentHashMapCountDownLatchCyclicBarrier 等,来避免显式的同步块。

4.7 volatile 字段

volatile 是一个关键字,用于修饰字段(成员变量),以确保多个线程对该字段的读写操作在一定程度上是可见的和有序的。使用 volatile 关键字修饰的字段具有以下特性:

  1. 可见性: 当一个线程修改了一个被 volatile 修饰的字段的值,该变化会立即被其他线程看到。这是因为 volatile 字段会引导编译器和处理器生成相应的指令,确保变化对其他线程是可见的。
  2. 禁止重排序: volatile 关键字还会防止编译器和处理器对 volatile 字段相关的读写操作进行重排序。这样可以确保 volatile 字段的写操作不会在写操作之前被重排序到写操作之后。

虽然 volatile 提供了一定程度的可见性和有序性,但它并不适用于所有并发场景。volatile 适用于以下情况:

  • 状态标志: 当一个字段用于标志某个状态(例如线程的终止标志),并且多个线程需要读取该状态,使用 volatile 可以确保及时看到状态的变化。
  • 单次初始化: 当一个字段只会被写入一次,用于确保该字段在多线程环境下正确初始化。使用 volatile 可以确保初始化值对其他线程是可见的。

然而,volatile 并不能替代所有的锁机制,因为它不能提供原子性。例如,如果一个字段的读写操作涉及到多个步骤,使用 volatile 可能无法保证原子性,因此在这种情况下,需要使用其他的同步机制,如 synchronizedjava.util.concurrent 包中的原子操作类。

4.8 final变量

在Java并发编程中,使用 final 关键字来修饰字段(成员变量)具有特殊的含义。final 字段在多线程环境下具有以下特性:

  1. 不可修改性: 一旦一个字段被声明为 final,它的值就不能再被修改。这意味着字段的值在初始化后就不能再被改变,从而确保了不会发生并发线程之间的修改冲突。
  2. 可见性: 使用 final 关键字修饰的字段具有良好的可见性。当一个线程初始化了一个 final 字段的值,其他线程可以立即看到这个值,而不需要特殊的同步机制。
  3. 内存屏障效果: 在初始化 final 字段时,会引入一些内存屏障效果,这有助于确保该字段的初始化操作对其他线程的可见性。

虽然 final 字段在多线程环境下具有可见性和不可修改性,但需要注意以下几点:

  • final 字段的不可修改性只适用于字段本身,如果字段引用的是可变对象,该对象的内部状态仍然可以被修改。为了确保整体的不可修改性,需要在设计时考虑引用对象的不可变性。
  • 在使用 final 字段时,需要确保在构造函数中对其进行初始化,以避免可能的空指针异常和错误。
  • 使用 final 字段通常适用于常量值、不可变的配置信息等。如果字段需要多次修改,final 可能并不适用。

总之,final 字段可以在多线程环境下提供一定程度的可见性和不可修改性,但它并不适用于所有的并发场景。在设计并发程序时,需要根据实际需求选择适当的同步和并发控制机制。

4.9 原子性

在Java并发编程中,原子性是指一个操作或一系列操作是不可分割的,要么全部执行成功,要么全部不执行,不存在中间状态。原子性操作可以确保数据在多线程环境下保持一致性,避免了并发竞态条件和数据不一致的问题。

原子性操作的实现通常涉及到以下两个概念:

  1. 原子性操作(Atomic Operation): 单个操作是原子的,它可以以不可分割的方式在多线程环境下执行。例如,基本数据类型的读写操作通常是原子的,比如 intlong 类型的赋值操作。
  2. 原子性操作类(Atomic Operations Classes): Java提供了一些特殊的类,如 java.util.concurrent.atomic 包中的原子操作类,用于实现复合操作的原子性。这些类提供了一些原子性操作方法,如 getAndIncrement()compareAndSet() 等,可以用于保证多个操作的原子性。

以下是一些原子性操作的示例:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // 原子性递增操作
    }

    public int getCount() {
        return count.get();
    }

    public static void main(String[] args) {
        AtomicExample example = new AtomicExample();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

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

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

        System.out.println("Count: " + example.getCount());
    }
}

在这个示例中,使用了 AtomicInteger 类来确保 increment() 方法的原子性。AtomicInteger 提供了 incrementAndGet() 方法,该方法是原子的,它可以确保递增操作在多线程环境下的原子性。

通过使用原子性操作类,可以在不使用显式锁的情况下实现一些常见的原子性需求,从而提高并发性能。但要注意,并非所有的操作都可以使用原子性操作类来解决,并发编程时还需要综合考虑数据的一致性和线程安全性。

4.10 死锁

死锁是一种多线程并发问题,当两个或多个线程相互等待对方释放资源而无法继续执行时,就会发生死锁。这种情况下,每个线程都在等待其他线程释放资源,导致所有线程都无法继续执行,从而形成了死锁状态。

死锁通常发生在多个线程同时竞争有限的资源,并按照不同的顺序获取锁。当多个线程按不同的顺序获取锁时,可能会出现环形等待,从而导致死锁的发生。

以下是一个简单的死锁示例:

public class DeadlockExample {
    public static void main(String[] args) {
        Object resource1 = new Object();
        Object resource2 = new Object();

        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1: Waiting for resource 2...");
                synchronized (resource2) {
                    System.out.println("Thread 1: Acquired resource 2.");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 2: Waiting for resource 1...");
                synchronized (resource1) {
                    System.out.println("Thread 2: Acquired resource 1.");
                }
            }
        });

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

在这个示例中,thread1thread2 各自持有一个资源,然后尝试获取另一个资源。由于两个线程的获取顺序不同,可能会出现相互等待的情况,导致死锁。

要避免死锁,可以考虑以下几种策略:

  1. 按顺序获取锁: 尽量以相同的顺序获取锁,这可以避免循环等待导致的死锁。
  2. 使用超时机制: 在尝试获取锁时设置超时时间,如果无法在规定时间内获取锁,则放弃当前操作。
  3. 使用 tryLock(): 在 Java 中,可以使用 tryLock() 方法尝试获取锁,如果获取失败,可以释放已经获取的锁,避免死锁。
  4. 使用线程池和资源分配策略: 使用线程池来控制资源的并发使用,确保资源分配策略合理。
  5. 使用工具进行死锁检测: 可以使用工具来检测死锁情况,如 JConsole、VisualVM 等。

死锁是并发编程中常见的问题,但通过合理的设计和策略,可以有效地预防和解决死锁问题。

4.11 为什么废弃stop和suspend方法

Java中的stop()suspend()方法已经被废弃,原因主要是因为它们可能引发严重的线程安全和程序稳定性问题,容易导致死锁、数据损坏和不一致等问题。下面是具体的原因:

  1. stop()方法: stop()方法用于立即终止线程的执行。然而,该方法会在终止线程时,不考虑线程所持有的锁,可能导致线程在释放锁之前被终止,从而引发数据损坏和不一致的问题。这种突然终止的行为可能破坏程序状态和资源。
  2. suspend()方法: suspend()方法用于挂起线程的执行。然而,线程在被挂起的状态下可能持有锁,导致其他线程无法获取这些锁,从而引发死锁问题。此外,当一个线程被挂起时,如果其他线程试图恢复它,可能导致线程在不可预测的状态下继续执行,导致程序逻辑错误。

由于这些方法存在严重的线程安全和稳定性问题,Java官方决定将它们标记为废弃。相反,官方推荐使用更安全和可控的方式来管理线程的生命周期和状态,例如:

  • 使用标志位:可以使用volatile修饰的标志位来控制线程的终止状态,从而安全地终止线程。
  • 使用合适的线程状态:Java提供了Thread.State枚举,可以使用线程的状态来控制其生命周期。
  • 使用interrupt()方法:通过调用interrupt()方法,可以中断线程的正常执行,从而安全地终止线程。
  • 使用join()方法:通过join()方法等待线程执行完成,以确保线程的正常结束。

总的来说,废弃stop()suspend()方法是出于对线程安全和程序稳定性的考虑。推荐使用更安全的替代方法来管理线程的生命周期和状态,以避免潜在的问题。

4.12 按需初始化

按需初始化(Lazy Initialization)是一种在需要时才进行对象或资源初始化的策略,可以在并发编程中使用,以减少不必要的开销和提高性能。这种策略可以确保资源在第一次使用时才被创建,而不是在对象创建的时候就立即初始化。

在并发环境中,按需初始化需要考虑线程安全性,以避免多个线程同时尝试初始化资源,导致重复创建或竞争条件。以下是几种实现按需初始化的方法:

  • 懒汉式单例模式: 在需要时才创建单例实例。可以使用双重检查锁定等机制来确保只有一个线程进行初始化。示例如下:
public class LazySingleton {
    private static volatile LazySingleton instance;

    private LazySingleton() {
        // 私有构造函数
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}
  1. 使用java.util.concurrent中的原子操作类: 可以使用java.util.concurrent.atomic包中的原子操作类,如AtomicReference来实现延迟初始化。这些类提供了一些原子性的操作,可以用于延迟初始化的场景。
  2. 使用java.util.concurrent中的ConcurrentHashMap 可以使用ConcurrentHashMapcomputeIfAbsent方法来实现延迟初始化。这个方法会确保在并发情况下只有一个线程会执行初始化操作。
  3. 使用ThreadLocal 可以使用ThreadLocal来实现线程级别的按需初始化。每个线程都有自己的变量副本,可以延迟初始化,不影响其他线程。

在实现按需初始化时,需要考虑线程安全性和性能。不同的场景可能适用不同的方法,根据实际需求来选择最合适的策略。总的来说,按需初始化可以提高资源利用率,减少不必要的开销,但需要注意线程安全性问题。

4.13 线程局部变量

线程局部变量(Thread-Local Variable)是一种在多线程环境下,每个线程都拥有自己独立的变量副本的机制。每个线程可以独立地访问和修改自己的线程局部变量,而不会影响其他线程的副本。这在一些并发编程场景中非常有用,例如需要在多线程中保存状态信息、线程上下文等情况。

Java提供了ThreadLocal类来实现线程局部变量。ThreadLocal可以将一个对象与当前线程关联起来,使得每个线程都可以访问到自己的对象副本。以下是使用ThreadLocal的示例:

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Runnable task = () -> {
            int value = threadLocal.get();
            System.out.println(Thread.currentThread().getName() + " - Initial value: " + value);
            threadLocal.set(value + 1);
            value = threadLocal.get();
            System.out.println(Thread.currentThread().getName() + " - Updated value: " + value);
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

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

在这个示例中,ThreadLocal<Integer>创建了一个整数类型的线程局部变量。通过get()方法获取变量的值,通过set()方法设置变量的值。每个线程的访问都是相互隔离的,互不干扰。

需要注意的是,使用ThreadLocal时需要注意以下几点:

  1. 初始值: 可以通过withInitial()方法设置线程局部变量的初始值。每个线程在首次访问该变量时,会使用这个初始值初始化自己的副本。
  2. 内存泄漏: 线程局部变量的值仅在当前线程有效,但如果没有适时地清理,可能会导致内存泄漏。可以通过remove()方法手动移除线程局部变量的值。
  3. 共享变量: 线程局部变量适用于多个线程需要独立副本的场景,不适用于需要共享变量的场景。
  4. 性能影响: 虽然线程局部变量提供了隔离性,但在一些情况下,使用ThreadLocal可能会引入一些性能开销。

总的来说,线程局部变量是一种有用的机制,可以用于在多线程环境下实现数据隔离,提高线程安全性。但在使用时需要谨慎处理,以避免潜在的问题。

5. 线程安全的集合

线程安全的集合是为了在多线程环境下安全地使用集合数据结构而设计的。多线程环境下,多个线程可能同时访问和修改同一个集合,如果不进行适当的同步处理,就可能会导致数据不一致、并发问题或错误。

以下是 Java 中常见的线程安全的集合类:

  1. ConcurrentHashMap: 这是一个线程安全的哈希表实现,适用于并发读写操作。它采用分段锁的方式来实现高效的并发操作。
  2. CopyOnWriteArrayList 和 CopyOnWriteArraySet: 这些集合类在每次修改操作时会创建一个新的副本,因此读操作和写操作可以同时进行,不会产生并发问题。
  3. ConcurrentLinkedQueue 和 ConcurrentLinkedDeque: 这些队列实现了高效的无锁算法,适用于高并发的队列操作。
  4. BlockingQueue: 这是一个接口,定义了阻塞队列的操作,常见的实现包括 LinkedBlockingQueue 和 ArrayBlockingQueue,用于在多线程环境下实现生产者-消费者模式。
  5. ConcurrentSkipListMap 和 ConcurrentSkipListSet: 这些集合实现了基于跳跃表的数据结构,支持高并发操作,适用于有序集合的场景。

需要注意的是,虽然这些集合类提供了线程安全的操作,但在使用时仍然需要注意正确的使用方式,以避免死锁、竞态条件和性能问题。此外,选择适当的线程安全集合也要根据具体的应用场景来进行,不是所有情况下都需要使用线程安全的集合,因为线程安全会带来一些额外的开销。

5.1 阻塞队列

阻塞队列是一种特殊类型的线程安全集合,用于实现在多线程环境下的生产者-消费者模式。阻塞队列提供了一种安全的方式来进行数据交换,确保在队列为空或已满时线程的阻塞和唤醒,从而有效地协调不同线程之间的操作。

以下是 Java 中常见的阻塞队列类:

  1. LinkedBlockingQueue: 这是一个基于链表的阻塞队列实现,可以指定队列的容量。生产者在队列满时会被阻塞,消费者在队列空时会被阻塞。
  2. ArrayBlockingQueue: 这是一个基于数组的阻塞队列实现,也需要指定队列的容量。与 LinkedBlockingQueue 类似,生产者和消费者在队列满或队列空时会被阻塞。
  3. PriorityBlockingQueue: 这是一个基于优先级的阻塞队列,不同于其他阻塞队列,它没有固定的容量限制。
  4. DelayQueue: 这是一个支持延迟操作的阻塞队列,其中的元素只有在指定的延迟时间过去后才能被取出。
  5. SynchronousQueue: 这是一个特殊的阻塞队列,每次只能包含一个元素。当一个线程尝试将元素放入队列时,必须等待另一个线程将该元素取出。

阻塞队列的主要特点是当队列满或队列空时,对于生产者和消费者的操作会被阻塞,直到队列中有足够的空间或者有元素可取。这种机制能够有效地协调不同线程之间的操作,避免了一些典型的多线程并发问题,如死锁和饥饿。

在使用阻塞队列时,你需要根据具体的需求和场景来选择合适的队列类型。阻塞队列通常在需要实现生产者-消费者模式、任务调度等多线程场景中非常有用。

阻塞队列方法

  1. put(E element): 将指定的元素添加到队列的末尾,如果队列已满则阻塞等待。
  2. offer(E element): 将指定的元素添加到队列的末尾,如果队列已满则立即返回 false。
  3. offer(E element, long timeout, TimeUnit unit): 将指定的元素添加到队列的末尾,在指定的时间范围内等待队列有空闲空间。
  4. take(): 移除并返回队列头部的元素,如果队列为空则阻塞等待。
  5. poll(): 移除并返回队列头部的元素,如果队列为空则立即返回 null。
  6. poll(long timeout, TimeUnit unit): 移除并返回队列头部的元素,在指定的时间范围内等待队列有元素。
  7. peek(): 返回队列头部的元素,但不移除它。
  8. size(): 返回队列中当前的元素数量。
  9. isEmpty(): 判断队列是否为空。
  10. remainingCapacity(): 返回队列中剩余的容量,如果是无限容量的队列则返回 Integer.MAX_VALUE。
  11. clear(): 清空队列中的所有元素。

这些方法是基本的阻塞队列操作,用于在生产者和消费者之间进行数据的添加和移除,同时保证线程安全。需要注意的是,阻塞队列中的方法可以自动进行线程的阻塞和唤醒,从而避免了手动的同步操作。

程序示例

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Stream;

public class BlockingQueueTest {
    private static final int FILE_QUEUE_SIZE = 10;
    public static final int SEARCH_THREADS = 100;
    private static final Path DUMMY = Path.of("");
    private static BlockingQueue<Path> queue = new ArrayBlockingQueue<>(FILE_QUEUE_SIZE);

    public static void main(String[] args) {
        try (var in = new Scanner((System.in))) {
            System.out.println("请输入基础目录(e.g. /opt/jdk-11-src): ");
            String directory = in.nextLine();
            System.out.println("输入关键字(e.g. xzy): ");
            String keyword = in.nextLine();
            Runnable enumerator = () -> {
                try {
                    enumerate(Path.of(directory));
                    queue.put(DUMMY);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (InterruptedException e){
                }
            };
            new Thread(enumerator).start();

            for (int i = 0; i <= SEARCH_THREADS; i++) {
                Runnable searcher = ()-> {
                    try {
                        boolean done = false;
                        while (!done){
                            Path file = queue.take();
                            if (file == DUMMY){
                                queue.put(file);
                                done = true;
                            }else {
                                search(file,keyword);
                            }
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                };
                new Thread(searcher).start();
            }
        }
    }

    private static void search(Path file, String keyword) throws IOException {
        try (var in = new Scanner(file, StandardCharsets.UTF_8)) {
            int lineNumber = 0;
            while (in.hasNextLine()){
                lineNumber++;
                String line = in.nextLine();
                if (line.contains(keyword)) System.out.printf("%s:::%d:::%s%n",file,lineNumber,line);
            }
        }
    }

    private static void enumerate(Path directory) throws IOException, InterruptedException {
        try (Stream<Path> children = Files.list(directory)) {
            for (Path child : children.toList()){
                if (Files.isDirectory(child)){
                    enumerate(child);
                }else{
                    queue.put(child);
                }
            }
        }
    }
}

5.2 高效的映射、集和队列

有许多用于映射、集合和队列的高效实现。这些实现提供了不同的性能特点和适用场景,可以根据你的需求来选择合适的数据结构。

以下是一些常见的高效映射、集合和队列的实现:

高效映射(Map)实现:

  1. HashMap:基于哈希表的映射实现,提供 O(1) 的常量时间复杂度用于插入、获取和删除操作。但是迭代顺序是不确定的。
  2. LinkedHashMap:在 HashMap 基础上添加了链表,可以保持元素的插入顺序,迭代时按照插入顺序返回元素。
  3. TreeMap:基于红黑树的映射实现,元素按照键的自然顺序或者指定的比较器进行排序。
  4. ConcurrentHashMap:线程安全的哈希表映射,通过分段锁来提高并发性能。

高效集合(Set)实现:

  1. HashSet:基于哈希表的集合实现,具有 O(1) 的插入、删除和查找操作的性能。
  2. LinkedHashSet:在 HashSet 基础上保持元素的插入顺序,迭代时按照插入顺序返回元素。
  3. TreeSet:基于红黑树的集合实现,元素按照自然顺序或者指定的比较器进行排序。
  4. EnumSet:专门用于枚举类型的高效集合实现,基于位向量。

高效队列(Queue)实现:

  1. ArrayDeque:基于数组的双端队列实现,支持快速插入和删除操作。
  2. LinkedList:基于链表的队列实现,支持快速插入和删除操作,同时支持栈操作。
  3. PriorityQueue:基于堆的优先队列实现,可以根据元素的优先级进行调度。
  4. ConcurrentLinkedQueue:线程安全的非阻塞队列,使用无锁算法实现。

以上只是一些常见的高效实现,根据实际需求和使用场景,你可以选择合适的数据结构来满足性能和功能要求。

5.3 映射条目的原子更新

在多线程环境下,对映射条目(Map entries)进行原子更新是一种常见的需求,以确保并发访问时的数据一致性和正确性。Java 提供了一些机制来实现映射条目的原子更新,其中最常用的是 java.util.concurrent.ConcurrentHashMap 提供的方法。

ConcurrentHashMap 中,你可以使用一些特定的方法来进行映射条目的原子更新,如下所示:

  1. putIfAbsent(key, value):如果指定的键不存在,则将键值对放入映射中。这个操作是原子的,可以用于避免重复添加相同的键值对。
  2. replace(key, oldValue, newValue):如果指定键的值与旧值匹配,则将值替换为新值。这个操作是原子的,可以用于实现条件更新。
  3. compute(key, remappingFunction):原子地将指定键的值与指定的 remapping 函数进行计算,然后将计算结果放入映射中。
  4. merge(key, value, remappingFunction):原子地将指定键的值与给定的 value 和 remapping 函数进行合并,然后将结果放入映射中。

示例:

import java.util.concurrent.ConcurrentHashMap;

public class AtomicMapUpdateExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        map.put("key1", 10);

        // Atomic putIfAbsent
        map.putIfAbsent("key1", 20); // No change, "key1" already exists

        // Atomic replace
        map.replace("key1", 10, 30); // Value of "key1" is replaced with 30

        // Atomic compute
        map.compute("key1", (key, oldValue) -> oldValue + 5); // Value of "key1" becomes 35

        // Atomic merge
        map.merge("key2", 10, (oldValue, newValue) -> oldValue + newValue); // Creates new entry with value 10

        System.out.println(map); // Output: {key1=35, key2=10}
    }
}

5.4 并发散列映射的批操作

并发散列映射(ConcurrentHashMap)在 Java 中是一种特殊的映射数据结构,它支持高并发环境下的操作,同时提供了一些批量操作的方法,以提高效率。下面是一些并发散列映射的批操作方法:

  1. forEach 方法:forEach 方法允许你对映射中的每个键值对执行一个操作。在并发散列映射中,这个操作可以在并发环境下安全地执行。

    map.forEach((key, value) -> {
        // 执行操作
    });
    
  2. replaceAll 方法:replaceAll 方法允许你对映射中的每个键值对执行一个替换操作,可以用来更新映射中的值。

    map.replaceAll((key, value) -> {
        // 执行替换操作
        return newValue;
    });
    
  3. compute 方法:compute 方法允许你为指定的键执行一个计算操作,如果计算结果不为 null,会将结果更新到映射中。

    map.compute(key, (k, v) -> {
        // 执行计算操作,返回新值或 null
        return newValue;
    });
    
  4. merge 方法:merge 方法允许你将指定的键和值合并到映射中,如果键不存在或对应的值为 null,会直接插入新值。

    map.merge(key, newValue, (existingValue, newValue) -> {
        // 执行合并操作,返回新值
        return mergedValue;
    });
    

5.5 并发集视图

假设你想要的是一个很大的线程安全的集还不是映射。并没有ConcurrentHashSet类,而且你肯定不想自己创建这样一个类,当然,可以使用包含"假"值的ConcurrentHashMap,不过这会得到一个映射还不是集,而且不能应用Set接口的操作

在 Java 并发集合框架中,并没有提供 ConcurrentHashSet 这样的直接类。然而,你可以通过以下方式创建一个类似于 ConcurrentHashSet 的线程安全集合:

  • 使用 ConcurrentHashMap: 正如你提到的,可以使用 ConcurrentHashMap,并将集合的元素作为键,假的 Boolean 值(或者其他适当的假值)作为值。这样可以创建一个类似于集合的结构。但是,你会注意到这并不是一个集合,也不能应用集合接口的操作。
  • 使用 Collections.newSetFromMap: Java 提供了一个方法 Collections.newSetFromMap,它允许你从一个现有的映射中创建一个线程安全的集合视图。你可以创建一个 ConcurrentHashMap,然后使用 Collections.newSetFromMap 方法将其转换为线程安全的集合视图。但需要注意的是,这个集合视图是基于映射的,因此不支持集合接口中的某些操作。

下面是使用 Collections.newSetFromMap 的示例:

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashSetDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Boolean> map = new ConcurrentHashMap<>();
        Set<String> concurrentSet = Collections.newSetFromMap(map);

        // 添加元素到集合
        concurrentSet.add("item1");
        concurrentSet.add("item2");

        // 使用集合的操作
        boolean containsItem = concurrentSet.contains("item1");
        System.out.println("Contains item: " + containsItem);
    }
}
  • ConcurrentHashMap.newKeySet() 是 Java 8 中新增的一个方法,它返回一个支持并发操作的线程安全的集合视图,该集合视图是基于 ConcurrentHashMap 的键集合的。这个方法的返回值是一个 ConcurrentHashMap.KeySetView 实例,它实现了 Set 接口,允许你在多线程环境中以线程安全的方式访问键的集合。

使用 ConcurrentHashMap.newKeySet() 可以很方便地创建一个线程安全的键集合,但需要注意的是,这个集合视图是基于键的,不支持集合接口中的所有操作,例如,它不支持添加重复元素。这个方法适用于在多线程环境中对键集合进行并发操作。

下面是使用 ConcurrentHashMap.newKeySet() 的示例:

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapKeySetDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        Set<String> keySet = map.newKeySet();

        // 添加键到集合
        keySet.add("key1");
        keySet.add("key2");

        // 使用集合的操作
        boolean containsKey = keySet.contains("key1");
        System.out.println("Contains key: " + containsKey);
    }
}

这样的集合视图是适合在多线程环境中对键集合进行读取和部分操作,但如果需要对集合进行更复杂的操作,可能需要考虑其他更全面的线程安全集合实现。

ConcurrentHashMap.newKeySet()Collections.newSetFromMap 都用于在多线程环境下创建线程安全的集合视图,但它们有一些区别:

  1. 返回类型:
    • ConcurrentHashMap.newKeySet(): 这个方法返回一个 ConcurrentHashMap.KeySetView 实例,它是 ConcurrentHashMap 的键集合的视图,实现了 Set 接口。
    • Collections.newSetFromMap: 这个方法返回一个普通的 Set 集合,这个集合是基于传入的映射(Map)的。你可以传递任何实现了 Map 接口的映射对象给它,不限于 ConcurrentHashMap
  2. 支持的操作:
    • ConcurrentHashMap.newKeySet(): 返回的 KeySetView 实例可以通过原始的 ConcurrentHashMap 执行一些高级的并发操作,如 forEachreplaceAllremoveIf 等。
    • Collections.newSetFromMap: 返回的普通 Set 集合是不支持并发操作的,它仅提供基本的集合操作。
  3. 适用范围:
    • ConcurrentHashMap.newKeySet(): 适用于需要在多线程环境下对 ConcurrentHashMap 的键集合进行并发操作的场景。
    • Collections.newSetFromMap: 适用于需要将任何实现了 Map 接口的映射转换成线程安全的集合视图的场景。

总的来说,如果你已经使用了 ConcurrentHashMap,并且需要在多线程环境下对其键集合进行并发操作,那么你可以使用 ConcurrentHashMap.newKeySet() 来创建线程安全的键集合视图。如果你需要将任何映射对象转换为线程安全的集合视图,可以使用 Collections.newSetFromMap

5.6 写时拷贝数组

写时拷贝数组(Copy-on-Write Array)是一种并发数据结构,主要用于读多写少的场景,特别是在读取操作频繁的情况下,可以提供高效的并发性能。

写时拷贝数组的基本思想是,当需要修改数据时,不直接在原始数据上进行修改,而是先复制一份数据(拷贝),然后在拷贝上进行修改。这样可以确保读取操作不受修改操作的影响,从而提供了高并发性能。写操作的开销会比较高,因为需要进行数据复制,但在读多写少的场景下,读取性能会得到极大的提升。

在 Java 中,并没有标准的内置写时拷贝数组,但你可以通过使用 java.util.concurrent.CopyOnWriteArrayList 类来实现类似的功能。CopyOnWriteArrayList 是一个线程安全的列表,它使用写时拷贝策略来保证读取操作的线程安全性。

5.7 并行数组算法

并行数组算法是一种并行计算的策略,其中数据被划分成多个部分,每个部分都在不同的处理单元上并行处理。这种方法在处理数组类型的数据时特别有效,因为数组的元素通常具有相似的结构,可以并行地进行计算和处理。并行数组算法可以在多核处理器、并行计算集群和分布式系统等环境中得到应用。

以下是一些常见的并行数组算法示例:

  1. 并行遍历:将数组分割成多个子数组,每个子数组由一个处理单元负责遍历。这样可以同时遍历多个子数组,从而提高遍历效率。
  2. 并行归约:将数组划分成多个子数组,每个子数组进行归约操作(如求和、求平均值等),然后将子数组的归约结果合并成最终的结果。
  3. 并行排序:通过将数组划分成多个子数组,每个子数组进行独立的排序,然后合并子数组的有序部分来实现并行排序。
  4. 并行搜索:将数组分割成多个子数组,每个子数组进行独立的搜索操作,然后将子数组的搜索结果合并,找到所需的元素。
  5. 并行映射:对数组中的每个元素应用一个映射函数,将其转换为另一个值,各个元素的映射操作可以并行执行。
  6. 并行过滤:根据某个条件过滤数组中的元素,将满足条件的元素保留下来,过滤操作可以在多个处理单元上并行执行。

java.util.Arrays 类提供了多个并行数组算法来利用多核处理器的并行计算能力来提高数组操作的性能。除了 parallelSortparallelSetAll,还有一个重要的方法是 parallelPrefix

  1. Arrays.parallelSortparallelSort 方法用于并行地对数组进行排序。它会将数组分割成多个子数组,然后并行地对这些子数组进行排序,最后将子数组合并成一个有序的数组。这可以显著提高排序的性能,特别是对于大型数组。

    示例:

    int[] arr = {5, 2, 9, 1, 5, 6};
    Arrays.parallelSort(arr);
    
  2. Arrays.parallelSetAllparallelSetAll 方法用于并行地设置数组的元素值。它接受一个 lambda 表达式作为参数,根据索引位置生成数组元素的值,并可以在多个处理单元上并行执行。

    示例:

    int[] arr = new int[10];
    Arrays.parallelSetAll(arr, i -> i * 2);
    
  3. Arrays.parallelPrefixparallelPrefix 方法用于并行地计算数组的前缀和。它会将数组分割成多个子数组,然后在每个子数组上并行地计算前缀和,最终将前缀和合并成一个数组。

    示例:

    int[] arr = {1, 2, 3, 4, 5, 6};
    Arrays.parallelPrefix(arr, (x, y) -> x + y);
    

5.8 较早的线程安全集合

在 Java 的早期版本中,并发编程和线程安全的集合支持是相对较弱的。然而,Java 5(也称为Java 1.5)引入了 java.util.concurrent 包,其中提供了一些用于并发编程的新的线程安全集合类。在此之前,开发者通常需要使用传统的同步机制(如 synchronized 关键字)来手动管理线程安全性。

以下是一些较早的线程安全集合类以及在 Java 5 之前的一些常见做法:

  1. VectorVector 是 Java 早期提供的线程安全动态数组,它提供了对所有操作进行同步的机制。但由于所有操作都是同步的,可能会导致性能问题,尤其是在高并发情况下。
  2. HashtableHashtable 是 Java 早期提供的线程安全哈希表,类似于 HashMap,但对所有操作进行同步。同样地,全局同步可能导致性能问题。
  3. 同步容器类: 在 Java 5 之前,开发者可以使用同步块和 synchronized 关键字来手动保证集合操作的线程安全性。例如,使用 Collections.synchronizedList(List<T> list) 可以将普通的 List 转换为线程安全的。
  4. 手动同步: 在较早的版本中,开发者也可以使用自己实现的同步机制来保证集合操作的线程安全性,例如使用 synchronized 关键字或者 Object 的监视器方法。

然而,这些较早的方式通常会带来性能问题和复杂性,因为全局同步可能导致竞争和阻塞,影响并发性能。从 Java 5 开始,java.util.concurrent 包引入了更先进的并发集合类,如 ConcurrentHashMapCopyOnWriteArrayList 等,它们提供更细粒度的同步,以及更好的并发性能,适用于读多写少的场景。

6. 任务和线程池

任务和线程池是多线程编程中的重要概念,用于有效地管理和执行并发任务。让我们逐个解释这两个概念。

任务(Task): 任务是执行特定工作的单元。在多线程编程中,任务可以是一个要在后台执行的代码块,它可以完成特定的计算、处理、IO操作等。任务通常实现了Runnable接口(或者通过Callable接口),它们封装了要执行的操作,并且可以由线程来执行。通过任务,可以将需要并行处理的工作分解成可管理的小块,从而提高程序的并发性能。

在Java中,任务可以通过创建Runnable接口的实例来定义,然后将其传递给一个线程来执行。例如:

public class MyTask implements Runnable {
    @Override
    public void run() {
        // 执行任务的代码
    }
}

// 创建任务实例
Runnable task = new MyTask();

// 创建线程并执行任务
Thread thread = new Thread(task);
thread.start();

线程池(Thread Pool): 线程池是一种用于管理和复用线程的机制。它避免了频繁地创建和销毁线程,从而减少了线程创建的开销。线程池维护了一组预先创建的线程,这些线程可以被用来执行提交给池中的任务。当任务完成时,线程不会被销毁,而是会被重新放入线程池中,以便在未来执行其他任务。

Java提供了Executor框架来管理线程池。通过Executor,你可以创建不同类型的线程池,如ThreadPoolExecutor,以根据需要配置线程池的大小、任务队列等参数。

使用线程池的优点包括:

  1. 降低资源消耗: 避免了频繁创建和销毁线程的开销。
  2. 提高响应性: 可以立即执行任务,而不必等待线程的创建。
  3. 控制并发度: 可以限制同时执行的线程数量,避免资源过度占用。
  4. 任务队列: 允许将任务排队,等待线程池中的线程执行。

下面是一个简单的使用线程池的示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        
        // 提交任务给线程池
        for (int i = 0; i < 10; i++) {
            threadPool.execute(new MyTask()); // MyTask 是实现了 Runnable 接口的任务
        }
        
        // 关闭线程池
        threadPool.shutdown();
    }
}

在这个示例中,我们创建了一个固定大小为3的线程池,然后提交了10个任务给线程池执行。执行完成后,我们关闭了线程池。这样,线程池会管理这些任务的执行,避免了频繁地创建和销毁线程。

6.1 Callable和Future

CallableFuture是与Runnable和线程池紧密相关的概念,用于执行有返回值的任务并获取任务执行结果。让我们逐个解释这两个概念。

Callable: Callable是一个接口,类似于Runnable,但它可以返回一个值或者抛出一个异常。Callable定义了一个单一方法call(),该方法包含了任务的主要逻辑,当任务被执行时,call()方法会被调用。

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 执行任务的代码,并返回一个结果
        return 42;
    }
}

Future: Future是用于获取异步任务执行结果的类。当你将一个Callable任务提交给线程池执行时,线程池会返回一个Future对象,你可以使用这个对象来查询任务的执行状态和获取任务的返回值。Future提供了一系列方法,例如get()方法,可以等待任务完成并获取返回结果。

import java.util.concurrent.*;

public class CallableFutureExample {
    public static void main(String[] args) throws Exception {
        ExecutorService threadPool = Executors.newFixedThreadPool(1);
        
        Callable<Integer> callableTask = new MyCallable();
        
        Future<Integer> future = threadPool.submit(callableTask);
        
        // 等待任务完成并获取结果
        Integer result = future.get();
        System.out.println("Result: " + result);
        
        threadPool.shutdown();
    }
}

在上面的示例中,我们创建了一个Callable任务,并将其提交给线程池执行。线程池返回一个Future对象,我们使用get()方法来等待任务完成并获取返回结果。需要注意的是,get()方法是阻塞的,如果任务还没有完成,调用get()将会阻塞当前线程,直到任务完成为止。

总结:

  • Callable是一个可以返回结果或抛出异常的接口,类似于Runnable
  • Future是用于获取异步任务执行结果的类,它提供了方法来查询任务的状态和获取任务的返回值。
  • 通过将Callable任务提交给线程池,可以异步执行任务并在需要时获取结果。

6.2 执行器

Executors是Java标准库中提供的一个工厂类,用于创建不同类型的线程池和执行器。它提供了一些静态方法来快速创建线程池,以及其他与并发执行任务相关的方法。虽然Executors提供了便利的方式来创建线程池,但需要注意在某些情况下,直接使用Executors创建线程池可能会导致资源管理和性能问题。

以下是一些常用的Executors方法:

  1. newFixedThreadPool(int nThreads) 创建一个固定大小的线程池,其中包含指定数量的线程,线程池的大小不会更改。任务会在空闲线程中排队,如果没有可用线程,则会等待。
  2. newCachedThreadPool() 创建一个可根据需要创建新线程的线程池。当线程池中的线程闲置时,它们会被保留一段时间,然后在需要时被丢弃。适用于短期异步任务执行。
  3. newSingleThreadExecutor() 创建一个只有一个线程的线程池,用于按顺序执行任务。任务会在一个单独的线程中执行,可以保证任务的顺序性。
  4. newScheduledThreadPool(int corePoolSize) 创建一个固定大小的线程池,支持延迟和周期性任务的调度执行。
  5. newWorkStealingPool(int parallelism) 创建一个工作窃取线程池,每个线程都维护自己的任务队列。适用于处理计算密集型任务。

虽然这些方法提供了方便的方式来创建线程池,但是它们并不适用于所有情况。在一些需要更精细控制线程池参数和行为的情况下,可能需要直接使用ThreadPoolExecutor类进行自定义配置。

总结:

  • Executors是Java标准库中的工厂类,用于创建线程池和执行器。
  • 它提供了一些静态方法来创建不同类型的线程池,如固定大小线程池、缓存线程池等。
  • 尽管方便,但某些情况下使用这些方法可能导致资源管理和性能问题。
  • 对于更复杂的线程池需求,可以考虑直接使用ThreadPoolExecutor类来进行自定义配置。

下面总结了使用连接池时所做的工作:

  1. 调用Executors类的静态方法newCachedThreadPool或newFixedThreadPool
  2. 调用submit提交Runnable或Callable对象
  3. 保留返回的Future对象,以便得到结果或者取消任务
  4. 不想再提交任何任务时,调用shutdown

6.3 控制任务组

invokeAny是Java多线程编程中ExecutorService接口提供的一个方法,用于执行一组任务并返回其中任意一个完成的任务的结果(或者抛出的异常)。这在某些情况下很有用,例如你有多个任务可以执行相同的操作,只需要获取最先完成的任务结果。

以下是invokeAny方法的基本语法:

<T> T invokeAny(Collection<? extends Callable<T>> tasks)
    throws InterruptedException, ExecutionException;

其中:

  • tasks 是一组实现了Callable接口的任务集合。
  • invokeAny 方法将会执行这些任务,并返回其中任意一个任务的结果。
  • 如果其中一个任务完成(无论成功还是失败),invokeAny 将立即返回结果,忽略其他任务的执行。
  • 如果没有任务成功完成,invokeAny 将抛出一个ExecutionException,其中包含了所有任务抛出的异常。

示例代码:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class InvokeAnyExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        List<Callable<String>> tasks = new ArrayList<>();
        tasks.add(() -> {
            Thread.sleep(2000); // 模拟任务执行时间
            return "Task 1";
        });
        tasks.add(() -> "Task 2");
        tasks.add(() -> "Task 3");

        String result = executorService.invokeAny(tasks);

        System.out.println("Result: " + result);

        executorService.shutdown();
    }
}

在上述示例中,我们创建了一个包含三个任务的任务集合。invokeAny 方法会执行这些任务,并返回最先完成的任务的结果。在这个例子中,最先完成的任务是第二个任务,它会返回字符串 "Task 2"。注意,其他任务的执行结果不会被返回。

需要注意的是,invokeAny 方法并不会等待所有任务完成,而是在有一个任务完成时就立即返回。因此,它适用于那些只需要一个任务的结果的情况。如果你需要等待所有任务完成并收集所有结果,应该使用 invokeAll 方法。

6.4 fork-join框架

Fork-Join框架是Java标准库中的一个并发编程框架,用于处理任务分解和并行执行的问题。它主要用于解决递归划分的任务,通常用于处理分而治之的问题。Fork-Join框架的核心概念是"分而治之"(Divide and Conquer)。

以下是使用Fork-Join框架的关键概念和用法:

  1. ForkJoinPool:Fork-Join框架的核心是ForkJoinPool,它是一个线程池,用于管理和执行Fork-Join任务。它通过工作窃取(Work Stealing)算法来实现任务的动态分配和负载均衡。
  2. RecursiveTask和RecursiveAction:Fork-Join框架定义了两种类型的任务,RecursiveTask用于返回结果的任务,RecursiveAction用于没有返回值的任务。你可以继承这两个类来创建自己的任务。
  3. fork()和join()方法:在任务内部,你可以使用fork()方法将任务拆分成子任务,然后使用join()方法等待子任务完成并收集结果。这种方式使得任务可以递归地分解为更小的任务,然后并行执行。
  4. 任务的划分:Fork-Join框架通常用于解决递归的问题,其中任务被划分为较小的子任务,然后并行执行。这种划分通常在任务的compute()方法内部进行。

下面是一个简单的示例,演示了如何使用Fork-Join框架来计算斐波那契数列的值:

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class FibonacciTask extends RecursiveTask<Long> {
    private final int n;

    public FibonacciTask(int n) {
        this.n = n;
    }

    @Override
    protected Long compute() {
        if (n <= 1) {
            return (long) n;
        } else {
            FibonacciTask task1 = new FibonacciTask(n - 1);
            FibonacciTask task2 = new FibonacciTask(n - 2);
            task1.fork();
            return task2.compute() + task1.join();
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        FibonacciTask task = new FibonacciTask(10);
        long result = pool.invoke(task);
        System.out.println("Fibonacci(10) = " + result);
    }
}

在这个示例中,我们创建了一个FibonacciTask任务,它递归地计算斐波那契数列的值。在compute()方法内部,任务被划分为两个子任务,然后使用fork()join()来执行子任务并汇总结果。

7. 异步计算

7.1 可完成Future

你可以使用CompletableFuture类来创建可完成的Future,它提供了更多的功能和灵活性,允许你以异步和非阻塞的方式处理任务。CompletableFuture是Java标准库中的一部分,自Java 8版本开始引入。

下面是一个简单的示例,演示如何使用CompletableFuture创建可完成的Future并异步执行任务:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureExample {
    public static void main(String[] args) {
        // 创建一个CompletableFuture并异步执行任务
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            // 模拟一个耗时任务
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 42; // 返回结果
        });

        // 使用thenApply异步处理任务结果
        CompletableFuture<String> resultFuture = future.thenApplyAsync(result -> {
            return "Result is: " + result;
        });

        // 阻塞等待任务完成并获取结果
        try {
            String result = resultFuture.get();
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,我们首先创建了一个CompletableFuture并使用supplyAsync方法异步执行一个耗时任务,然后使用thenApplyAsync方法异步处理任务的结果。最后,我们使用get方法阻塞等待任务完成并获取最终的结果。

CompletableFuture提供了丰富的方法来处理异步任务的结果,包括thenApplythenAcceptthenCombine等等,可以根据需要链式调用这些方法来构建异步处理流程。

使用CompletableFuture可以更方便地编写异步和非阻塞的代码,以提高应用程序的并发性和性能。这对于处理多个并行任务或异步I/O操作非常有用。

7.2 组合可完成Future

可以使用CompletableFuture类来组合多个可完成的Future,以便在一个或多个任务完成时触发其他操作。CompletableFuture提供了丰富的组合方法,例如thenCombinethenComposethenApplyAsync等,用于处理多个CompletableFuture的结果。

以下是一些示例,演示如何组合可完成的Future

1. 使用thenCombine组合两个CompletableFuture的结果:

import java.util.concurrent.CompletableFuture;

public class CompletableFutureCombinationExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);

        CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + result2;
        });

        combinedFuture.thenAccept(result -> {
            System.out.println("Combined result: " + result);
        });
    }
}

在这个示例中,我们创建了两个CompletableFuture,然后使用thenCombine方法将它们的结果组合起来,最后使用thenAccept方法处理组合的结果。

2. 使用thenCompose组合两个CompletableFuture

import java.util.concurrent.CompletableFuture;

public class CompletableFutureCombinationExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);

        CompletableFuture<Integer> combinedFuture = future1.thenCompose(result1 -> {
            return future2.thenApply(result2 -> result1 + result2);
        });

        combinedFuture.thenAccept(result -> {
            System.out.println("Combined result: " + result);
        });
    }
}

在这个示例中,我们使用thenCompose方法将两个CompletableFuture的结果组合在一起,然后使用thenAccept方法处理组合的结果。

CompletableFuture的组合方法使你能够构建复杂的异步操作流程,根据不同的需求组合和处理多个CompletableFuture的结果。你可以根据具体的情况选择适合的组合方法来实现你的异步任务逻辑。

8. 进程

8.1 建立进程

通过使用ProcessBuilder类来启动外部进程。以下是一个简单的示例,演示如何使用Java创建一个新的进程:

import java.io.IOException;

public class CreateProcessExample {
    public static void main(String[] args) {
        try {
            // 创建一个进程构建器
            ProcessBuilder processBuilder = new ProcessBuilder("your_command_here", "argument1", "argument2");
            
            // 设置工作目录(可选)
            processBuilder.directory(new File("path_to_working_directory"));
            
            // 启动进程
            Process process = processBuilder.start();
            
            // 等待进程执行完毕
            int exitCode = process.waitFor();
            
            // 输出进程的退出码
            System.out.println("进程退出码:" + exitCode);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上面的示例中,你需要将your_command_here替换为你要执行的实际命令,并提供任何需要的参数。ProcessBuilder还允许你设置工作目录,以及其他进程的环境变量等。

创建进程后,你可以使用waitFor方法等待进程执行完毕,并获取进程的退出码,以确定进程是否成功完成。