[Java EE] 多线程进阶(JUC)(2)

三.JUC(java.util.concurrent) 常见类

Java 并发编程中的 JUC 是处理高并发场景的核心工具包 , 解决了原生 synchronized,wait/notify 等机制的局限性 , 包含了线程池 , 锁 , 原子锁 , 并发集合 , 同步器等核心组件

1. 原生并发编程的痛点:

  • Runnable 无返回值,无法抛出受检异常
  • synchronized 是隐式锁,无法中断、无法超时、只有非公平锁
  • Vector/Hashtable 等同步集合性能差
  • 手动管理线程(创建 / 销毁)开销大等

JUC 的核心目标 : 更灵活的同步控制、更高的并发性能、更简洁的异步编程

2.Callable 接口

Callable 是 JUC 包下的函数式接口 , 核心作用 : 定义有返回值 , 可抛异常的线程 ; 弥补了 Runnable 接口的不足 , 是多线程异常执行并获取结果的核心工具

示例:创建线程计算1+2+3+...+1000

① 如果不使用 callable:

java 复制代码
public class demo43 {
    private static volatile int result = 0;

    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker){
                for (int i = 1; i < 100; i++) {
                    result+=i;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                locker.notify();
            }

        });
        Thread t2 = new Thread(()->{
            synchronized (locker) {
                try {
                    while (result == 0) {
                        locker.wait();
                    }
                    System.out.println("result = " + result);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();//恢复线程中断状态
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }
}

java 复制代码
public class Demo {
    // 封装结果和锁对象的内部类
    static class Result {
        public int sum = 0;
        public Object lock = new Object();
    }

    public static void main(String[] args) throws InterruptedException {
        Result result = new Result();

        // 线程t:计算1-1000的累加和
        Thread t = new Thread() {
            @Override
            public void run() {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                // 必须在同步块内操作锁和结果
                synchronized (result.lock) {
                    result.sum = sum;
                    result.lock.notify(); // 持有锁时调用notify
                }
            }
        };
        t.start();

        // 主线程:等待计算完成,打印结果
        synchronized (result.lock) {
            // 循环判断(防虚假唤醒)+ 修正"=="
            while (result.sum == 0) {
                result.lock.wait();
            }
            System.out.println("1-1000的累加和:" + result.sum); // 输出500500
        }
    }
}

显然上述的代码操作复杂 , 容易出错

② 使用 Callable 的版本(优化)

  • 创建一个匿名内部类,实现 Callable 接口.Callable 带有泛型参数。泛型参数表示返回值的类型
  • 重写 Callable 的 call 方法,完成累加的过程。直接通过返回值返回计算结果
  • 把 callable 实例使用 FutureTask 包装一下
  • 创建线程,线程的构造方法传入 FutureTask. 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法,完成计算。计算结果就放到了 FutureTask 对象中
  • 在主线程中调用 futureTask.get () 能够阻塞等待新线程计算完毕。并获取到 FutureTask 中的结果
java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class demo42 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.此处Callable只是定义了一个带有返回值的任务 , 并没有在执行
        //执行还是需要搭配Thread对象
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for(int i = 1;i<100;i++){
                    result+=i;
                }
                return result;
            }
        };
        //2.封装为FutureTask
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //3.创建线程
        Thread t = new Thread(futureTask);
        //4.启动线程
        t.start();
        System.out.println(futureTask.get());//此处只有get执行完毕,才能拿到返回结果,可能会陷入阻塞
    }
}

3.ReentrantLock

可重入互斥锁, 和 synchronized 定位类似 , 都是用来实现互斥效果的 , 保证线程安全

①常用方法

|----------------------------------------------|------------------------------------------------------------------------------------------------------------|
| void lock() | 加锁;①若锁空闲,立即获取锁,锁计数器+1 ; ②若锁被占用,当时线程阻塞,直到获取锁 |
| void unlock() | 解锁;① 锁计数器-1 ; ②计数器归0,释放锁,唤醒等待队列线程**(必须在 finally 中调用)** |
| void lockInterruptibly() | 可中断锁;与 lock()逻辑一致 ; 等待锁是,线程若被中断,抛 InterruptedException(中断线程后需要恢复)(Thread.currentThread().interrupt();) |
| boolean tryLock() | 尝试加锁;①锁空闲->获取锁,返回true ; ② 锁被占用->立即返回 false |
| boolean tryLock(long timeout, TimeUnit unit) | 超时加锁;①在指定时间内尝试获取锁; ②超时 / 被中断→返回 false |

