掘金 后端 ( ) • 2024-04-25 13:23

在此之前,我们先要明白什么是并发?为什么要并发编程?

在计算机中,同一时刻,只能有一条指令,在一个CPU上执行 后面的指令必须等到前面指令执行完才能执行,就是串行。在早年CPU核心数还少的时候倒是没什么。但是现如今,CPU性能(核心数和频率)已经不同往昔,为了充分利用CPU性能,我们引入并发。就好比银行只有5个窗口,有5个人要办事,就可以一起处理,第六个人到来才需要排队。

Java如何进行并发编程

在 Java 中进行并发编程可以利用语言和库提供的特性,如线程、线程池、同步机制等。Java 为并发编程提供了许多有用的工具和库,包括基本的 Thread 类、并发集合、锁、条件变量等。在 Java 7 及以上版本中,java.util.concurrent 包还提供了更高层次的并发编程工具,包括线程池、并发队列、异步任务(FutureCompletableFuture)等。

1. 开辟新线程

  • Thread:Java 提供了 Thread 类来创建和管理线程。你可以通过继承 Thread 类或实现 Runnable 接口来定义线程。
public class MyThread extends Thread {
    @Override
    public void run() {
        // 在这个方法中定义线程要执行的任务
        System.out.println("Thread is running");
    }

    public static void main(String[] args) {
        // 创建 MyThread 类的实例
        MyThread myThread = new MyThread();
        // 启动线程
        myThread.start();
    }
}

  • Runnable 接口:通过实现 Runnable 接口,并将其传递给 Thread 类的构造函数,来定义线程的执行逻辑。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 在这个方法中定义线程要执行的任务
        System.out.println("Runnable is running");
    }

    public static void main(String[] args) {
        // 创建 MyRunnable 类的实例
        MyRunnable myRunnable = new MyRunnable();
        // 将 Runnable 对象传递给 Thread 构造函数
        Thread thread = new Thread(myRunnable);
        // 启动线程
        thread.start();
    }
}

2. 同步与锁

  • synchronized 关键字:用于在方法或代码块上加锁,确保在同一时间只有一个线程可以执行受保护的代码。
  • Lock 接口:提供了 synchronized 的替代方法,更灵活的锁定机制。
  • ReadWriteLock:提供了读写锁,用于优化读多写少的场景。

3. 线程池

  • ExecutorService:用于管理线程池,可以通过 Executors 类提供的方法来创建不同类型的线程池。
  • ScheduledExecutorService:用于安排定时或周期性的任务执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ConcurrencyDemo {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // 提交任务给线程池
        Future<?> future1 = executor.submit(() -> {
            System.out.println("任务 1 开始");
            // 执行任务逻辑
            System.out.println("任务 1 结束");
        });
        
        Future<?> future2 = executor.submit(() -> {
            System.out.println("任务 2 开始");
            // 执行任务逻辑
            System.out.println("任务 2 结束");
        });

        // 等待任务执行完成
        try {
            future1.get(); // 阻塞直到任务 1 完成
            future2.get(); // 阻塞直到任务 2 完成
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executor.shutdown();
    }
}

创建了一个固定大小为 3 的线程池,然后提交了两个任务,并等待它们执行完毕。最后关闭线程池。

4. 并发集合

  • java.util.concurrent 包中的并发集合:例如 ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueue 等,适合在多线程环境下使用。

5. 异步任务与回调

  • Future 接口:代表异步计算的结果。可以通过 ExecutorService 提供的方法提交任务,得到 Future 对象。
  • CompletableFuture:更高级的异步任务工具,提供链式回调、组合和异常处理等功能。

6. 其他并发工具

  • CountDownLatch:用于线程同步,让线程等待直到某些操作完成。
  • CyclicBarrier:一个同步点,允许多个线程等待彼此到达一个状态。
  • Semaphore:用于控制资源的访问数量。

Go语言怎么并发

Go 语言以其强大的并发编程特性而闻名。Go 语言提供了一些基本概念和机制来处理并发,包括 goroutines、channels 和 select 语句。这些工具使得 Go 在处理并发任务时非常高效且易于编写。

1. Goroutine

  • Goroutine 是 Go 语言中的轻量级线程。通过使用 go 关键字,可以在后台启动一个新的 goroutine 来执行任务。
  • Goroutine 是非常轻量级的,可以同时启动大量 goroutine,而不会对系统资源产生很大的负担。
