Java中的线程创建和同步快速入门(保姆级教程)

1. Java 中线程的概念和原理

1.1 什么是线程?

​ 线程是一个程序中独立的执行路径。一个 Java 应用程序至少有一个线程,即主线程(main 线程),由 main 方法开始执行。Java 通过多线程机制,可以让程序同时执行多个任务(即多线程并发),从而提高程序的效率和响应性。

1.2 线程的原理

​ 在 Java 中,线程是通过 java.lang.Thread 类或实现 java.lang.Runnable接口 或和使用 Callable 接口与 Future 结合来实现的。每个线程都有自己的调用栈和程序计数器。Java 的多线程机制依赖于底层操作系统提供的线程支持,Java 虚拟机(JVM)将线程映射到操作系统的原生线程。

线程的基本原理:多个线程可以并发执行,JVM 会在多个线程之间切换,提供一种“并行”运行的假象。这种并发执行可以在单处理器或多处理器系统上实现。在单处理器系统上,线程的并发执行是通过时间分片(time-slicing)实现的;在多处理器系统上,不同的线程可能真的在同时执行。

2. 创建线程的多种方式

​ Java 中有三种主要方式来创建线程:继承 Thread 类、实现 Runnable 接口、使用 CallableFuture 接口。

2.1 继承 Thread 类

2.1.1 设置和获取线程名称 (setName() 和 getName())

class MyThread extends Thread  //继承Thread
{
    @Override
    public void run() {
        System.out.println(getName() + " 正在运行...");  // 获取并输出线程名称
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        thread1.setName("线程1"); //设置线程1名称
        thread2.setName("线程2"); //设置线程2名称
        
        String thread1Name = thread1.getName(); //获取线程1名称
        String thread2Name = thread2.getName(); //获取线程2名称

        System.out.println(thread1Name);
        System.out.println(thread2Name);

        thread1.start();  // 启动线程1
        thread2.start();  // 启动线程2
    }
}

避免直接调用 run():正确的方式是使用 start() 方法启动线程,以确保任务在新线程中执行。

单次启动:一个 Thread 对象只能启动一次,重复调用 start() 方法会抛出 IllegalThreadStateException 异常。因此,每次启动新线程时都需要创建新的 Thread 对象。

运行结果:

线程1
线程2
线程1 正在运行…
线程2 正在运行…

2.1.2 使用 join() 等待线程完成

class MyThread extends Thread  //继承Thread
{
    @Override
    public void run() {
        System.out.println(getName() + " 正在运行...");  // 获取并输出线程名称
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.setName("线程1"); //设置线程名称
        thread2.setName("线程2"); //设置线程名称

        System.out.println("主线程 正在运行...");
        thread1.start();  // 启动线程1
        thread2.start();  // 启动线程2
        try {
            thread1.join();  // 等待线程1完成
            System.out.println(thread1.getName() + " 已经完成。");

            thread2.join();  // 等待线程2完成
            System.out.println(thread2.getName() + " 已经完成。");
        } catch (InterruptedException e) {
            System.out.println("主线程被中断。");
        }
        System.out.println("主线程 已经完成。");
    }
}

解释:当你在一个线程上调用 join() 方法时,当前线程将被阻塞,直到调用 join() 的那个线程完成执行。也就是说,主线程会等待指定的子线程执行完毕后才会继续执行。

运行结果:

主线程 正在运行…
线程1 正在运行…
线程2 正在运行…
线程1 已经完成。
线程2 已经完成。
主线程 已经完成

2.1.3 使用 sleep() 让线程休眠

