公开笔记

Java 并发编程

Java 多线程,包含线程创建与停止、线程安全三大特性;讲解 volatile、synchronized、ReentrantLock 原理与区别;介绍 AQS、JUC 同步工具、线程池机制配置;还涵盖 CAS、死锁、ThreadLocal 及并发实战场景。

发布于 更新于

多线程

多线程注意点

Java 多线程需要注意的点

  • 首先是线程安全问题。多个线程同时操作共享数据时,可能出现错误。比如两个线程同时给一个变量加 1,原本该加 2,结果可能只加了 1,这是因为线程切换时没做好数据保护。需要用 synchronized 关键字、Lock 锁等方式,保证同一时间只有一个线程操作共享数据。
  • 其次是线程间通信。线程需要协作时,比如一个线程生产数据,另一个线程消费数据,要通过 wait()notify() 等方法控制,避免出现一方没准备好,另一方就操作的情况,否则可能导致数据错误或线程无限等待。
  • 然后是线程的创建和销毁成本。频繁创建和销毁线程会消耗系统资源,影响性能。可以用线程池管理线程,提前创建好一定数量的线程,重复使用,减少资源消耗。

Java 的线程安全三个方面体现

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在 Java 中使用了 atomic 包(这个包提供了一些支持原子操作的类,这些类可以在多线程环境下保证操作的原子性)和 synchronized 关键字来确保原子性;
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,在 Java 中使用了 synchronized 和 volatile 这两个关键字确保可见性;
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在 Java 中使用了 happens-before 原则来确保有序性。

保证数据一致性的方案

事务管理

使用数据库事务来确保一组数据库操作要么全部成功提交,要么全部失败回滚。通过 ACID(原子性、一致性、隔离性、持久性)属性,数据库事务可以保证数据的一致性。

锁机制

使用锁来实现对共享资源的互斥访问。在 Java 中,可以使用 synchronized 关键字、ReentrantLock 或其他锁机制来控制并发访问,从而避免并发操作导致数据不一致。

版本控制

通过乐观锁的方式,在更新数据时记录数据的版本信息,从而避免同时对同一数据进行修改,进而保证数据的一致性。

线程创建方式

继承 Thread 类

继承 java.lang.Thread 类,重写 run(),创建实例后通过调用 start() 启动线程

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
}
  • 优点:编写简单,如果需要访问当前线程,无需使用 Thread.currentThread () 方法,直接使用 this,即可获得当前线程
  • 缺点:因为线程类已经继承了 Thread 类,所以不能再继承其他的父类

实现 Runnable 接口

如果一个类已经继承了其他类,就不能再继承 Thread 类,此时可以实现 java.lang.Runnable 接口。实现 Runnable 接口需要重写 run() 方法,然后将此 Runnable 对象作为参数传递给 Thread 类的构造器,创建 Thread 对象后调用其 start() 方法启动线程。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public static void main(String[] args) {
    Thread t = new Thread(new MyRunnable());
    t.start();
}
  • 优点:线程类只是实现了 Runable 接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU 代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  • 缺点:编程稍微复杂,如果需要访问当前线程,必须使用 Thread.currentThread() 方法

实现 Callable 接口与 FutureTask

java.util.concurrent.Callable 接口类似于 Runnable,但 Callable 的 call() 方法可以有返回值并且可以抛出异常。要执行 Callable 任务,需将它包装进一个 FutureTask,因为 Thread 类的构造器只接受 Runnable 参数,而 FutureTask 实现了 Runnable 接口。

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 线程执行的代码,这里返回一个整型结果
        return 1;
    }
}

public static void main(String[] args) {
    MyCallable task = new MyCallable();
    FutureTask<Integer> futureTask = new FutureTask<>(task);
    Thread t = new Thread(futureTask);
    t.start();

    try {
        Integer result = futureTask.get();  // 获取线程执行结果
        System.out.println("Result: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}
  • 缺点:编程稍微复杂,如果需要访问当前线程,必须调用 Thread.currentThread() 方法。
  • 优点:线程只是实现 Runnable 或实现 Callable 接口,还可以继承其他类。这种方式下,多个线程可以共享一个 target 对象,非常适合多线程处理同一份资源的情形。

使用线程池(Executor 框架)

从 Java 5 开始引入的 java.util.concurrent.ExecutorService 和相关类提供了线程池的支持,这是一种更高效的线程管理方式,避免了频繁创建和销毁线程的开销。可以通过 Executors 类的静态方法创建不同类型的线程池。

class Task implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
    }
}

public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(10);  // 创建固定大小的线程池
    for (int i = 0; i < 100; i++) {
        executor.submit(new Task());  // 提交任务到线程池执行
    }
    executor.shutdown();  // 关闭线程池
}
  • 缺点:程池增加了程序的复杂度,特别是当涉及线程池参数调整和故障排查时。错误的配置可能导致死锁、资源耗尽等问题,这些问题的诊断和修复可能较为复杂。
  • 优点:线程池可以重用预先创建的线程,避免了线程创建和销毁的开销,显著提高了程序的性能。对于需要快速响应的并发请求,线程池可以迅速提供线程来处理任务,减少等待时间。并且,线程池能够有效控制运行的线程数量,防止因创建过多线程导致的系统资源耗尽(如内存溢出)。通过合理配置线程池大小,可以最大化 CPU 利用率和系统吞吐量。

停止线程运行

  • 异常法停止:线程调用 interrupt() 方法后,在线程的 run 方法中判断当前对象的 interrupted() 状态,如果是中断状态则抛出异常,达到中断线程的效果。
  • 在沉睡中停止:先将线程 sleep,然后调用 interrupt 标记中断状态,interrupt 会将阻塞状态的线程中断。会抛出中断异常,达到停止线程的效果
  • stop() 暴力停止:线程调用 stop() 方法会被暴力停止,方法已弃用,该方法会有不好的后果:强制让线程停止有可能使一些请理性的工作得不到完成。
  • 使用 return 停止线程:调用 interrupt 标记为中断状态后,在 run 方法中判断当前线程状态,如果为中断状态则 return,能达到停止线程的效果。

调用 interrupt 是如何让线程抛出异常的?

每个线程都一个与之关联的布尔属性来表示其中断状态,中断状态的初始值为 false,当一个线程被其它线程调用 Thread.interrupt() 方法中断时,会根据实际情况做出响应。

  • 如果该线程正在执行低级别的可中断方法(如 Thread.sleep()Thread.join()Object.wait()),则会解除阻塞并抛出 InterruptedException 异常
  • 否则 Thread.interrupt() 仅设置线程的中断状态,在该被中断的线程中稍后可通过轮询中断状态来决定是否要停止当前正在执行的任务。

Java 线程的状态有哪些?

img

sleep 和 wait 的区别是什么?

特性sleep()wait()
所属类Thread 类(静态方法)Object 类(实例方法)
锁释放
使用前提任意位置调用必须在同步块内(持有锁)
唤醒机制超时自动恢复需 notify()/notifyAll() 或超时
设计用途暂停线程执行,不涉及锁协作线程间协调,释放锁让其他线程工作
  • 所属分类的不同:sleep 是 Thread 类的静态方法,可以在任何地方直接通过 Thread.sleep() 调用,无需依赖对象实例。wait 是 Object 类的实例方法,这意味着必须通过对象实例来调用。
  • 锁释放的情况:Thread.sleep() 在调用时,线程会暂停执行指定的时间,但不会释放持有的对象锁。也就是说,在 sleep 期间,其他线程无法获得该线程持有的锁。Object.wait():调用该方法时,线程会释放持有的对象锁,进入等待状态,直到其他线程调用相同对象的 notify() 或 notifyAll() 方法唤醒它
  • 使用条件:sleep 可在任意位置调用,无需事先获取锁。 wait 必须在同步块或同步方法内调用(即线程需持有该对象的锁),否则抛出 IllegalMonitorStateException
  • 唤醒机制:sleep 休眠时间结束后,线程 自动恢复 到就绪状态,等待 CPU 调度。wait 需要其他线程调用相同对象的 notify() 或 notifyAll() 方法才能被唤醒。notify() 会随机唤醒一个在该对象上等待的线程,而 notifyAll() 会唤醒所有在该对象上等待的线程。