示例 :

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class Demo44 {
    private static int count = 0;

    public static void main(String[] args) {
        // 开启公平锁(true),默认非公平锁(false)
        ReentrantLock locker = new ReentrantLock(true);

        Thread t1 = new Thread(() -> { // 简化Lambda写法
            for (int i = 0; i < 50000; i++) {
                locker.lock(); // 获取锁(阻塞直到拿到锁)
                try {
                    count++; // 临界区:原子操作
                } finally {
                    locker.unlock(); // 必须放finally,确保锁释放
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                locker.lock();
                try { // 修复:t2的unlock移到finally
                    count++;
                } finally {
                    locker.unlock();
                }
            }
        });

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

        try {
            // 等待t1、t2执行完毕,避免主线程提前打印结果
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            // 中断后恢复中断状态(规范做法)
            Thread.currentThread().interrupt();
        }

        System.out.println("Final count: " + count); // 预期输出 100000
    }
}

注意 :

  1. ReentrantLock locker = new ReentrantLock(true);是一个公平锁的实例 ; 可以 保证先得到的先执行,每个线程最终都能获取锁 , 不会出现饥饿问题 ; 但是 公平锁需要维护等待队列的有序性 , 即使临时锁空闲 , 也不会让请求的线程"插队" , 必须唤醒队列的头部线程 , 导致上下文切换增多 , 最终导致性能损耗大
  2. locker.lock();放在 try{}外面 ; 会有风险如果 lock() 执行时抛出异常(比如 lockInterruptibly() 被中断、或 JVM 异常),lock() 并未成功获取锁,但 finally 块会无条件执行 unlock(); 此时当前线程未持有锁,调用 unlock() 会直接抛出 IllegalMonitorStateException,导致程序崩溃

② 总结创建线程的写法 :

  1. 继承 Thread 类(单独定义类/匿名内部类)
  2. 实现 Runnable 接口(定义单独类/匿名内部类)
  3. lambda
  4. 实现 Callable 接口(单独定义类/匿名内部类)
  5. 线程池 ThreadFactory

③ReentrantLock 和 synchronized 的区别

  1. synchronized 是一个关键字 , 是JVM 内部实现的 (基于 C++) ; ReentrantLock 是标准库的一个类 , 在 JVM 外部实现(基于 Java)
  2. synchronized 使用时不需要手动释放锁 ; ReentrantLock 使用时需要手动释放 , 使用起来更灵活 , 但也更容易遗漏 unlock()
  3. synchronized 在申请锁失败时 , 会死等 ; ReentrantLock 可以通过 tryLock 的方式等待一段时间就放弃
  4. synchronized 是非公平锁 ; ReentrantLock 默认是非公平锁 , 可以通过构造方法传入一个 true 开启公平锁 (ReentrantLock locker = new ReentrantLock(true);)
  5. synchronized 是通过 Object 的 wait/notify 来实现等待-唤醒 , 每次唤醒的是一个随机等待的线程 ; ReentrantLock 搭配 Condition 类实现等待-唤醒 , 可以精准控制唤醒某个指定的线程
  6. synchronized 可以不支持查询锁状态 ; ReentrantLock 支持isLocked()/isHeldByCurrentThread()等查询锁状态
  7. synchronized 不可中断锁(获取锁时线程会一直阻塞) ; ReentrantLock 可中断锁(lockInterruptibly())
  8. 都是可重入锁
Condition 类示例 :
java 复制代码
ReentrantLock lock = new ReentrantLock();
Condition conditionA = lock.newCondition(); // 条件A
Condition conditionB = lock.newCondition(); // 条件B

// 线程1等待条件A
lock.lock();
try {
    conditionA.await();
} finally {
    lock.unlock();
}

// 线程2唤醒等待条件A的线程
lock.lock();
try {
    conditionA.signal(); // 仅唤醒等待条件A的线程
} finally {
    lock.unlock();
}
lockInterruptibly() 可中断锁示例:
java 复制代码
Thread t = new Thread(() -> {
    try {
        lock.lockInterruptibly(); // 可中断获取锁
    } catch (InterruptedException e) {
        System.out.println("线程被中断,放弃获取锁");
        return;
    }
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
});
t.start();
t.interrupt(); // 中断线程,触发 InterruptedException

4.原子类

在前面 CAS 中提到过原子类 , 此处结合上一篇来讲

原子类是java.util.concurrent.atomic 包下的一组工具类 , 基于 CAS 机制实现无锁化的线程安全操作 , 避免锁竞争和上下文切换

① 原子类分类

|----------|-------------------------------------------------------|-----------------------------|
| 类别 | 核心类 | 用途 |
| 基本类型原子类 | AtomicInteger、AtomicLong、AtomicBoolean | 原子更新int / long / boolean 类型 |
| 引用类型原子类 | AtomicReference、AtomicStampedReference | 原子更新对象引用(解决 ABA 问题) |
| 数组类型原子类 | AtomicIntegerArray、AtomicLongArray | 原子更新数组中的元素 |
| 字段更新器原子类 | AtomicIntegerFieldUpdater、AtomicReferenceFieldUpdater | 原子更新对象的非静态字段(需 volatile 修饰) |

② 基本类型原子类

此处以 AtomicInterger 为例

|----------------------------------------|------------------------|
| 方法 | 功能 |
| get() | 获取当前值 |
| set(int newValue) | 设置新值(非原子,仅赋值) |
| getAndSet(int newValue) | 原子设置新值,返回旧值 |
| compareAndSet(int expect,int update) | CAS 更新:预期值匹配则更新,返回是否成功 |
| getAndIncrement() | 原子自增(i++),返回旧值 |
| incrementAndGet() | 原子自增(++i),返回新值 |
| getAndAdd(int delta) | 原子累加 delta,返回旧值 |
| addAndGet(int delta) | 原子累加 delta,返回新值 |

示例 :
java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class demo45 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger(0);
        for(int i = 0;i<10;i++){
            Thread t1 = new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    count.incrementAndGet();//count++;

                }
            });
            t1.start();
        }
        Thread.sleep(1000);
        System.out.println("count = "+count);
    }
}