package main

import (
    "fmt"
)

func sayHello() {
    fmt.Println("Hello!")
}

func main() {
    go sayHello() // 启动一个新的 goroutine 执行 sayHello 函数

    fmt.Println("Main goroutine")
}

2. Channels

  • Channels 是 Go 语言中用于 goroutine 之间通信的工具。通过 channels,可以在不同的 goroutine 之间传递数据。
  • 可以使用 make 函数创建 channels,并通过 <- 运算符发送和接收数据。
package main

import (
    "fmt"
)

func producer(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i // 发送数据到 channel
        fmt.Printf("Produced: %d\n", i)
    }
    close(ch) // 关闭 channel
}

func consumer(ch chan int) {
    for value := range ch { // 从 channel 接收数据
        fmt.Printf("Consumed: %d\n", value)
    }
}

func main() {
    ch := make(chan int) // 创建一个整型 channel

    go producer(ch) // 启动生产者 goroutine
    go consumer(ch) // 启动消费者 goroutine

    // 让主程序等待 goroutine 结束
    // 可以使用 time.Sleep() 或 wait group 来实现
}

3. Select 语句

  • select 语句用于在多个 channel 上等待,并选择其中一个准备好的 channel 进行通信。
  • select 语句类似于 switch 语句,但它是针对 channels 的。
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- 1
    }()

    go func() {
        time.Sleep(1 * time.Second)
        ch2 <- 2
    }()

    select {
        case value := <-ch1:
            fmt.Printf("Received %d from ch1\n", value)
        case value := <-ch2:
            fmt.Printf("Received %d from ch2\n", value)
    }
}

select 语句等待两个 channel 之一发送数据,然后接收数据并执行相应的分支。

这些是 Go 语言中并发编程的基本概念和用法。通过组合这些特性,Go 语言可以处理复杂的并发任务。

Java的虚拟线程

Java 21 中引入了 虚拟线程(Virtual Threads) ,它是一种新的线程实现,旨在提高并发应用程序的性能和可扩展性。虚拟线程与传统线程不同,它们是一种轻量级的线程实现,可以在 Java 虚拟机(JVM)上更有效地处理大量并发任务。

以下是虚拟线程的主要特点和优势:

1. 轻量级

  • 虚拟线程是轻量级的,创建和销毁的开销很小,因此可以在应用程序中创建大量虚拟线程。
  • 与传统线程相比,虚拟线程的资源占用较小,这使得在大量并发任务的场景下更加高效。

2. 阻塞友好

  • 虚拟线程可以友好地阻塞在 I/O 操作或其他同步操作上,而不会影响其他虚拟线程的执行。
  • JVM 可以将阻塞的虚拟线程切换到其他可运行的虚拟线程,保持高效的并发执行。

3. 无缝集成

  • 虚拟线程与现有的 Java 代码无缝集成,这意味着开发者可以在现有的代码基础上直接使用虚拟线程。
  • 开发者可以继续使用现有的并发 API,如 ThreadExecutorService 等,只需将其替换为虚拟线程的实现即可。

4. 简单的编程模型

  • 虚拟线程提供了一个更简单的编程模型,因为开发者可以像使用普通线程一样使用虚拟线程。
  • 开发者可以更专注于业务逻辑,而不必担心底层线程管理的复杂性。

5. 高性能

  • 虚拟线程的调度和执行更高效,可以充分利用多核 CPU 的优势。
  • 对于需要处理大量并发任务的应用程序,虚拟线程可以显著提高性能。

使用示例:

使用虚拟线程非常简单,只需在 Thread 类的实例化中指定 Thread.ofVirtual() 作为工厂方法即可:

public class VirtualThreadExample {
    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println("Virtual thread is running");
        };

        // 创建一个虚拟线程
        Thread virtualThread = Thread.ofVirtual().start(task);

        // 等待虚拟线程执行完毕
        try {
            virtualThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们使用 Thread.ofVirtual() 创建了一个虚拟线程,并启动它来执行指定的任务。与传统线程的使用类似,但虚拟线程在性能和资源效率方面有更大的优势。

虚拟线程是 Java 21 的一个重要特性,为开发者提供了处理大量并发任务的强大工具。