blocked 和 waiting 区别

  • BLOCKED 是锁竞争失败后被被动触发的状态,WAITING 是人为的主动触发的状态
  • BLCKED 的唤醒时自动触发的,而 WAITING 状态是必须要通过特定的方法来主动唤醒

不同的线程之间如何通信?

共享变量是最基本的线程间通信方式。多个线程可以访问和修改同一个共享变量,从而实现信息的传递。为了保证线程安全,通常需要使用 synchronized 关键字或 volatile 关键字。

class SharedVariableExample {
    // 使用 volatile 关键字保证变量的可见性
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改共享变量
            flag = true;
            System.out.println("Producer: Flag is set to true.");
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            while (!flag) {
                // 等待共享变量被修改
            }
            System.out.println("Consumer: Flag is now true.");
        });

        producer.start();
        consumer.start();
    }
}

代码解释

  • volatile 关键字确保了 flag 变量在多个线程之间的可见性,即一个线程修改了 flag 的值,其他线程能立即看到。
  • 生产者线程在睡眠 2 秒后将 flag 设置为 true,消费者线程在 flag 为 false 时一直等待,直到 flag 变为 true 才继续执行。 Object 类中的 wait()notify() 和 notifyAll() 方法可以用于线程间的协作。wait() 方法使当前线程进入等待状态,notify() 方法唤醒在此对象监视器上等待的单个线程,notifyAll() 方法唤醒在此对象监视器上等待的所有线程。
class WaitNotifyExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Producer: Producing...");
                    Thread.sleep(2000);
                    System.out.println("Producer: Production finished. Notifying consumer.");
                    // 唤醒等待的线程
                    lock.notify();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Consumer: Waiting for production to finish.");
                    // 进入等待状态
                    lock.wait();
                    System.out.println("Consumer: Production finished. Consuming...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        consumer.start();
        producer.start();
    }
}

代码解释:

  • lock 是一个用于同步的对象,生产者和消费者线程都需要获取该对象的锁才能执行相应的操作。
  • 消费者线程调用 lock.wait() 方法进入等待状态,释放锁;生产者线程执行完生产任务后调用 lock.notify() 方法唤醒等待的消费者线程。 java.util.concurrent.locks 包中的 Lock 和 Condition 接口提供了比 synchronized 更灵活的线程间通信方式。Condition 接口的 await() 方法类似于 wait() 方法,signal() 方法类似于 notify() 方法,signalAll() 方法类似于 notifyAll() 方法。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockConditionExample {
    private static final Lock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Producer: Producing...");
                Thread.sleep(2000);
                System.out.println("Producer: Production finished. Notifying consumer.");
                // 唤醒等待的线程
                condition.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Consumer: Waiting for production to finish.");
                // 进入等待状态
                condition.await();
                System.out.println("Consumer: Production finished. Consuming...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        consumer.start();
        producer.start();
    }
}

代码解释:

  • ReentrantLock 是 Lock 接口的一个实现类,condition 是通过 lock.newCondition() 方法创建的。
  • 消费者线程调用 condition.await() 方法进入等待状态,生产者线程执行完生产任务后调用 condition.signal() 方法唤醒等待的消费者线程。 java.util.concurrent 包中的 BlockingQueue 接口提供了线程安全的队列操作,当队列满时,插入元素的线程会被阻塞;当队列为空时,获取元素的线程会被阻塞。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class BlockingQueueExample {
    private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                System.out.println("Producer: Producing...");
                queue.put(1);
                System.out.println("Producer: Production finished.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                System.out.println("Consumer: Waiting for production to finish.");
                int item = queue.take();
                System.out.println("Consumer: Consumed item: " + item);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        consumer.start();
        producer.start();
    }
}

代码解释:

  • LinkedBlockingQueue 是 BlockingQueue 接口的一个实现类,容量为 1。
  • 生产者线程调用 queue.put(1) 方法将元素插入队列,如果队列已满,线程会被阻塞;消费者线程调用 queue.take() 方法从队列中取出元素,如果队列为空,线程会被阻塞。

线程间通信方式汇总

  • Java 最基本的 wait()、 notify() 、notifyAll()
  • Lock 和 Condition 接口。Lock 接口提供了比 synchronized 更灵活的锁机制,Condition 接口则配合 Lock 实现线程间的等待 / 通知机制。
    • await():使当前线程进入等待状态,直到被其他线程唤醒。
    • signal():唤醒一个等待在该 Condition 上的线程。
    • signalAll():唤醒所有等待在该 Condition 上的线程。
  • volatile 关键字。volatile 关键字用于保证变量的可见性。它会保证对该变量的写操作会立即刷新到主内存中,而读操作会从主内存中读取最新的值。
  • CountDownLatch。CountDownLatch 是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。
    • CountDownLatch(int count):构造函数,指定需要等待的线程数量。
    • countDown():减少计数器的值。
    • await():使当前线程等待,直到计数器的值为 0。
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 执行任务
                    System.out.println(Thread.currentThread().getName() + " 完成任务");
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        latch.await();
        System.out.println("所有线程任务完成");
    }
}
  • CyclicBarrier。CyclicBarrier 是一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达某个公共屏障点。
    • CyclicBarrier(int parties, Runnable barrierAction):构造函数,指定参与的线程数量和所有线程到达屏障点后要执行的操作。
    • await():使当前线程等待,直到所有线程都到达屏障点。
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程都到达屏障点");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 执行任务
                    System.out.println(Thread.currentThread().getName() + " 到达屏障点");
                    barrier.await();
                    // 继续执行后续任务
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  • Semaphore。Semaphore 是一个计数信号量,它可以控制同时访问特定资源的线程数量。
    • Semaphore(int permits):构造函数,指定信号量的初始许可数量。
    • acquire():获取一个许可,如果没有可用许可则阻塞。
    • release():释放一个许可。
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int permitCount = 2;
        Semaphore semaphore = new Semaphore(permitCount);

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获得许可");
                    // 执行任务
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                }
            }).start();
        }
    }
}

停止线程

在 Java 中,停止线程的正确方式是 通过协作式的逻辑控制线程终止,而非强制暴力终止(如已废弃的 Thread.stop()

共享标志位

定义一个 可见的 状态变量,由主线程控制其值,工作线程循环检测该变量以决定是否退出。

public class SafeStopWithFlag implements Runnable {
    // 使用 volatile 保证可见性
    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {
            try {
                // 处理任务逻辑
                System.out.println("Thread is running...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 捕获中断异常后设置 running=false
                running = false;
                Thread.currentThread().interrupt(); // 重新设置中断标志
            }
        }
        System.out.println("Thread terminated safely.");
    }

    // 停止线程的方法(由外部调用)
    public void stop() {
        running = false;
    }
}

调用方式:

SafeStopWithFlag task = new SafeStopWithFlag();
Thread thread = new Thread(task);
thread.start();
// 某个时刻调用停止
Thread.sleep(3000);
task.stop();

中断机制

通过 Thread.interrupt() 触发线程中断状态,结合中断检测逻辑实现安全停止。

public class InterruptExample implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                System.out.println("Working...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 当阻塞时被中断,抛出异常并清除中断状态
                System.out.println("Interrupted during sleep!");
                Thread.currentThread().interrupt(); // 重新设置中断标志
            }
        }
        System.out.println("Thread terminated by interrupt.");
    }
}