class MyThread extends Thread  //继承Thread
{
    @Override
    public void run() {
        try {
            System.out.println(getName() + " 正在运行...");
            Thread.sleep(2000);  // 线程休眠2秒钟
            System.out.println(getName() + " 执行完毕。");
        } catch (InterruptedException e) {
            System.out.println(getName() + " 在休眠时被中断。");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.setName("线程1"); //设置线程名称
        thread2.setName("线程2"); //设置线程名称

        thread1.start();  // 启动线程1
        thread2.start();  // 启动线程2

        try {
            thread1.join();  // 等待线程1完成
            thread2.join();  // 等待线程2完成
        } catch (InterruptedException e) {
            System.out.println("主线程被中断。");
        }
    }
}

运行结果:

线程2 正在运行…
线程1 正在运行…
线程2 执行完毕。 //需要等待2s
线程1 执行完毕。 //需要等待2s

2.1.4 使用 interrupt() 和 isInterrupted() 处理中断

class MyThread extends Thread {
    @Override
    public void run() {
        try {
            System.out.println(getName() + " 开始运行...");
            // 模拟长时间任务,线程休眠10秒钟
            for (int i = 0; i < 10; i++) {
                System.out.println(getName() + " 正在工作:" + (i + 1) + "/10");
                Thread.sleep(1000);  // 每次休眠1秒
            }
            System.out.println(getName() + " 任务完成。");
        } catch (InterruptedException e) {
            // 重新设置中断状态
            Thread.currentThread().interrupt();
            System.out.println(getName() + " 被中断!");
        }
        if (isInterrupted()) {
            System.out.println(getName() + " 检测到中断状态。");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.setName("工作线程");
        thread.start();  // 启动线程

        try {
            Thread.sleep(3000);  // 主线程休眠3秒,让子线程执行一段时间
            thread.interrupt();  // 中断子线程
        } catch (InterruptedException e) {
            System.out.println("主线程被中断。");
        }

        try {
            thread.join();  // 等待子线程完成
        } catch (InterruptedException e) {
            System.out.println("主线程在等待子线程时被中断。");
        }

        System.out.println("主线程结束。");
    }
}

解释:每个线程都有一个中断状态,调用 interrupt() 方法会将目标线程的中断状态设置为“已中断”。线程可以通过检查自身的中断状态(使用 isInterrupted() 方法)或处理 InterruptedException 异常来响应中断。当 InterruptedException 被抛出时,线程的中断状态会被自动清除。如果你想保留中断状态,可以在捕获 InterruptedException 异常后手动再次设置中断状态,这样 isInterrupted() 就会返回 true。

运行结果:

工作线程 开始运行…
工作线程 正在工作:1/10
工作线程 正在工作:2/10
工作线程 正在工作:3/10
工作线程 被中断!
工作线程 检测到中断状态。
主线程结束。

2.1.5 设置守护线程

2.1.5.1 什么是守护线程?

守护线程与普通用户线程的区别在于:

  • 用户线程:当 Java 程序启动时,它会创建一个主线程(main 线程),并可能会启动其他子线程。程序会等待所有用户线程完成后才会终止。
  • 守护线程:与用户线程不同,守护线程是用来在后台执行支持性任务的,当所有用户线程结束时,守护线程会自动终止,不管它是否完成了自己的任务。
2.1.5.2 设置守护线程

​ 你可以通过调用线程的 setDaemon(true) 方法将其设置为守护线程。这个方法必须在调用 start() 方法之前执行,否则会抛出 IllegalThreadStateException 异常。

class DaemonThread extends Thread {
    @Override
    public void run() {
        try {
            while (true) {
                System.out.println(getName() + " 作为守护线程正在运行...");
                Thread.sleep(1000);  // 模拟后台任务,休眠1秒
            }
        } catch (InterruptedException e) {
            System.out.println(getName() + " 守护线程被中断。");
        }
    }
}

class UserThread extends Thread {
    @Override
    public void run() {
        System.out.println(getName() + " 作为用户线程正在运行...");
        try {
            Thread.sleep(3000);  // 模拟任务,休眠3秒
            System.out.println(getName() + " 用户线程任务完成。");
        } catch (InterruptedException e) {
            System.out.println(getName() + " 用户线程被中断。");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setName("守护线程");
        daemonThread.setDaemon(true);  // 设置为守护线程
        daemonThread.start();  // 启动守护线程

        UserThread userThread = new UserThread();
        userThread.setName("用户线程");
        userThread.start();  // 启动用户线程

        try {
            userThread.join();  // 等待用户线程执行完毕
        } catch (InterruptedException e) {
            System.out.println("主线程被中断。");
        }

        System.out.println("主线程结束。");
    }
}

解释:

