JUC编程02

JUC编程

06 Callable

java 复制代码
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
  • Callable泛型的参数<V>为 call方法的返回值

如何启动线程调用Callable接口?

java 复制代码
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // new Thread().start(); 只能执行Runnable接口
        MyThread myThread = new MyThread(); // Callable
        FutureTask<String> futureTask = new FutureTask<>(myThread); // Callable -> FutureTask(Runnable)
        new Thread(futureTask, "A").start();
        new Thread(futureTask, "B").start();
        String result = futureTask.get();
        // 获取Callable的返回结果 等待结果可能会产生阻塞! 一般放在代码最后 或者采用异步通信
        System.out.println(result);
    }
}

// Callable泛型的参数为 call方法的返回值
class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("call()");
        return "1024";
    }
}
  • 利用Runnable接口的实现类FutureTask,使用构造函数FutureTask(Callable<V> callable)搭建Callable和Runnable之间的桥梁。

  • 利用futureTask.get()方法获取Callable的返回值。

FutureTask类使用的两个注意事项

  1. get方法需要获得返回结果,可能会产生阻塞!一般放在代码最后,或者采用异步通信来解决。

  2. 多个线程同时启动调用Callable,call方法只会执行一次。

    • FutureTask.run() 内部用 state + CAS(runner) 保证只有一个线程能真正进入 call() 执行区。

      run方法中:

      java 复制代码
      if (state != NEW ||
                  !RUNNER.compareAndSet(this, null, Thread.currentThread()))
                  return;
      1. state != NEW:如果任务不是"新建状态",说明它要么已经执行过、要么取消了、要么正在结束流程,直接return,不再执行。
      2. !RUNNER.compareAndSet(this, null, Thread.currentThread()):用CAS原子地把runner从null设置为当前线程。
        • 成功:表示当前线程抢到了执行权。
        • 失败:说明别的线程已经在执行(或刚执行完但还没清理),当前线程直接return,防止并发重复执行。

07 JUC常用辅助类

1、CountDownLatch

CountDownLatch 用来让一个(或多个)线程等待其他线程把事情做完,再继续往下执行。

关键词只有两个:

  • 我等你
  • 你们做完,我再走

把它想成一个 倒计时闸门

  • 初始化一个数字(这里是 6)
  • 每调用一次 countDown(),数字减 1
  • 当数字变成 0 :所有在 await() 上等待的线程 同时被放行
  • 一旦归零,就再也回不去了
java 复制代码
// 计数器
public class CountDownLatchTest {
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "Go out!");
                countDownLatch.countDown(); // 数量-1
            }, String.valueOf(i)).start();
        }
        try {
            countDownLatch.await(); // 等待计数器归零 然后再向下执行
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("Close door!");
    }
}
  • public void await():导致当前线程等到锁存器计数到零,除非线程是interrupted 。
  • public void countDown():减少锁存器的计数,如果计数达到零,释放所有等待的线程。

总结:CountDownLatch 是一种同步工具,用于使一个或多个线程等待,直到其他线程完成一组操作,其内部通过一个不可重置的计数器实现线程间的协调。

2、CyclicBarrier

CyclicBarrier 是一个"让一组线程互相等待,等人到齐后再一起继续执行"的同步工具。

关键词只有三个:

  • 一组线程
  • 彼此等待
  • 到齐后同时放行
CyclicBarrier的两个核心参数:
  1. parties:
java 复制代码
CyclicBarrier cyclicBarrier = new CyclicBarrier(7);

必须有 7 个线程调用 await(),屏障才会被打开。

  1. barrierAction(可选):

    java 复制代码
    () -> {System.out.println("召唤神龙!");}

    当第 7 个线程到达屏障时,先执行这段代码。注意:

    • 只执行一次
    • 最后一个到达的线程 执行
    java 复制代码
    public class CyclicBarrierTest {
        public static void main(String[] args) {
            CyclicBarrier cyclicBarrier = new CyclicBarrier(7,
                    ()-> System.out.println("召唤神龙!") // 当第 7 个线程到达屏障时,先执行这段代码
            );
            for (int i = 0; i < 7; i++) {
                final int temp = i % 7 + 1;
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName() + "收集了第" + temp + "颗龙珠!");
                    try {
                        cyclicBarrier.await(); // 等待 必须有 7 个线程调用 await(),屏障才会被打开
                    } catch (InterruptedException | BrokenBarrierException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("开始许愿!"); // await() 返回,线程继续执行后面的代码
                }, "Thread-" + i).start();
            }
        }
    }