调用方式:

Thread thread = new Thread(new InterruptExample());
thread.start();
// 中断线程
Thread.sleep(3000);
thread.interrupt();
  • interrupt() 不会立刻终止线程,只是设置中断标志位。
  • 线程需手动检查中断状态(isInterrupted())或触发可中断操作(如 sleep()wait()join()) 响应中断。
  • 阻塞操作中收到中断请求时,会抛出 InterruptedException 并清除中断状态。

Future 取消任务

使用线程池提交任务,并通过 Future.cancel() 停止线程,依赖中断机制。

public class FutureCancelDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<?> future = executor.submit(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Task running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Task interrupted.");
                    Thread.currentThread().interrupt();
                }
            }
        });

        try {
            Thread.sleep(3000);
            future.cancel(true); // true表示尝试中断任务线程
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            executor.shutdown();
        }
    }
}

处理不可中断的阻塞操作

某些 I/O 或同步操作(如 Socket.accept()Lock.lock())无法通过中断直接响应。此时需结合资源关闭操作。比如,关闭 Socket 释放阻塞。

public class SocketHandler implements Runnable {
    private ServerSocket serverSocket;

    public SocketHandler(ServerSocket serverSocket) {
        this.serverSocket = serverSocket;
    }

    @Override
    public void run() {
        try {
            // serverSocket.accept()阻塞时无法响应中断
            while (!Thread.currentThread().isInterrupted()) {
                Socket socket = serverSocket.accept();
                // 处理连接...
            }
        } catch (IOException e) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Thread stopped by interrupt.");
            }
        }
    }

    // 特殊关闭方法(销毁资源)
    public void stop() {
        try {
            serverSocket.close(); // 关闭资源使accept()抛出异常
        } catch (IOException e) {
            System.out.println("Error closing socket: " + e);
        }
    }
}

调用方式:调用 stop() 方法关闭资源以解除阻塞。

线程停止的正确实践,如下表格:

方法适用场景注意事项
循环检测标志位简单无阻塞的逻辑确保标志位使用 volatile 或通过锁保证可见性
中断机制可中断的阻塞操作正确处理 InterruptedException 并恢复中断标志
Future.cancel()线程池管理任务需要线程池任务支持中断处理机制
资源关闭不可中断的阻塞操作(如 Sockets)显式关闭资源触发异常,结合中断状态判断回滚
  • Thread.stop():暴力终止,可能导致状态不一致。
  • Thread.suspend()/resume():易导致死锁。

并发安全

JUC 包类

线程池

  • ThreadPoolExecutor:最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。
  • Executors:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如 newFixedThreadPool(创建固定线程数的线程池)、newCachedThreadPool(创建可缓存线程池)、newSingleThreadExecutor(创建单线程线程池)等,方便开发者快速创建线程池。

并发集合类

  • ConcurrentHashMap:线程安全的哈希映射表,用于在多线程环境下高效地存储和访问键值对。它采用了分段锁等技术,允许多个线程同时访问不同的段,提高了并发性能,在高并发场景下比传统的 Hashtable 性能更好。
  • CopyOnWriteArrayList:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行,从而实现了读写分离,提高了并发读的性能,适用于读多写少的场景。

同步工具类

  • CountDownLatch:允许一个或多个线程等待其他一组线程完成操作后再继续执行。它通过一个计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调用 countDown 方法将计数器减一,当计数器为零时,等待的线程可以继续执行。常用于多个线程完成各自任务后,再进行汇总或下一步操作的场景。
  • CyclicBarrier:让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。与 CountDownLatch 不同的是,CyclicBarrier 可以重复使用,当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。适用于多个线程需要协同工作,在某个阶段完成后再一起进入下一个阶段的场景。
  • Semaphore:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。

原子类

  • AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。
  • AtomicReference:原子引用类,用于对对象引用进行原子操作。可以保证在多线程环境下,对对象的更新操作是原子性的,即要么全部成功,要么全部失败,不会出现数据不一致的情况。常用于实现无锁数据结构或需要对对象进行原子更新的场景。

保证多线程安全

synchronized 关键字

可以使用 synchronized 关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问这些代码。对象锁是通过 synchronized 关键字锁定对象的监视器(monitor)来实现的。

public synchronized void someMethod() { /* ... */ }

public void anotherMethod() {
    synchronized (someObject) {
        /* ... */
    }
}

volatile 关键字

volatile 关键字用于变量,确保所有线程看到的是该变量的最新值,而不是可能存储在本地寄存器中的副本。

public volatile int sharedVariable;

Lock 接口和 ReentrantLock 类

java.util.concurrent.locks.Lock 接口提供了比 synchronized 更强大的锁定机制,ReentrantLock 是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。

private final ReentrantLock lock = new ReentrantLock();

public void someMethod() {
    lock.lock();
    try {
        /* ... */
    } finally {
        lock.unlock();
    }
}

原子类

Java 并发库(java.util.concurrent.atomic)提供了原子类,如 AtomicIntegerAtomicLong 等,这些类提供了原子操作,可以用于更新基本类型的变量而无需额外的同步。示例:

AtomicInteger counter = new AtomicInteger(0);
int newValue = counter.incrementAndGet();

线程局部变量

ThreadLocal 类可以为每个线程提供独立的变量副本,这样每个线程都拥有自己的变量,消除了竞争条件。

ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();
threadLocalVar.set(10);
int value = threadLocalVar.get();

并发集合

使用 java.util.concurrent 包中的线程安全集合,如 ConcurrentHashMapConcurrentLinkedQueue 等,这些集合内部已经实现了线程安全的逻辑。

JUC 工具类

使用 java.util.concurrent 包中的一些工具类可以用于控制线程间的同步和协作。例如:SemaphoreCyclicBarrier 等。

常用锁

内置锁(synchronized)

Java 中的 synchronized 关键字是内置锁机制的基础,可以用于方法或代码块。当一个线程进入 synchronized 代码块或方法时,它会获取关联对象的锁;当线程离开该代码块或方法时,锁会被释放。如果其他线程尝试获取同一个对象的锁,它们将被阻塞,直到锁被释放。其中,syncronized 加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。偏向锁用于当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁,以减少锁的开销。轻量级锁使用线程栈上的数据结构,避免了操作系统级别的锁。重量级锁则涉及操作系统级的互斥锁。

ReentrantLock

java.util.concurrent.locks.ReentrantLock 是一个显式的锁类,提供了比 synchronized 更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock 使用 lock()unlock() 方法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿。

读写锁(ReadWriteLock)

java.util.concurrent.locks.ReadWriteLock 接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。

乐观锁和悲观锁

悲观锁(Pessimistic Locking)通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。synchronizedReentrantLock 都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现。

自旋锁

自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃 CPU 并阻塞。通常可以使用 CAS 来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费 CPU 资源。

Java 并发工具

Java 中一些常用的并发工具,它们位于 java.util.concurrent 包中

CountDownLatch