  • 主线程使用 userThread.join() 等待用户线程执行完毕。此时,主线程会被阻塞,直到用户线程执行完成。

  • 当用户线程完成后,主线程也会执行结束。在此时,所有的用户线程已经结束,守护线程将自动终止,即使守护线程还在运行。

运行结果:

用户线程 作为用户线程正在运行…
守护线程 作为守护线程正在运行…
守护线程 作为守护线程正在运行…
守护线程 作为守护线程正在运行…
守护线程 作为守护线程正在运行…
用户线程 用户线程任务完成。
主线程结束。

2.2 实现 Runnable 接口

Runnable 接口是一个功能性接口(Functional Interface),它只有一个方法 run(),用于定义线程执行的任务逻辑。实现了 Runnable 接口的类可以被线程执行,但 Runnable 本身并不具备线程特性,需要配合 Thread 类来使用。

2.2.1 简单使用

class MyRunnable implements Runnable  //实现接口
{
    private int id;
    public MyRunnable(int id) {
        this.id = id;
    }
    @Override
    public void run() {
        System.out.println("Runnable-"+id+" is running...");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable(1));
        Thread thread2 = new Thread(new MyRunnable(2));
        thread1.start(); // 启动线程
        thread2.start(); // 启动线程
    }
}

解释:

  • 需要将 Runnable 对象传递给 Thread 实例。

  • run() 方法只是定义了线程执行的任务逻辑,直接调用 run() 方法不会启动新的线程,而是会在当前线程中顺序执行 run() 方法的代码。因此,必须使用 Thread 对象的 start() 方法来启动线程,这样才能真正并发执行任务。

  • 一个 Thread 对象只能启动一次,重复调用 start() 方法会抛出 IllegalThreadStateException 异常。因此,每次启动新线程时都需要创建新的 Thread 对象。