③ 引用类型原子类

  • AtomicReference:原子更新对象引用(如自定义对象)
  • AtomicStampedReference:带版本号的原子引用,解决 CAS 的 ABA 问题
  • AtomicMarkableReference:带标记的原子引用(标记值为 boolean,简化版版本号)
示例 :
java 复制代码
import java.util.concurrent.atomic.AtomicStampedReference;

public class demo46 {
    public static void main(String[] args) {
        // 线程1:将 A→B→A(模拟 ABA 场景)
        AtomicStampedReference<String> ars = new AtomicStampedReference<>("A",1);
        Thread t1 = new Thread(()->{
            int stamp = ars.getStamp();//获取版本号
            ars.compareAndSet("A","B",stamp,stamp+1);
            ars.compareAndSet("A","B",stamp+1,stamp+2);


        });
        t1.start();

        // 线程2:CAS 更新,需匹配引用+版本号
        Thread t2 = new Thread(()->{
            int stamp = ars.getStamp(); // 初始版本号 1
            // 版本号不匹配(实际已变为 2),更新失败
            boolean success = ars.compareAndSet("A", "C", stamp, stamp + 1);
            System.out.println("更新是否成功:" + success); // false
            System.out.println("当前值:" + ars.getReference()); // A
            System.out.println("当前版本号:" + ars.getStamp()); // 2
        });
        t2.start();

    }
}

④ 数组原子类

AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray:原子更新数组中的元素(数组本身是普通数组,仅元素操作原子化)

示例 :
java 复制代码
import java.util.concurrent.atomic.AtomicIntegerArray;

public class demo47 {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        AtomicIntegerArray atomicArr = new AtomicIntegerArray(arr);

        // 原子更新索引 1 的元素(2→20)
        atomicArr.compareAndSet(1, 2, 20);
        System.out.println(atomicArr.get(1)); // 20

        // 原子自增索引 0 的元素(1→2)
        atomicArr.getAndIncrement(0);
        System.out.println(atomicArr.get(0)); // 2
    }
}

⑤ 字段更新器原子类

AtomicIntegerFieldUpdater/AtomicLongFieldUpdater/AtomicReferenceFieldUpdater:原子更新对象的非静态字段(需满足 2 个条件):

  • 字段必须用 volatile 修饰(保证可见性)
  • 字段的访问权限:若更新器在外部类,字段需为 public/protected,或通过反射突破访问限制
示例 :
java 复制代码
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class demo48 {
    public static void main(String[] args) {
        class User {
            // 必须用 volatile 修饰
            volatile int age;
            public User(int age) {
                this.age = age;
            }
        }

        // 创建字段更新器(参数:类、字段名)
        AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

        User user = new User(18);
        // 原子自增 age(18→19)
        updater.getAndIncrement(user);
        System.out.println(user.age); // 19

        // CAS 更新 age(19→20)
        updater.compareAndSet(user, 19, 20);
        System.out.println(user.age); // 20
    }
}