CountDownLatch 是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。它使用一个计数器进行初始化,调用 countDown() 方法会使计数器减一,当计数器的值减为 0 时,等待的线程会被唤醒。可以把它想象成一个倒计时器,当倒计时结束(计数器为 0)时,等待的事件就会发生。示例代码:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int numberOfThreads = 3;
        CountDownLatch latch = new CountDownLatch(numberOfThreads);

        // 创建并启动三个工作线程
        for (int i = 0; i < numberOfThreads; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 正在工作");
                try {
                    Thread.sleep(1000);  // 模拟工作时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                latch.countDown();  // 完成工作,计数器减一
                System.out.println(Thread.currentThread().getName() + " 完成工作");
            }).start();
        }

        System.out.println("主线程等待工作线程完成");
        latch.await();  // 主线程等待,直到计数器为 0
        System.out.println("所有工作线程已完成,主线程继续执行");
    }
}

CyclicBarrier

CyclicBarrier 允许一组线程互相等待,直到到达一个公共的屏障点。当所有线程都到达这个屏障点后,它们可以继续执行后续操作,并且这个屏障可以被重置循环使用。与 CountDownLatch 不同,CyclicBarrier 侧重于线程间的相互等待,而不是等待某些操作完成。示例代码:

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int numberOfThreads = 3;
        CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {
            System.out.println("所有线程都到达了屏障,继续执行后续操作");
        });

        for (int i = 0; i < numberOfThreads; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 正在运行");
                    Thread.sleep(1000);  // 模拟运行时间
                    barrier.await();  // 等待其他线程
                    System.out.println(Thread.currentThread().getName() + " 已经通过屏障");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Semaphore

Semaphore 是一个计数信号量,用于控制同时访问某个共享资源的线程数量。通过 acquire() 方法获取许可,使用 release() 方法释放许可。如果没有许可可用,线程将被阻塞,直到有许可被释放。可以用来限制对某些资源(如数据库连接池、文件操作等)的并发访问量。代码如下:

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);  // 允许 2 个线程同时访问

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();  // 获取许可
                    System.out.println(Thread.currentThread().getName() + " 获得了许可");
                    Thread.sleep(2000);  // 模拟资源使用
                    System.out.println(Thread.currentThread().getName() + " 释放了许可");
                    semaphore.release();  // 释放许可
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Future 和 Callable

Callable 是一个类似于 Runnable 的接口,但它可以返回结果,并且可以抛出异常。Future 用于表示一个异步计算的结果,可以通过它来获取 Callable 任务的执行结果或取消任务。代码如下:

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

public class FutureCallableExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        Callable<Integer> callable = () -> {
            System.out.println(Thread.currentThread().getName() + " 开始执行 Callable 任务");
            Thread.sleep(2000);  // 模拟耗时操作
            return 42;  // 返回结果
        };

        Future<Integer> future = executorService.submit(callable);
        System.out.println("主线程继续执行其他任务");

        try {
            Integer result = future.get();  // 等待 Callable 任务完成并获取结果
            System.out.println("Callable 任务的结果: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }

        executorService.shutdown();
    }
}

ConcurrentHashMap

ConcurrentHashMap 是一个线程安全的哈希表,它允许多个线程同时进行读操作,在一定程度上支持并发的修改操作,避免了 HashMap 在多线程环境下需要使用 synchronized 或 Collections.synchronizedMap() 进行同步的性能问题。代码如下:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key1", 1);
        map.put("key2", 2);

        // 并发读操作
        map.forEach((key, value) -> System.out.println(key + ": " + value));

        // 并发写操作
        map.computeIfAbsent("key3", k -> 3);
    }
}

synchronized 工作原理

synchronized 是 Java 提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用 synchronized 之后,会在编译之后在同步的代码块前后加上 monitorenter 和 monitorexit 字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。

执行 monitorenter 指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器 +1。此时其他竞争锁的线程则会进入等待队列中。执行 monitorexit 指令时则会把计数器 -1,当计数器值为 0 时,则锁释放,处于等待队列中的线程再继续竞争锁。

synchronized 是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于 Java 中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。

从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。

实际上大部分时候我认为说到 monitorenter 就行了,但是为了更清楚的描述,还是再具体一点。

如果再深入到源码来说,synchronized 实际上有两个队列 waitSet 和 entryList。

  1. 当多个线程进入同步代码块时,首先进入 entryList
  2. 有一个线程获取到 monitor 锁后,就赋值给当前线程,并且计数器 +1
  3. 如果线程调用 wait 方法,将释放锁,当前线程置为 null,计数器 -1,同时进入 waitSet 等待被唤醒,调用 notify 或者 notifyAll 之后又会进入 entryList 竞争锁
  4. 如果线程执行完毕,同样释放锁,计数器 -1,当前线程置为 null

Reentrantlock 工作原理

ReentrantLock 的底层实现主要依赖于 AbstractQueuedSynchronizer(AQS)这个抽象类。AQS 是一个提供了基本同步机制的框架,其中包括了队列、状态值等。

ReentrantLock 在 AQS 的基础上通过内部类 Sync 来实现具体的锁操作。不同的 Sync 子类实现了公平锁和非公平锁的不同逻辑:

  • 可中断性: ReentrantLock 实现了可中断性,这意味着线程在等待锁的过程中,可以被其他线程中断而提前结束等待。在底层,ReentrantLock 使用了与 LockSupport.park() 和 LockSupport.unpark() 相关的机制来实现可中断性。
  • 设置超时时间: ReentrantLock 支持在尝试获取锁时设置超时时间,即等待一定时间后如果还未获得锁,则放弃锁的获取。这是通过内部的 tryAcquireNanos 方法来实现的。
  • 公平锁和非公平锁: 在直接创建 ReentrantLock 对象时,默认情况下是非公平锁。公平锁是按照线程等待的顺序来获取锁,而非公平锁则允许多个线程在同一时刻竞争锁,不考虑它们申请锁的顺序。公平锁可以通过在创建 ReentrantLock 时传入 true 来设置,例如:
ReentrantLock fairLock = new ReentrantLock(true);
  • 多个条件变量: ReentrantLock 支持多个条件变量,每个条件变量可以与一个 ReentrantLock 关联。这使得线程可以更灵活地进行等待和唤醒操作,而不仅仅是基于对象监视器的 wait() 和 notify()。多个条件变量的实现依赖于 Condition 接口,例如:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 使用下面方法进行等待和唤醒
condition.await();
condition.signal();
  • 可重入性: ReentrantLock 支持可重入性,即同一个线程可以多次获得同一把锁,而不会造成死锁。这是通过内部的 holdCount 计数来实现的。当一个线程多次获取锁时,holdCount 递增,释放锁时递减,只有当 holdCount 为零时,其他线程才有机会获取锁。

synchronized 和 ReentrantLock 应用场景

synchronized:

  • 简单同步需求: 当你需要对代码块或方法进行简单的同步控制时,synchronized 是一个很好的选择。它使用起来简单,不需要额外的资源管理,因为锁会在方法退出或代码块执行完毕后自动释放。
  • 代码块同步: 如果你想对特定代码段进行同步,而不是整个方法,可以使用 synchronized 代码块。这可以让你更精细地控制同步的范围,从而减少锁的持有时间,提高并发性能。
  • 内置锁的使用: synchronized 关键字使用对象的内置锁(也称为监视器锁),这在需要使用对象作为锁对象的情况下很有用,尤其是在对象状态与锁保护的代码紧密相关时。 ReentrantLock:
  • 高级锁功能需求: ReentrantLock 提供了 synchronized 所不具备的高级功能,如公平锁、响应中断、定时锁尝试、以及多个条件变量。当你需要这些功能时,ReentrantLock 是更好的选择。
  • 性能优化: 在高度竞争的环境中,ReentrantLock 可以提供比 synchronized 更好的性能,因为它提供了更细粒度的控制,如尝试锁定和定时锁定,可以减少线程阻塞的可能性。
  • 复杂同步结构: 当你需要更复杂的同步结构,如需要多个条件变量来协调线程之间的通信时,ReentrantLock 及其配套的 Condition 对象可以提供更灵活的解决方案。 综上,synchronized 适用于简单同步需求和不需要额外锁功能的场景,而 ReentrantLock 适用于需要更高级锁功能、性能优化或复杂同步逻辑的情况。选择哪种同步机制取决于具体的应用需求和性能考虑。