  • run() 方法不能抛出受检异常(Checked Exception),因此必须在方法内部处理所有可能的异常。否则,未处理的异常可能会导致线程意外终止。

运行结果:

Runnable-1 is running…
Runnable-2 is running…

2.2.2 与线程池结合使用

Runnable 特别适合与线程池(如 ExecutorService)结合使用,可以避免频繁创建和销毁线程的开销,从而提高性能。

下面是关于不同类型的 ExecutorService 线程池的对比表格:

线程池类型描述
newFixedThreadPool(int nThreads)创建一个固定大小的线程池。线程数量固定,当所有线程都在忙碌时,新的任务会在队列中等待。
newCachedThreadPool()创建一个可根据需要创建新线程的线程池。如果线程空闲 1 秒后会被终止和移除。
newSingleThreadExecutor()创建一个只有一个线程的线程池。确保所有任务按顺序执行,且在一个线程中串行执行任务。
newScheduledThreadPool(int corePoolSize)创建一个支持定时和周期性任务的线程池。可以调度任务在固定延迟后或周期性地执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

class MyRunnable implements Runnable {
    private int id;
    public MyRunnable(int id) {
        this.id = id;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("Runnable-"+id+" is running...");
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建一个固定大小为 3 的线程池,线程池管理并复用这 3 个线程
        try (ExecutorService executor = Executors.newFixedThreadPool(3)) {
            // 使用 for 循环提交 3 个任务给线程池
            for (int i = 0; i < 3; i++) {
                // 每次循环都会创建一个新的 MyRunnable 实例,并将其提交给线程池
                executor.submit(new MyRunnable(i));
            }
            // 告诉线程池不再接受新的任务,并在所有提交的任务完成后关闭线程池
            executor.shutdown();
        }

        System.out.println("############");
        // 创建一个只有一个线程的线程池
        try (ExecutorService executor = Executors.newSingleThreadExecutor()){
            // 使用 for 循环提交 3 个任务给线程池
            for (int i = 0; i < 3; i++) {
                // 每次循环都会创建一个新的 MyRunnable 实例,并将其提交给线程池
                executor.submit(new MyRunnable(i));
            }
            // 告诉线程池不再接受新的任务,并在所有提交的任务完成后关闭线程池
            executor.shutdown();
        }

        System.out.println("############");
        // 创建一个可根据需要创建新线程的线程池,如果线程空闲 1 秒后会被终止和移除。
        try (ExecutorService executor = Executors.newCachedThreadPool()) {

            // 使用 for 循环提交 3 个任务给线程池
            for (int i = 0; i < 3; i++) {
                // 每次循环都会创建一个新的 MyRunnable 实例,并将其提交给线程池
                executor.submit(new MyRunnable(i));
            }

            // 等待所有任务完成,如果超过指定时间仍未完成,则强制终止
            if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
                executor.shutdownNow();  // 强制终止线程池
            }

            // 告诉线程池不再接受新的任务,并在所有提交的任务完成后关闭线程池
            executor.shutdown();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("############");
        // 创建一个核心线程池大小为 3 的调度线程池
        try (ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3)) {

            // 调度一个任务在延迟 5 秒后执行一次
            scheduler.schedule(new MyRunnable(1), 5, TimeUnit.SECONDS);

            // 调度一个任务在延迟 2 秒后开始执行,并且每隔 3 秒执行一次
            scheduler.scheduleAtFixedRate(new MyRunnable(2), 2, 3, TimeUnit.SECONDS);

            // 调度一个任务在延迟 3 秒后开始执行,并且每次执行完后等待 4 秒再执行
            scheduler.scheduleWithFixedDelay(new MyRunnable(3), 3, 4, TimeUnit.SECONDS);

            // 主线程等待一段时间,以便观察任务执行
            try {
                Thread.sleep(20000);  // 主线程休眠 20 秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 关闭调度线程池
            scheduler.shutdown();
            try {
                // 等待所有任务完成,如果超过指定时间仍未完成,则强制终止
                if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
                    scheduler.shutdownNow();  // 强制终止线程池
                }
            } catch (InterruptedException e) {
                scheduler.shutdownNow();
            }
        }

    }
}

运行结果:

Runnable-2 is running…
Runnable-0 is running…
Runnable-1 is running…
############
Runnable-0 is running…
Runnable-1 is running…
Runnable-2 is running…
############
Runnable-0 is running…
Runnable-1 is running…
Runnable-2 is running…
############
Runnable-2 is running…
Runnable-3 is running…
Runnable-1 is running…
Runnable-2 is running…
Runnable-3 is running…
Runnable-2 is running…
Runnable-2 is running…
Runnable-3 is running…
Runnable-2 is running…
Runnable-3 is running…
Runnable-2 is running…

2.2.3 使用匿名类或 lambda 表达式

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("任务正在运行...");
        });
        thread.start();
    }
}

2.3 使用 CallableFuture(带返回值的线程)

在 Java 中,CallableFuture 是用于多线程编程的强大工具,它们提供了比 Runnable 更高级的功能,尤其是在需要任务返回结果或处理任务执行过程中的异常时。

2.3.1 什么是 Callable?

Callable接口类似于 RunnableCallable 也用于定义要在线程中执行的任务。然而,CallableRunnable 的主要区别在于:

  • Callable 可以返回一个结果。

  • Callable 可以抛出受检异常(Checked Exception)。

  • Callable<V> 接口只有一个方法 call(),其中 V 是返回值的类型。调用 call() 方法时,任务会执行并返回一个结果,或者抛出一个异常。

2.3.2 什么是 Future?

Future 接口用于表示异步计算的结果。通过 Future 对象,你可以检查任务是否完成、等待任务完成并获取结果、取消任务等。

  • get() 方法:用于获取任务的结果,会阻塞直到任务完成。

  • cancel() 方法:用于尝试取消任务。

  • isDone() 方法:检查任务是否已经完成。

  • Future<V> 接口与 Callable<V> 结合使用时,Future 对象表示 Callable 的执行结果。