注意点:Lambda 只能捕获 final / effectively final 的局部变量,是为了避免线程执行时变量已经改变或消失,从而保证语义清晰和线程安全。

总结:CyclicBarrier 用于协调一组线程在某个同步点相互等待,直到所有线程都到达后,再统一继续执行,并且支持在屏障触发时执行一个统一的动作,同时屏障可重复使用。

3、Semaphore

Semaphore 用来控制"同时能访问某个资源的线程数量"。

关键词只有一个:

  • 限流(并发数控制)
关键参数:
  1. 创建 Semaphore:最多允许 2 个线程同时进入临界区(只有两个停车位)

    java 复制代码
    Semaphore semaphore = new Semaphore(2);

    注意:

    • 2不是线程总数
    • 并发访问的最大数量
  2. 请求许可:要一个许可证(进入停车位)

    java 复制代码
    semaphore.acquire();
    • 如果当前还有permit:立即获得,继续执行。
    • 如果permit用完:线程阻塞,排队等待。
  3. 释放许可:释放许可证(离开停车位)

    java 复制代码
    semaphore.release();
    • permit+1
    • 如果有线程在等待:唤醒其中一个(或多个)
为什么release放在finally?
java 复制代码
finally {
    semaphore.release();
}

即时线程中途抛出异常,也必须释放许可,否则会"永久占坑",导致死锁。

Semaphore的几个关键特性
  1. 控制的是数量,不是顺序

    Semaphore只关心有几个permit,不保证线程执行顺序。

  2. acquire/release可以不成对,但不推荐。

  3. 可用来做互斥锁,等价于同一时刻只允许一个线程进入。类似synchronized/ReentrantLock

    java 复制代码
    Semaphore mutex = new Semaphore(1);
  4. 支持公平/非公平

    java 复制代码
    Semaphore semaphore = new Semaphore(2, true);
    • true:先到先得
    • false:可能插队,性能更好(默认)

Semaphore 是一种计数信号量,用于控制同时访问某一资源的线程数量,通过许可证(permit)的获取与释放来实现并发访问的限流。

08 读写锁ReadWriteLock

ReadWriteLock通过"读读并发、读写互斥、写写互斥"的机制,在读多写少的场景下显著提高并发性能。

关键词:

  • 读共享
  • 写独享
  • 提升吞吐
java 复制代码
public class ReadWriteLockTest {
    public static void main(String[] args) {
        // MyCache myCache = new MyCache();
        MyLockCache myCache = new MyLockCache();
        // 写线程
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()-> myCache.put(String.valueOf(temp), temp), String.valueOf(i)).start();
        }
        // 读线程
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()-> myCache.get(String.valueOf(temp)), String.valueOf(i)).start();
        }
    }
}
// 自定义缓存(无锁)
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    // 存 写
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入成功!");
    }
    // 取 读
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "读取" + key);
        Object o =  map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取成功!");
    }
}
// 自定义缓存(加锁)
class MyLockCache {
    private volatile Map<String, Object> map = new HashMap<>();
    private ReadWriteLock lock =  new ReentrantReadWriteLock();
    // 存 写
    public void put(String key, Object value) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入成功!");
        } catch (Exception ex) {
            ex.printStackTrace();
        }finally {
            lock.writeLock().unlock();
        }
    }
    // 取 读
    public void get(String key) {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取" + key);
            Object o =  map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取成功!");
        } catch(Exception ex) {
            ex.printStackTrace();
        }finally {
            lock.readLock().unlock();
        }
    }
}

1、为什么需要ReadWriteLock?

java 复制代码
private volatile Map<String, Object> map = new HashMap<>();

问题在于:

  • volatile关键字只保证可见性不保证复合操作原子性
  • HashMap再并发写时不是线程安全的

如果使用synchronized会怎样?

java 复制代码
synchronized void put(...) { ... }
synchronized void get(...) { ... }

这样也是安全的,但是问题是:读和读也互斥,在**"读多写少"**的场景下,性能浪费很大。

2、ReadWriteLock的核心规则(重点)

以ReentrantReadWriteLock为例:

  1. 读-读:可以并发
    • 多个线程同时读
    • 互不阻塞
  2. 读-写:互斥
    • 有线程在读(写)
    • 写(读)线程必须等
  3. 写-写:互斥
    • 同一时间只允许一个写线程

写锁是"独占锁",读锁是"共享锁"。