syncronized 锁升级

无锁 ->偏向锁 ->轻量级锁 ->重量级锁。

无锁

这是没有开启偏向锁的时候的状态,在 JDK1.6 之后偏向锁的默认开启的,但是有一个偏向延迟,需要在 JVM 启动之后的多少秒之后才能开启,这个可以通过 JVM 参数进行设置,同时是否开启偏向锁也可以通过 JVM 参数设置。

偏向锁

这个是在偏向锁开启之后的锁的状态,如果还没有一个线程拿到这个锁的话,这个状态叫做匿名偏向,当一个线程拿到偏向锁的时候,下次想要竞争锁只需要拿线程 ID 跟 MarkWord 当中存储的线程 ID 进行比较,如果线程 ID 相同则直接获取锁(相当于锁偏向于这个线程),不需要进行 CAS 操作和将线程挂起的操作。

轻量级锁

在这个状态下线程主要是通过 CAS 操作实现的。将对象的 MarkWord 存储到线程的虚拟机栈上,然后通过 CAS 将对象的 MarkWord 的内容设置为指向 Displaced Mark Word 的指针,如果设置成功则获取锁。在线程出临界区的时候,也需要使用 CAS,如果使用 CAS 替换成功则同步成功,如果失败表示有其他线程在获取锁,那么就需要在释放锁之后将被挂起的线程唤醒。

重量级锁

当有两个以上的线程获取锁的时候轻量级锁就会升级为重量级锁,因为 CAS 如果没有成功的话始终都在自旋,进行 while 循环操作,这是非常消耗 CPU 的,但是在升级为重量级锁之后,线程会被操作系统调度然后挂起,这可以节约 CPU 资源。

image.png 线程 A 进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word 中存储的线程 ID 来判断,当前线程 A 是否就是持有偏向锁的线程。如果是,则忽略 check,线程 A 直接执行临界区内的代码。

但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的; 如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁。

后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞。

JVM 对 Synchornized 的优化?

锁膨胀

synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。

锁消除

指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。

锁粗化

将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

自适应自旋锁

指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

AQS

AQS 全称为 AbstractQueuedSynchronizer,是 Java 中的一个抽象类。 AQS 是一个用于构建锁、同步器、协作工具类的工具类(框架)。

AQS 核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是 CLH 队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

CLH:Craig、Landin and Hagersten 队列,是单向链表,AQS 中的队列是 CLH 变体的虚拟双向队列(FIFO),AQS 是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

主要原理图如下: image.png

AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。AQS 广泛用于控制并发流程的类,如下图:

其中 Sync 是这些类中都有的内部类,其结构如下:

可以看到:SyncAQS 的实现。 AQS 主要完成的任务:

  • 同步状态(比如说计数器)的原子性管理;
  • 线程的阻塞和解除阻塞;
  • 队列的管理。 AQS 最核心的就是三大部分:
  • 状态:state;
    • 这里 state 的具体含义,会根据具体实现类的不同而不同:比如在 Semapore 里,他表示剩余许可证的数量;在 CountDownLatch 里,它表示还需要倒数的数量;在 ReentrantLock 中,state 用来表示“锁”的占有情况,包括可重入计数,当 state 的值为 0 的时候,标识该 Lock 不被任何线程所占有。
    • state 是 volatile 修饰的,并被并发修改,所以修改 state 的方法都需要保证线程安全,比如 getState、setState 以及 compareAndSetState 操作来读取和更新这个状态。这些方法都依赖于 unsafe 类。
  • 控制线程抢锁和配合的 FIFO 队列(双向链表);
    • 这个队列用来存放“等待的线程,AQS 就是“排队管理器”,当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁。
    • AQS 会维护一个等待的线程队列,把线程都放到这个队列里,这个队列是双向链表形式。
  • 期望协作工具类去实现的获取/释放等重要方法(重写)。
    • 这里的获取和释放方法,是利用 AQS 的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同;
    • 获取方法:获取操作会以来 state 变量,经常会阻塞(比如获取不到锁的时候)。在 Semaphore 中,获取就是 acquire 方法,作用是获取一个许可证; 而在 CountDownLatch 里面,获取就是 await 方法,作用是等待,直到倒数结束;
    • 释放方法:在 Semaphore 中,释放就是 release 方法,作用是释放一个许可证; 在 CountDownLatch 里面,获取就是 countDown 方法,作用是将倒数的数减一;
    • 需要每个实现类重写 tryAcquire 和 tryRelease 等方法。 CAS 是一种乐观锁机制,AOS 是用于构建锁和同步器的框架。CAS 为 AQS 提供原子操作

用 AQS 实现一个可重入的公平锁?

AQS 实现一个可重入的公平锁的详细步骤:

  1. 继承 AbstractQueuedSynchronizer:创建一个内部类继承自 AbstractQueuedSynchronizer,重写 tryAcquiretryReleaseisHeldExclusively 等方法,这些方法将用于实现锁的获取、释放和判断锁是否被当前线程持有。
  2. 实现可重入逻辑:在 tryAcquire 方法中,检查当前线程是否已经持有锁,如果是,则增加锁的持有次数(通过 state 变量);如果不是,尝试使用 CAS 操作来获取锁。
  3. 实现公平性:在 tryAcquire 方法中,按照队列顺序来获取锁,即先检查等待队列中是否有线程在等待,如果有,当前线程必须进入队列等待,而不是直接竞争锁。
  4. 创建锁的外部类:创建一个外部类,内部持有 AbstractQueuedSynchronizer 的子类对象,并提供 lock 和 unlock 方法,这些方法将调用 AbstractQueuedSynchronizer 子类中的方法。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class FairReentrantLock {
    
    private static class Sync extends AbstractQueuedSynchronizer {
    
        // 判断锁是否被当前线程持有
        protected boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        // 尝试获取锁
        protected boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 公平性检查:检查队列中是否有前驱节点,如果有,则当前线程不能获取锁
                if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            } else if (current == getExclusiveOwnerThread()) {
                // 可重入逻辑:如果是当前线程持有锁,则增加持有次数
                int nextc = c + acquires;
                if (nextc < 0) {
                    throw new Error("Maximum lock count exceeded");
                }
                setState(nextc);
                return true;
            }
            return false;
        }

        // 尝试释放锁
        protected boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread()!= getExclusiveOwnerThread()) {
                throw new IllegalMonitorStateException();
            }
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        // 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
        ConditionObject newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    // 加锁方法
    public void lock() {
        sync.acquire(1);
    }

    // 解锁方法
    public void unlock() {
        sync.release(1);
    }

    // 判断当前线程是否持有锁
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    // 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
    public Condition newCondition() {
        return sync.newCondition();
    }
}