2.3.3 Callable 和 Future 的使用场景

  • 需要返回结果:当任务需要返回计算结果时,使用 CallableFuture 更为合适,例如执行计算密集型任务或异步获取数据。
  • 处理异常:如果任务可能抛出异常,Callable 可以处理这些异常并将它们返回给调用方。
  • 异步任务的管理:使用 Future 可以异步地检查任务状态或取消任务。
import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
    private final int number;

    public MyCallable(int number) {
        this.number = number;
    }

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + " 正在计算 " + number + " 的平方...");
        if (number == 0) {
            throw new IllegalArgumentException("数字不能为 0");
        }
        TimeUnit.SECONDS.sleep(2);  // 模拟长时间任务
        return number * number;  // 返回计算结果
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建一个线程池,大小为 3
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 提交任务并获取 Future 对象
        Future<Integer> future1 = executor.submit(new MyCallable(10));
        Future<Integer> future2 = executor.submit(new MyCallable(20));
        Future<Integer> future3 = executor.submit(new MyCallable(0));  // 将会引发异常

        try {
            // 尝试获取第一个任务的结果,等待时间最多 1 秒
            Integer result1 = future1.get(1, TimeUnit.SECONDS);
            System.out.println("future1 的计算结果: " + result1);
        } catch (TimeoutException e) {
            System.out.println("future1 超时未完成");
        } catch (ExecutionException e) {
            System.out.println("future1 执行时出错: " + e.getCause());
        } catch (InterruptedException e) {
            System.out.println("获取 future1 结果时线程被中断");
        }

        try {
            // 直接获取第二个任务的结果(没有超时限制)
            Integer result2 = future2.get();
            System.out.println("future2 的计算结果: " + result2);
        } catch (ExecutionException e) {
            System.out.println("future2 执行时出错: " + e.getCause());
        } catch (InterruptedException e) {
            System.out.println("获取 future2 结果时线程被中断");
        }

        try {
            // 尝试获取第三个任务的结果,预计会发生异常
            Integer result3 = future3.get();
            System.out.println("future3 的计算结果: " + result3);
        } catch (ExecutionException e) {
            System.out.println("future3 执行时出错: " + e.getCause());
        } catch (InterruptedException e) {
            System.out.println("获取 future3 结果时线程被中断");
        }

        // 尝试取消 future2 任务,虽然它已经完成,但演示取消机制
        boolean cancelled = future2.cancel(true);
        System.out.println("future2 取消结果: " + cancelled);

        // 关闭线程池
        executor.shutdown();
        try {
            // 等待所有任务完成,如果超过指定时间仍未完成,则强制终止
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();  // 强制终止线程池
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }

        System.out.println("主线程结束。");
    }
}

运行结果:

pool-1-thread-2 正在计算 20 的平方…
pool-1-thread-1 正在计算 10 的平方…
pool-1-thread-3 正在计算 0 的平方…
future1 超时未完成
future2 的计算结果: 400
future3 执行时出错: java.lang.IllegalArgumentException: 数字不能为 0
future2 取消结果: false
主线程结束。

3. 线程的生命周期

Java 线程从创建到终止,经历以下状态:

线程状态描述状态转换条件
新建(New)线程对象被创建,但尚未调用 start() 方法。线程对象创建后,等待调用 start() 方法。
就绪(Runnable)调用 start() 方法后,线程进入就绪状态,等待 CPU 调度。调用 start() 方法后,线程进入就绪状态,等待被线程调度器选中。
运行(Running)线程获得 CPU 时间片后开始执行 run() 方法。线程调度器分配 CPU 时间片,线程从就绪状态进入运行状态。
阻塞(Blocked)线程因等待某种条件(如 I/O 操作、锁)暂时停止执行。线程在运行时被阻塞,如等待 I/O 操作完成、等待锁释放等。
终止(Terminated)线程完成执行或被强制终止,生命周期结束。线程执行完成、run() 方法正常结束,或线程因异常退出。