5.Semaphone 信号量

Java 中的 Semaphone 是 juc 包下的并发工具类 , 核心作用 : 控制同时访问某个资源的线程数量 , 本质是 通过维护一个"许可(permits)"计数器实现限流 / 资源管控 , 是实现"优先资源共享" 的经典方案

核心逻辑 :

Semaphone 是基于 AQS(抽象队列同步器)实现的

  1. 初始化时指定许可数(允许同时访问的线程数)
  2. 线程通过 acquire()获取许可 : 许可次数>0 则-1,且线程继续执行 ; 许可数 = 0 则线程阻塞,直到其他线程释放许可
  3. 线程通过 release()释放许可 : 许可数+1 , 唤醒等待队列中的线程

示例 :

java 复制代码
import java.util.concurrent.Semaphore;

public class demo49 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        semaphore.acquire();
        System.out.println("进行一次 P 操作");
        semaphore.acquire();
        System.out.println("进行一次 P 操作");
        semaphore.acquire();
        System.out.println("进行一次 P 操作");
        semaphore.acquire();
        System.out.println("进行一次 P 操作");
    }
}

应用 : 当初始值为 1 的信号量(二元信号量) , 等价于锁(不可重入锁)

java 复制代码
import java.util.concurrent.Semaphore;

public class demo50 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(1);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                try {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    try{
                        semaphore.acquire();
                        count++;
                        semaphore.release();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+count);
    }
}

6.CountDownLatch

是 JUC 的同步辅助工具类 , 核心作用 : 让一组线程等待其他线程完成指定操作后 , 再继续执行

核心原理 :

  1. 初始化计数器 : 创建 CountDownLatch 时指定计数器初始值
  2. 线程等待 : 调用 await()的线程会阻塞 , 直到计数器值减为 0;
  3. 计数器递减 : 完成任务的线程调用 countDown() , 将计数器值减为 1;
  4. 放弃等待线程 : 但计数器值减为 0 时 , 所有调用 await()的线程被唤醒 , 继续执行

注意 : 计数器只能递减 , 无法重置(一旦减为 0 , 后续调用 countDown()无效果 , await()也会立即返回)

示例 :

java 复制代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class demo57 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        ExecutorService executor = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 10; i++) {
            int id = i;
            executor.submit(() -> {
                System.out.println("子任务开始执行: " + id);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("子任务结束执行: " + id);
                latch.countDown();
            });

        }
        latch.await();//这个方法阻塞等待所有的任务结束
        System.out.println("所有线程执行完毕");
        executor.shutdown();
        /***
         * ①创建计数为 10 的 CountDownLatch,表示需要等待 10 个子任务完成;
         * ②用 4 核心的线程池批量提交 10 个子任务,每个任务执行 1 秒后调用 countDown() 递减计数器;
         * ③主线程调用 latch.await() 阻塞,直到 10 个任务全部完成(计数器归 0);
         * ④最后输出提示并关闭线程池。
         */

    }
}
相关推荐
小坏讲微服务44 分钟前
SpringCloud整合Scala实现MybatisPlus实现业务增删改查
java·spring·spring cloud·scala·mybatis plus
N***p3651 小时前
五大消息模型介绍(RabbitMQ 详细注释版)
java·rabbitmq·java-rabbitmq
雨中飘荡的记忆1 小时前
深入理解设计模式之单例模式
java·设计模式
程序员西西1 小时前
Spring Boot整合MyBatis调用存储过程?
java·后端
2501_941879812 小时前
Python在微服务高并发异步API网关请求处理与智能路由架构中的实践
java·开发语言
AAA简单玩转程序设计2 小时前
Java进阶小白手册:基础玩法升级,告别青铜套路
java
whltaoin2 小时前
【 手撕Java源码专栏 】Spirng篇之手撕SpringBean:(包含Bean扫描、注册、实例化、获取)
java·后端·spring·bean生命周期·手撕源码
闻缺陷则喜何志丹2 小时前
【SOSDP模板 容斥原理 逆向思考】3757. 有效子序列的数量|分数未知
c++·算法·力扣·容斥原理·sosdp·逆向思考
CoovallyAIHub2 小时前
如何在手机上轻松识别多种鸟类?我们发现了更简单的秘密……
深度学习·算法·计算机视觉