内部类 Sync:

  • isHeldExclusively:使用 getExclusiveOwnerThread 方法检查当前锁是否被当前线程持有。
  • tryAcquire
    • 首先获取当前锁的状态 c
    • 如果 c 为 0,表示锁未被持有,此时进行公平性检查,通过 hasQueuedPredecessors 检查是否有前驱节点在等待队列中。如果没有,使用 compareAndSetState 尝试将状态设置为 acquires(通常为 1),并设置当前线程为锁的持有线程。
    • 如果 c 不为 0,说明锁已被持有,检查是否为当前线程持有。如果是,增加锁的持有次数(可重入),但要防止溢出。
  • tryRelease
    • 先将状态减 releases(通常为 1)。
    • 检查当前线程是否为锁的持有线程,如果不是,抛出异常。
    • 如果状态减为 0,说明锁被完全释放,将持有线程设为 null
  • newCondition:创建一个 ConditionObject 用于更复杂的同步操作,如等待 / 通知机制。 外部类 FairReentrantLock:
  • lock 方法:调用 sync.acquire(1) 尝试获取锁。
  • unlock 方法:调用 sync.release(1) 释放锁。
  • isLocked 方法:调用 sync.isHeldExclusively 判断锁是否被当前线程持有。
  • newCondition 方法:调用 sync.newCondition 提供条件变量。

ThreadLocal

3 Java 并发编程_1764300559385

  • Thread 类中,有个 ThreadLocal.ThreadLocalMap 的成员变量。
  • ThreadLocalMap 内部维护了 Entry 数组,每个 Entry 代表一个完整的对象,keyThreadLocal本身,value 是 ThreadLocal 的泛型对象值。 ThreadLocal 的作用
  • 线程隔离:ThreadLocal 为每个线程提供了独立的变量副本,这意味着线程之间不会相互影响,可以安全地在多线程环境中使用这些变量而不必担心数据竞争或同步问题。
  • 降低耦合度:在同一个线程内的多个函数或组件之间,使用 ThreadLocal 可以减少参数的传递,降低代码之间的耦合度,使代码更加清晰和模块化。
  • 性能优势:由于 ThreadLocal 避免了线程间的同步开销,所以在大量线程并发执行时,相比传统的锁机制,它可以提供更好的性能。 ThreadLocal 的原理 ThreadLocal 的实现依赖于 Thread 类中的一个 ThreadLocalMap 字段,这是一个存储 ThreadLocal 变量本身和对应值的映射。每个线程都有自己的 ThreadLocalMap 实例,用于存储该线程所持有的所有 ThreadLocal 变量的值。当你创建一个 ThreadLocal 变量时,它实际上就是一个 ThreadLocal 对象的实例。每个 ThreadLocal 对象都可以存储任意类型的值,这个值对每个线程来说是独立的。
  • 当调用 ThreadLocalget() 方法时,ThreadLocal 会检查当前线程的 ThreadLocalMap 中是否有与之关联的值。
  • 如果有,返回该值;
  • 如果没有,会调用 initialValue() 方法(如果重写了的话)来初始化该值,然后将其放入 ThreadLocalMap 中并返回。
  • 当调用 set() 方法时,ThreadLocal 会将给定的值与当前线程关联起来,即在当前线程的 ThreadLocalMap 中存储一个键值对,键是 ThreadLocal 对象自身,值是传入的值。
  • 当调用 remove() 方法时,会从当前线程的 ThreadLocalMap 中移除与该 ThreadLocal 对象关联的条目。 可能存在的问题 当一个线程结束时,其 ThreadLocalMap 也会随之销毁,但是 ThreadLocal 对象本身不会立即被垃圾回收,直到没有其他引用指向它为止。因此,在使用 ThreadLocal 时需要注意,如果不显式调用 remove() 方法,或者线程结束时未正确清理 ThreadLocal 变量,可能会导致内存泄漏,因为 ThreadLocalMap 会持续持有 ThreadLocal 变量的引用,即使这些变量不再被其他地方引用。 因此,实际应用中需要在使用完 ThreadLocal 变量后调用 remove() 方法释放资源

乐观锁实现方式

CAS(Compare and Swap)

CAS 是乐观锁的基础。Java 提供了 java.util.concurrent.atomic 包,包含各种原子变量类(如 AtomicInteger、AtomicLong),这些类使用 CAS 操作实现了线程安全的原子操作,可以用来实现乐观锁。

版本号控制

增加一个版本号字段记录数据更新时候的版本,每次更新时递增版本号。在更新数据时,同时比较版本号,若当前版本号和更新前获取的版本号一致,则更新成功,否则失败。

时间戳

使用时间戳记录数据的更新时间,在更新数据时,在比较时间戳。如果当前时间戳大于数据的时间戳,则说明数据已经被其他线程更新,更新失败。

CAS

缺点:CAS 操作是基于循环重试的机制,如果 CAS 操作一直未能成功,线程会一直自旋重试,占用 CPU 资源。在高并发情况下,大量线程自旋会导致 CPU 资源浪费。

ABA 问题

变量值在操作过程中先被其他线程从 A 修改为 B,又被改回 A,CAS 无法感知中途变化,导致操作误判为“未变更”

Java 提供的工具类会在 CAS 操作中增加版本号(Stamp)或标记,每次修改都更新版本号,使得即使值相同也能识别变更历史。比如,可以用 AtomicStampedReference 来解决 ABA 问题,通过比对值版本号识别 ABA 问题。

AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);

// 尝试修改值并更新版本号
boolean success = ref.compareAndSet(100, 200, 0, 1); 
// 前提:当前值=100 且 版本号=0,才会更新为(200,1)

voliatle 作用

保证变量对所有线程的可见性

当一个变量被声明为 volatile 时,它会保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取,从而确保了多线程环境下对该变量访问的可见性。这意味着一个线程修改了 volatile 变量的值,其他线程能够立刻看到这个修改,不会受到各自线程工作内存的影响。

禁止指令重排序优化

volatile 关键字在 Java 中主要通过内存屏障来禁止特定类型的指令重排序。

  • 写 - 写(Write-Write)屏障:在对 volatile 变量执行写操作之前,会插入一个写屏障。这确保了在该变量写操作之前的所有普通写操作都已完成,防止了这些写操作被移到 volatile 写操作之后。
  • 读 - 写(Read-Write)屏障:在对 volatile 变量执行读操作之后,会插入一个读屏障。它确保了对 volatile 变量的读操作之后的所有普通读操作都不会被提前到 volatile 读之前执行,保证了读取到的数据是最新的。
  • 写 - 读(Write-Read)屏障:这是最重要的一个屏障,它发生在 volatile 写之后和 volatile 读之前。这个屏障确保了 volatile 写操作之前的所有内存操作(包括写操作)都不会被重排序到 volatile 读之后,同时也确保了 volatile 读操作之后的所有内存操作(包括读操作)都不会被重排序到 volatile 写之前。

volatile 可以保证线程安全吗?

volatile 关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。volatile 关键字用于修饰变量,当一个线程修改了 volatile 修饰的变量的值,其他线程能够立即看到最新的值,从而避免了线程之间的数据不一致。

但是,volatile 并不能解决多线程并发下的复合操作问题,比如 i++ 这种操作不是原子操作,如果多个线程同时对 i 进行自增操作,volatile 不能保证线程安全。对于复合操作,需要使用 synchronized 关键字或者 Lock 来保证原子性和线程安全。

volatile 和 sychronized 比较

Synchronized 解决了多线程访问共享资源时可能出现的竞态条件和数据不一致的问题,保证了线程安全性。Volatile 解决了变量在多线程环境下的可见性和有序性问题,确保了变量的修改对其他线程是可见的。

  • Synchronized: Synchronized 是一种排他性的同步机制,保证了多个线程访问共享资源时的互斥性,即同一时刻只允许一个线程访问共享资源。通过对代码块或方法添加 Synchronized 关键字来实现同步。
  • Volatile: Volatile 是一种轻量级的同步机制,用来保证变量的可见性和禁止指令重排序。当一个变量被声明为 Volatile 时,线程在读取该变量时会直接从内存中读取,而不会使用缓存,同时对该变量的写操作会立即刷回主内存,而不是缓存在本地内存中。