4. 线程同步和并发问题

多线程环境下,如果多个线程同时访问共享资源,可能导致数据不一致问题。为了解决这些问题,Java 提供了多种同步机制。

4.1 同步方法

class Counter {
    private int count = 0;

    public synchronized void increment() { // 同步方法,防止并发修改
        count++;
    }

    public int getCount() {
        return count;
    }
}

同步方法确保在一个时刻只能有一个线程执行该方法。其他线程必须等待,直到当前线程执行完毕。

4.2 同步块

同步块可以细化锁定范围,提升性能。

4.2.1 使用 this作为锁对象

class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) { // 同步块,锁定当前对象
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

解释:this 是当前对象的引用,表示锁定这个对象本身。对于同一个对象实例,synchronized(this) 确保在某个线程持有锁的情况下,其他线程无法同时进入这个 synchronized 块或同步方法。这种方式适用于实例级别的同步,即同一个实例的多个线程不能同时执行 synchronized 保护的代码。

4.2.2 使用一个私有的锁对象

private final Object lock = new Object();

public void increment() {
    synchronized (lock) { // 锁定私有锁对象
        count++;
    }
}

解释:我们创建了一个新的 Object 实例并将其用于锁定。这种方式的好处是更灵活且更安全,因为它仅用于同步 increment 方法的逻辑,不会和其他代码的 synchronized(this) 冲突。当你不希望锁定整个对象(this),而只是想为特定的代码块或方法设置一个锁时,可以使用这种方式。如果一个对象的不同方法需要不同的锁,可以为每个方法使用不同的锁对象。

4.2.3 使用类对象作为锁

public void increment() {
    synchronized (Counter.class) { // 锁定类对象
        count++;
    }
}

解释:这个锁对象是 Counter 类的类对象。所有该类的实例都共享同一个 Counter.class 锁。这意味着,无论创建了多少个 Counter 对象,这些对象在执行 increment() 方法时都会使用相同的锁对象。如果你希望所有实例共享同一个锁,可以使用这种方式。它适用于需要在类级别同步的方法,比如静态方法。

代码部分案例:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(counter.getCount()); // 没有锁的情况下结果可能小于 2000
    }
}

class Counter {
    private final Object lock = new Object();

    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

运行结果:

2000

4.3 使用 ReentrantLock

ReentrantLock 是 Java java.util.concurrent.locks 包中的一种锁实现,提供了比 synchronized 更加灵活的锁定机制。它允许显式地获取和释放锁,可以尝试获取锁,支持公平锁等功能。

需要手动控制锁的获取和释放ReentrantLock 允许你在代码中手动控制锁的获取和释放,这比 synchronized 更加灵活。

需要尝试获取锁ReentrantLock 提供了 tryLock() 方法,允许线程尝试获取锁而不是阻塞等待。

需要实现公平锁:通过构造 ReentrantLock 时传入 true,可以创建一个公平锁,保证锁的获取顺序是按照请求顺序的。

4.3.1 手动控制锁的获取和释放

class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 显式获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 确保锁被释放
        }
    }

    public int getCount() {
        return count;
    }
}

解释:在 increment() 方法中,显式调用 lock.lock() 获取锁,在 finally 块中调用 lock.unlock() 释放锁。这保证了无论是否发生异常,锁都会被释放。

4.3.2 尝试获取锁

import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private final ReentrantLock lock = new ReentrantLock();  // 创建 ReentrantLock 锁
    private int count = 0;

    public void increment() {
        while (true) {
            if (lock.tryLock()) // 尝试获取锁
                break;
            // 重试直到获取锁成功
        }
        try {
            count++;
        }finally {
            lock.unlock(); // 确保锁被释放
        }
    }
    public int getCount() {
        return count;
    }
}

解释:tryLock() 方法尝试获取锁,如果获取成功,返回 true,否则返回 false。这使得线程不会因为等待锁而阻塞,可以执行其他逻辑。

4.3.3 实现公平锁