3、关键代码

  1. 写操作(put):

    java 复制代码
    lock.writeLock().lock();

    申请独占锁:

    • 若有其他读锁/写锁 -> 阻塞
    • 成功后:没有任何读/写线程能同时访问map
  2. 读操作(get):

    java 复制代码
    lock.readLock().lock();

    申请共享锁:

    • 若当前没有写锁:多个线程可以同时拿到读锁
    • 若有写锁:必须等待
  3. finally解锁:

    复制代码
    finally {
        lock.readLock().unlock();
    }
    • 防止异常导致锁永远不释放 -> 全部线程阻塞

4、一个重要但容易踩的坑

  • 不能"锁升级"(读->写)

    java 复制代码
    lock.readLock().lock();
    // 想升级为写锁
    lock.writeLock().lock(); // 可能死锁
    • 写锁要求"没有任何读锁"
    • 但当前线程自己还持有读锁
  • 可以"锁降级"(写->读)

    java 复制代码
    lock.writeLock().lock();
    try {
        // 修改数据
        lock.readLock().lock();
    } finally {
        lock.writeLock().unlock();
    }
    // 仍然持有读锁

ReadWriteLock 通过将访问分为"共享读"和"独占写",在保证线程安全的前提下显著提高了读多写少场景下的并发性能,其典型实现是 ReentrantReadWriteLock。

09 队列Queue

1、BlockingQueue

用"队列 + 阻塞等待"把生产者线程和消费者线程安全地连接起来,实现线程间通信与解耦。

四组API
方法/方式 抛出异常 返回值 阻塞等待 超时等待
添加 add() offer() put() offer(,)
移除 remove() poll() take() poll(,)
检索队首元素 element() peek() - -
适合 你希望立刻知道出错, 不想等待。 你不想阻塞线程, 宁可拿不到就算了。 标准生产者-消费者模型(最常用)。 不想无限等待,业务上 允许超时处理。
BlockingQueue的作用是什么?
  1. 线程安全的队列操作:多线程同时put/take不会把队列搞乱。
  2. 自动协调生产与消费速度:
    • 队列满:生产者自动等待(阻塞)
    • 队列空:消费者自动等待(阻塞)

BlockingQueue适合写生产者-消费者模型、线程池任务队列、消息队列缓冲等。

2、SynchronousQueue

用于在线程之间"直接传递"元素(handoff),实现生产者和消费者一对一同步。

特点
  • 容量为0(没有内部缓冲)
  • put(x)必须等待另一个线程take()把x接走才会返回
  • take()必须等到另一个线程put()递过来元素才会返回

常用于:

  • 线程池(比如CachedThreadPool的任务移交)
  • 不希望排队/缓存,只希望"来一个交一个"的场景
  • 限制生产者不能超前生产(天然背压:下游处理不过来时,反向给上游"施加压力",迫使上游放慢或停下生产)
SynchronousQueue的原理

可以看成"没有仓库的快递交接点":

  • 生产者到了就把包裹递出来,但没人接就只能等
  • 消费者到了就准备接收包裹,但没人递也只能等
  • 一旦双方都到齐,就完成一次配对(match)/交换(exchange)

内部怎么做配对?

实现上SynchronousQueue不使用数组存元素,而是维护一个"等待者结构":

  • 要么有等待put的线程在排队(携带元素)
  • 要么有等待take的线程在排队(等待接收)

当对方到来时:

  • 直接把元素交给对方
  • 唤醒对方线程
  • 双方各自返回
相关推荐
我是一只小小鱼~2 小时前
JAVA 使用spring boot 搭建WebAPI项目
java·数据库·spring boot
量子炒饭大师2 小时前
【C++入门】—— 【什么时候需要用到深拷贝】C++的类中何时需要用到深拷贝?保姆级别带你罗列所有可能!
java·c++·dubbo·深拷贝
小信丶2 小时前
@EnableMethodCache 注解详解:原理、应用场景与示例代码
java·spring boot·后端·spring
坊钰2 小时前
【Rabbit MQ】Rabbit MQ 的结构详解,传输机制!!!
java·rabbitmq
Psycho_MrZhang2 小时前
Claude高质量产出
java·服务器·网络
spencer_tseng5 小时前
Stream not available [SysDictDataMapper.xml]
xml·java
蒸蒸yyyyzwd9 小时前
cpp对象模型学习笔记1.1-2.8
java·笔记·学习
程序员徐师兄10 小时前
Windows JDK11 下载安装教程,适合新手
java·windows·jdk11 下载安装·jdk11 下载教程