非公平锁吞吐量为什么比公平锁大?

  • 公平锁执行流程:获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
  • 非公平锁执行流程:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。 Synchronized 不属于公平锁,ReentrantLock 是公平锁。

ReentrantLock 是怎么实现公平锁的?

我们来看一下公平锁与非公平锁的加锁方法的源码。公平锁的锁获取源码如下:

protected final boolean tryAcquire(int acquires) {

    final Thread current = Thread.currentThread();
    int c = getState();

    if (c == 0) {

        if (!hasQueuedPredecessors() && //这里判断了 hasQueuedPredecessors()
                compareAndSetState(0, acquires)) {
            
            setExclusiveOwnerThread(current);
            
            return true;
        }

    } else if (current == getExclusiveOwnerThread()) {

        int nextc = c + acquires;

        if (nextc < 0) {
            throw new Error("Maximum lock count exceeded");
        }
        setState(nextc);
        return true;

    }
    return false;
}

非公平锁的锁获取源码如下:

final boolean nonfairTryAcquire(int acquires) {

    final Thread current = Thread.currentThread();
    int c = getState();

    if (c == 0) {

        if (compareAndSetState(0, acquires)) { //这里没有判断      hasQueuedPredecessors()

            setExclusiveOwnerThread(current);

            return true;

        }

    }

    else if (current == getExclusiveOwnerThread()) {

        int nextc = c + acquires;

        if (nextc < 0) // overflow

        throw new Error("Maximum lock count exceeded");

        setState(nextc);

        return true;

    }

    return false;

}

通过对比,我们可以明显的看出公平锁与非公平锁的 lock() 方法唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors() 为 false,这个方法就是判断在等待队列中是否已经有线程在排队了。

这也就是公平锁和非公平锁的核心区别,如果是公平锁,那么一旦已经有线程在排队了,当前线程就不再尝试获取锁;对于非公平锁而言,无论是否已经有线程在排队,都会尝试获取一下锁,获取不到的话,再去排队。这里有一个特例需要我们注意,针对 tryLock() 方法,它不遵守设定的公平原则。

例如,当有线程执行 tryLock() 方法的时候,一旦有线程释放了锁,那么这个正在 tryLock 的线程就能获取到锁,即使设置的是公平锁模式,即使在它之前已经有其他正在等待队列中等待的线程,简单地说就是 tryLock 可以插队。看它的源码就会发现:

public boolean tryLock() {

    return sync.nonfairTryAcquire(1);

}

这里调用的就是 nonfairTryAcquire(),表明了是不公平的,和锁本身是否是公平锁无关。综上所述,公平锁就是会按照多个线程申请锁的顺序来获取锁,从而实现公平的特性。

非公平锁加锁时不考虑排队等待情况,直接尝试获取锁,所以存在后申请却先获得锁的情况,但由此也提高了整体的效率。

死锁

死锁条件

  • 互斥条件:互斥条件是指多个线程不能同时使用同一个资源
  • 持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1
  • 不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
  • 环路等待条件:环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。 避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件:线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。

线程池

线程池怎么使用

线程池的核心作用是复用线程、控制并发数,避免频繁创建销毁线程带来的性能开销。

首先最常用的是通过 Executors 工具类快速创建线程池,适合简单场景,不用手动配置复杂参数。比如需要一个固定线程数的线程池,处理一批异步任务:

// 创建固定3个线程的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