import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private final ReentrantLock lock = new ReentrantLock(true); // 创建公平锁
    private int count = 0;

    public void increment() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() +" 获得锁");
            count++;
        }finally {
            System.out.println(Thread.currentThread().getName() +" 释放锁");
            lock.unlock(); // 确保锁被释放
        }
    }

    public int getCount() {
        return count;
    }
}

解释:通过 ReentrantLock(true) 创建的锁是公平锁,保证了多个线程获取锁的顺序是按照它们请求锁的顺序。公平锁可以避免线程饥饿问题,因为它确保每个请求锁的线程最终都会按顺序获得锁。

代码部分案例:

import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private final ReentrantLock lock = new ReentrantLock(true); // 创建公平锁
    private int count = 0;

    public void increment() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() +" 获得锁");
            count++;
        }finally {
            System.out.println(Thread.currentThread().getName() +" 释放锁");
            lock.unlock(); // 确保锁被释放
        }
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(counter.getCount()); 
    }
}

运行结果:

Thread-0 获得锁
Thread-0 释放锁
Thread-1 获得锁
Thread-1 释放锁
Thread-0 获得锁
Thread-0 释放锁

Thread-1 获得锁
Thread-1 释放锁
Thread-0 获得锁
Thread-0 释放锁
Thread-1 获得锁
Thread-1 释放锁
Thread-0 获得锁
Thread-0 释放锁
Thread-1 获得锁
Thread-1 释放锁
20

4.4 使用wait()和notify()

4.1 wait() 方法的作用

  • 挂起线程并释放锁wait() 的主要职责是使当前线程进入等待状态,并释放它持有的对象锁,以便其他线程能够获取该锁并继续执行。

  • 等待条件满足:线程调用 wait() 后,会一直等待,直到其他线程调用 notify()notifyAll() 方法来唤醒它。通常,这些方法在配合 synchronized 块使用时,允许线程在等待某个条件时进入等待状态。

  • 中断响应wait() 也会对中断响应。如果线程在等待期间被吗,断,它会抛出 InterruptedException

4.2 notify()的作用

  • 唤醒一个等待的线程notify() 的主要职责是唤醒在同一个对象上等待的某一个线程。如果多个线程在同一个对象上等待,notify() 会随机选择其中一个线程进行唤醒。

  • 不立即释放锁:调用 notify() 后,当前线程并不会立即释放锁,而是继续执行同步代码块,直到退出 synchronized 块后才释放锁。这时,被唤醒的线程才能尝试重新获取锁并继续执行。

4.3 notifyAll() 方法的职责

  • 唤醒所有等待的线程notifyAll() 的主要职责是唤醒在同一个对象上等待的所有线程。这些线程将会重新竞争对象锁,只有一个线程能够成功获取锁并继续执行,其他线程则继续等待锁的释放。

  • 确保所有等待线程都有机会执行:在一些复杂的场景中,多个线程可能会因为同一个条件而进入等待状态。notifyAll() 确保所有这些线程都有机会被唤醒,而不会出现有线程长时间等待的情况。

4.4 生产者-消费者模型

生产者:负责生产数据,并将其放入共享缓冲区中。如果缓冲区已满,生产者必须等待,直到消费者消费掉一些数据。

消费者:负责从共享缓冲区中取出数据。如果缓冲区为空,消费者必须等待,直到生产者生产出新的数据。

完整案例代码:

import java.util.LinkedList;
import java.util.Queue;

/**
 * Buffer 类实现了一个带有容量限制的线程安全的缓冲区,用于生产者-消费者模型。
 * 该类通过 synchronized 方法和 wait/notifyAll 机制,确保多个线程安全地访问共享资源。
 */
class Buffer {
    private final Queue<Integer> queue = new LinkedList<>();  // 使用 LinkedList 作为队列实现缓冲区
    private final int capacity;  // 缓冲区的最大容量

    /**
     * 构造方法,用于初始化缓冲区容量。
     * @param capacity 缓冲区的最大容量
     */
    public Buffer(int capacity) {
        this.capacity = capacity;
    }

    /**
     * 生产者方法,将一个整数值添加到缓冲区中。
     * 如果缓冲区已满,生产者将等待直到有空位。
     * @param value 要生产的整数值
     * @throws InterruptedException 如果线程在等待过程中被中断
     */
    public synchronized void produce(int value) throws InterruptedException {
        // 如果缓冲区已满,生产者进入等待状态
        while (queue.size() == capacity) {
            System.out.println("缓冲区已满,生产者等待...");
            wait();  // 当前线程进入等待状态,并释放锁
        }
        queue.offer(value);  // 将生产的值添加到队列中
        System.out.println("生产者生产了: " + value);
        notifyAll();  // 唤醒所有在等待的消费者线程
    }

    /**
     * 消费者方法,从缓冲区中取出一个整数值。
     * 如果缓冲区为空,消费者将等待直到有新的值被生产。
     * @throws InterruptedException 如果线程在等待过程中被中断
     */
    public synchronized void consume() throws InterruptedException {
        // 如果缓冲区为空,消费者进入等待状态
        while (queue.isEmpty()) {
            System.out.println("缓冲区为空,消费者等待...");
            wait();  // 当前线程进入等待状态,并释放锁
        }
        int value = queue.poll();  // 从队列中取出一个值
        System.out.println("消费者消费了: " + value);
        notifyAll();  // 唤醒所有在等待的生产者线程
    }
}

/**
 * Producer 类实现了一个生产者线程。
 * 生产者将不断向缓冲区中添加整数值,直到生产完成。
 */
class Producer implements Runnable {
    private final Buffer buffer;

    /**
     * 构造方法,初始化生产者的缓冲区引用。
     * @param buffer 与生产者关联的缓冲区对象
     */
    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    /**
     * 生产者线程的运行逻辑,生产10个整数值。
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                buffer.produce(i);  // 生产整数值 i
                Thread.sleep((int)(Math.random() * 500));  // 模拟生产时间,随机等待0-500毫秒
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();  // 如果线程被中断,恢复中断状态
            }
        }
    }
}

/**
 * Consumer 类实现了一个消费者线程。
 * 消费者将不断从缓冲区中取出整数值,直到消费完成。
 */
class Consumer implements Runnable {
    private final Buffer buffer;

    /**
     * 构造方法,初始化消费者的缓冲区引用。
     * @param buffer 与消费者关联的缓冲区对象
     */
    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }

    /**
     * 消费者线程的运行逻辑,消费10个整数值。
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                buffer.consume();  // 从缓冲区中消费一个整数值
                Thread.sleep((int)(Math.random() * 500));  // 模拟消费时间,随机等待0-500毫秒
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();  // 如果线程被中断,恢复中断状态
            }
        }
    }
}

/**
 * Main 类启动生产者-消费者模型的程序入口。
 */
public class Main {
    public static void main(String[] args) {
        Buffer buffer = new Buffer(5);  // 创建一个容量为5的缓冲区

        Producer producer = new Producer(buffer);  // 创建一个生产者,并关联到缓冲区
        Consumer consumer = new Consumer(buffer);  // 创建一个消费者,并关联到缓冲区

        Thread producerThread = new Thread(producer);  // 创建生产者线程
        Thread consumerThread = new Thread(consumer);  // 创建消费者线程

        producerThread.start();  // 启动生产者线程
        consumerThread.start();  // 启动消费者线程
    }
}

运行结果:

生产者生产了: 0
消费者消费了: 0
缓冲区为空,消费者等待…
生产者生产了: 1
消费者消费了: 1
生产者生产了: 2
生产者生产了: 3
生产者生产了: 4
生产者生产了: 5
消费者消费了: 2
生产者生产了: 6
消费者消费了: 3
生产者生产了: 7
消费者消费了: 4
消费者消费了: 5
生产者生产了: 8
消费者消费了: 6
生产者生产了: 9
消费者消费了: 7
消费者消费了: 8
消费者消费了: 9

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值