// 提交Runnable任务 无返回值
for (int i = 0; i < 5; i++) {
    int taskNum = i;
    fixedThreadPool.submit(() -> {
        System.out.println(当前线程  + Thread.currentThread().getName() +  处理任务 + taskNum);
        try {
            Thread.sleep(1000); // 模拟任务执行耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

// 提交Callable任务 有返回值
Future<Integer> future = fixedThreadPool.submit(() -> {
    Thread.sleep(500);
    return100 + 200; // 任务返回结果
});

// 获取Callable任务的返回值 会阻塞直到任务完成
try {
    Integer result = future.get();
    System.out.println(任务返回结果 + result);
} catch (Exception e) {
    e.printStackTrace();
}

// 任务执行完后关闭线程池 避免资源泄露
fixedThreadPool.shutdown();

不过 Executors 创建的线程池有潜在问题,比如 newCachedThreadPool 可能创建大量线程导致 OOM,newScheduledThreadPool 的核心线程数默认无界,所以复杂业务场景更推荐用 ThreadPoolExecutor 手动配置,能精准控制线程池行为。

手动创建 ThreadPoolExecutor 需要指定 7 个核心参数,理解这些参数才能用好。比如创建一个适合处理 IO 密集型任务的线程池(IO 密集型任务线程数可以多些,一般是 CPU 核心数的 2 倍):

// 获取CPU核心数 用于合理设置线程数
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;

// 手动配置线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    corePoolSize, // 核心线程数 线程池长期维持的最小线程数
    corePoolSize * 2, // 最大线程数 线程池能容纳的最多线程数
    60L, // 空闲线程存活时间 超过核心线程数的空闲线程 多久后销毁
    TimeUnit.SECONDS, // 存活时间单位
    new ArrayBlockingQueue<>(100), // 任务阻塞队列 核心线程忙时 新任务存这里
    Executors.defaultThreadFactory(), // 线程创建工厂 用于设置线程名 优先级等
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 队列满且线程数达最大时 如何处理新任务
);

// 提交任务的两种方式和之前一致 这里用execute提交Runnable(无返回值 不能捕获异常)
threadPool.execute(() -> {
    System.out.println(IO任务执行中 + Thread.currentThread().getName());
    // 模拟IO操作 比如数据库查询 网络请求
});

// 关闭线程池 推荐用shutdown 等待已提交任务完成后再关闭
threadPool.shutdown();
// 若需要强制关闭 可调用shutdownNow 会中断正在执行的任务 返回未执行的任务
// List<Runnable> unExecutedTasks = threadPool.shutdownNow();

这里要注意几个关键细节:比如拒绝策略有四种,除了默认的 AbortPolicy(直接抛异常),还有 CallerRunsPolicy(让提交任务的主线程自己执行,缓解压力)、DiscardPolicy(直接丢弃新任务)、DiscardOldestPolicy(丢弃队列里最旧的任务,再提交新任务),要根据业务选择,比如核心业务不能丢任务,就别用 Discard 相关策略。

另外,提交任务时,submit 和 execute 的区别在于 submit 能提交 Callable 有返回值,还能通过 Future 捕获任务执行中的异常,而 execute 只能提交 Runnable,异常会直接抛出,比如:

// submit捕获异常
Future<?> future = threadPool.submit(() -> {
    int i = 1 / 0; // 模拟异常
});
try {
    future.get();
} catch (ExecutionException e) {
    System.out.println(任务执行异常 + e.getCause()); // 捕获到算术异常
}

还有线程池的使用原则:不能创建后不关闭,否则会导致线程泄露,JVM 无法退出;任务队列的容量要合理设置,太大可能导致内存溢出,太小容易触发拒绝策略;线程数要根据任务类型调整,CPU 密集型任务(比如复杂计算)线程数不宜过多,一般和 CPU 核心数相当,避免线程切换开销,IO 密集型任务可以多设些线程,因为线程大部分时间在等待 IO 完成

线程池工作原理

3 Java 并发编程_1764309429282

线程池参数

  • corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
  • maximumPoolSize:限制了线程池能创建的最大线程总数(包括核心线程和非核心线程),当 corePoolSize 已满 并且 尝试将新任务加入阻塞队列失败(即队列已满)并且 当前线程数 < maximumPoolSize,就会创建新线程执行此任务,但是当 corePoolSize 满 并且 队列满 并且 线程数已达 maximumPoolSize 并且 又有新任务提交时,就会触发拒绝策略。
提交新任务
      |
      v
当前线程数 < corePoolSize?  ---是---> 创建新线程执行此任务
      |

      |    尝试将任务加入工作队列
      |          |
      |<---成功加入?--是---> 任务排队等待执行 (结束,不创建新线程)
      |          |
      |         否 (队列已满)
      |
      v
当前线程数 < maximumPoolSize?  ---是---> 创建新线程执行此任务 (注意:是执行刚提交的这个任务!)
      |

      |
      v
执行拒绝策略
  • keepAliveTime:当线程池中线程的数量大于 corePoolSize,并且某个线程的空闲时间超过了 keepAliveTime,那么这个线程就会被销毁。
  • unit:就是 keepAliveTime 时间的单位。
  • workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
  • threadFactory:线程工厂。可以用来给线程取名字等等
  • handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于 maximumPoolSize,就不会创建新线程,就会去执行拒绝策略

线程池参数设置

核心线程数(corePoolSize)设置的经验:

  • CPU 密集型:corePoolSize = CPU 核数 + 1(避免过多线程竞争 CPU)
  • IO 密集型:corePoolSize = CPU 核数 x 2(或更高,具体看 IO 等待时间) 场景一:电商场景,特点瞬时高并发、任务处理时间短,线程池的配置可设置如下:
new ThreadPoolExecutor(
    16,                     // corePoolSize = 16(假设8核CPU × 2)
    32,                     // maximumPoolSize = 32(突发流量扩容)
    10, TimeUnit.SECONDS,   // 非核心线程空闲10秒回收
    new SynchronousQueue<>(), // 不缓存任务,直接扩容线程
    new AbortPolicy()       // 直接拒绝,避免系统过载
);
  • 使用 SynchronousQueue 确保任务直达线程,避免队列延迟。
  • 拒绝策略快速失败,前端返回“活动火爆”提示,结合降级策略(如缓存预热)。 场景二:后台数据处理服务,特点稳定流量、任务处理时间长(秒级)、允许一定延迟,线程池的配置可设置如下:
new ThreadPoolExecutor(
    8,                      // corePoolSize = 8(8核CPU)
    8,                      // maximumPoolSize = 8(禁止扩容,避免资源耗尽)
    0, TimeUnit.SECONDS,    // 不回收线程
    new ArrayBlockingQueue<>(1000), // 有界队列,容量1000
    new CallerRunsPolicy()  // 队列满后由调用线程执行
);
  • 固定线程数避免资源波动,队列缓冲任务,拒绝策略兜底。
  • 配合监控告警(如队列使用率>80% 触发扩容)。 场景三:微服务 HTTP 请求处理,特点 IO 密集型、依赖下游服务响应时间,线程池的配置可设置如下:
new ThreadPoolExecutor(
    16,                     // corePoolSize = 16(8核 × 2)
    64,                     // maximumPoolSize = 64(应对慢下游)
    60, TimeUnit.SECONDS,   // 非核心线程空闲60秒回收
    new LinkedBlockingQueue<>(200), // 有界队列容量200
    new CustomRetryPolicy() // 自定义拒绝策略(重试或降级)
);
  • 根据下游 RT(响应时间)调整线程数,队列防止瞬时峰值。
  • 自定义拒绝策略将任务暂存 Redis,异步重试。

线程池种类

  • ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。
  • FixedThreadPool:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。
  • CachedThreadPool:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为 0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。
  • SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。
  • SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。

场景

单例模型既然已经用了 synchronized,为什么还要在加 volatile?

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

synchronized 关键字的作用用于确保在多线程环境下,只有一个线程能够进入同步块(这里是 synchronized (Singleton.class))。在创建单例对象时,通过 synchronized 保证了创建过程的线程安全性,避免多个线程同时创建多个单例对象。

volatile 确保了对象引用的可见性和创建过程的有序性,避免了由于指令重排序而导致的错误。

instance = new Singleton(); 这行代码并不是一个原子操作,它实际上可以分解为以下几个步骤:

  • 分配内存空间。
  • 实例化对象。
  • 将对象引用赋值给 instance。 由于 Java 内存模型允许编译器和处理器对指令进行重排序,在没有 volatile 的情况下,可能会出现重排序,例如先将对象引用赋值给 instance,但对象的实例化操作尚未完成。 这样,其他线程在检查 instance == null 时,会认为单例已经创建,从而得到一个未完全初始化的对象,导致错误。

3 个线程并发执行,1 个线程等待这三个线程全部执行完在执行,怎么实现?

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) {
        // 创建一个 CountDownLatch,初始计数为 3
        CountDownLatch latch = new CountDownLatch(3);

        // 创建并启动 3 个并发线程
        for (int i = 0; i < 3; i++) {
            final int threadNumber = i + 1;
            new Thread(() -> {
                try {
                    System.out.println("Thread " + threadNumber + " is working.");
                    // 模拟线程执行任务
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println("Thread " + threadNumber + " has finished.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 任务完成后,计数器减 1
                    latch.countDown();
                }
            }).start();
        }

        // 创建并启动第 4 个线程,等待其他 3 个线程完成
        new Thread(() -> {
            try {
                System.out.println("Waiting for other threads to finish.");
                // 等待计数器为 0
                latch.await();
                System.out.println("All threads have finished, this thread starts to work.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

3 个线程并发执行,1 个线程等待这三个线程全部执行完在执行,怎么实现?

可以使用 CountDownLatch 来实现 3 个线程并发执行,另一个线程等待这三个线程全部执行完再执行的需求。以下是具体的实现步骤:

  • 创建一个 CountDownLatch 对象,并将计数器初始化为 3,因为有 3 个线程需要等待。
  • 创建 3 个并发执行的线程,在每个线程的任务结束时调用 countDown 方法将计数器减 1。
  • 创建第 4 个线程,使用 await 方法等待计数器为 0,即等待其他 3 个线程完成任务。
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) {
        // 创建一个 CountDownLatch,初始计数为 3
        CountDownLatch latch = new CountDownLatch(3);

        // 创建并启动 3 个并发线程
        for (int i = 0; i < 3; i++) {
            final int threadNumber = i + 1;
            new Thread(() -> {
                try {
                    System.out.println("Thread " + threadNumber + " is working.");
                    // 模拟线程执行任务
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println("Thread " + threadNumber + " has finished.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 任务完成后,计数器减 1
                    latch.countDown();
                }
            }).start();
        }

        // 创建并启动第 4 个线程,等待其他 3 个线程完成
        new Thread(() -> {
            try {
                System.out.println("Waiting for other threads to finish.");
                // 等待计数器为 0
                latch.await();
                System.out.println("All threads have finished, this thread starts to work.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

代码解释

  • 首先,创建了一个 CountDownLatch 对象 latch,并将其初始计数设置为 3。
  • 然后,使用 for 循环创建并启动 3 个线程。每个线程会执行一些工作(这里使用 Thread.sleep 模拟),在工作完成后,会调用 latch.countDown() 方法,将 latch 的计数减 1。
  • 最后,创建第 4 个线程。这个线程在开始时调用 latch.await() 方法,它会阻塞,直到 latch 的计数为 0,即前面 3 个线程都调用了 countDown() 方法。一旦计数为 0,该线程将继续执行后续任务
← 返回 Notes

Contact

Contact Me

Leave a message here. The form sends directly from the browser to a form delivery service and then to my email.

Messages are delivered to lzx744008464@gmail.com.