JUC常见类

发布于:2025-02-10 ⋅ 阅读:(81) ⋅ 点赞:(0)

JUC是java.util.concurrent的简称,当中有我们需要熟知的常见类,此文为你带来相关知识

前面我们学习了3种创建线程的方法,分别是:

  1. 继承Thread类,重写run方法
  2. 实现Runnable接口,重写run方法
  3. 创建线程池,利用线程池创建线程

JUC中为我们提供了一个新的创建线程的方法

1. Callable接口

Callable 是⼀个interface.相当于把线程封装了⼀个"返回值".方便程序猿借助多线程的方式计算结果.

那么既然已经有了Runnable接口可以创建线程任务,为何还要Callable接口呢??

我们从一个案例来探究一下吧!


示例: 创建线程计算1+2+3+…+100,不使用Callable版本

  • 创建⼀个类Result,包含⼀个sum表示最终结果,lock表示线程同步使用的锁对象.
  • main方法中先创建Result实例,然后创建⼀个线程t.在线程内部计算1+2+3+…+1000.
  • 主线程同时使用wait等待线程t计算结束.(注意,如果执行到wait之前,线程t已经计算完了,就不
    必等待了).
  • 当线程t计算完毕后,通过notify唤醒主线程,主线程再打印结果.
class Result{
    //累加和
    int sum=0;
    // 锁对象
    public Object lock = new Object();
}
public class TestRunnable {
    public static void main(String[] args) {

        Result result=new Result();
        Thread t=new Thread(){
            @Override
            public void run() {
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    // 执行累加操作
                    sum += i;
                }
                // 为结果赋值
                result.sum = sum;
                // 唤醒等待的线程
                synchronized (result.lock) {
                    // 检查累加是否执行完成
                    while (result.sum == 0) {
                        // 没有累加完成,等待结果
                        try {
                            result.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 打印结果
                    System.out.println(result.sum);
                    result.lock.notify();
                }
            }
        };
        t.start();
    }
}

示例: 创建线程计算1+2+3+…+100,使用Callable版本

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

public class TestCallable {
    public static void main(String[] args) throws Exception {
        //实现Callable接口,重写call()方法
        Callable <Integer> callable= new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                // 执行累加操作
                int sum=0;
                for (int i = 0; i <=100; i++) {
                    sum=sum+i;
                }
                return sum;
            }
        };
        //Callable要配合FutureTask一起使用,FutureTask用来获取Callable的执行结果
        FutureTask<Integer> futureTask=new FutureTask<>(callable);
        // FutureTask当做构造参数传入到Thread构造方法中
        Thread thread=new Thread(futureTask);
        thread.start();

        try {
            // 等待结果, 的时间可能被中断,会抛出InterruptedException
            Integer result = futureTask.get();
            // 打印结果
            System.out.println("执行结果是:" + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
            // 打印异常信息
            System.out.println("打印日志:" + e.getMessage());
        }
    }
}

我们发现Callable接口可以返回一个值,并让FutureTask等待结果,不需要借助辅助类,并且不需要使用锁,减少了CPU的消耗


2. Callable和Runnable的差异

我们通过源码来探究其差异:

在这里插入图片描述

总结

  1. Callable要实现call(),且有返回值,Runnable要实现的run()但没有返回值
  2. Callable的call()可以抛出异常,Runnable的run()不能抛出异常
  3. Callable配合FutrueTask一起使用,通过futureTask,get()方法获取call()的结果
  4. 两都是描述线程任务的接口

3. ReentrantLock

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

ReentrantLock 也是可重⼊锁. Reentrant这个单词的原意就是"可重入"

3.1 ReentrantLock的用法

  • lock(): 加锁,如果获取不到锁就死等.
  • trylock(超时时间):加锁,如果获取不到锁,等待⼀定的时间之后就放弃加锁.
  • unlock(): 解锁

源码:

在这里插入图片描述

lock.lock();   
try {    
// 业务逻辑代码   
} finally {   
// 如果业务代码执行到一半抛出了异常,那么会导致释放锁的代码无法执行,所以使用finally
lock.unlock()    
}  

3.2 ReentrantLock 和synchronized 的区别

  • synchronized是⼀个关键字,是JVM内部实现的(大概率是基于C++实现).ReentrantLock是标准
    库的⼀个类,在JVM外实现的(基于Java实现).
  • synchronized使用时不需要⼿动释放锁.ReentrantLock使用时需要⼿动释放.使用起来更灵活,但
    是也容易遗漏unlock.
  • synchronized在申请锁失败时,会死等.ReentrantLock可以通过trylock的方式等待⼀段时间就放弃.
  • synchronized是非公平锁,ReentrantLock默认是非公平锁.可以通过构造方法传入⼀个true开启公平锁模式.实现的过程中有会一个队列来组织排队的线程
    在这里插入图片描述
  • 更强大的唤醒机制.synchronized是通过Object的wait/notify实现等待-唤醒.每次唤醒的是⼀个随机等待的线程.ReentrantLock搭配Condition类实现等待-唤醒,可以更精确控制唤醒某个指定的线程.
ReentrantLock lock=new ReentrantLock(true);
// 条件1
Condition male= lock.newCondition();
// 条件2
Condition female= lock.newCondition();
// 根据不同的条件进行阻塞等待
male.await();
male.signal();       // 唤醒相应队列中的一个线程
male.signalAll();   // 唤醒相应队列中的所有线程

// 根据不同的条件进行阻塞等待
female.await();
female.signal();    // 唤醒相应队列中的一个线程
female.signalAll(); // 唤醒相应队列中的所有线程
  • 读写锁
//创建一个读写锁
ReentrantReadWriteLock readwriteLock=new ReentrantReadWriteLock();
//获取读锁
ReentrantReadWriteLock.ReadLock readLock=readwriteLock.readLock();
//获取写锁
ReentrantReadWriteLock.ReadLock writeLock=readwriteLock.readLock();

3.3 如何选择使用哪个锁?

  • 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便.
  • 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更灵活控制加锁的行为,而不是死等.
  • 如果需要使用公平锁,使用ReentrantLock.

4. 信号量Semaphore

信号量:用来表示"可用资源的个数".本质上就是⼀个计数器.

理解信号量
可以把信号量想象成是饭店的座位牌:当前有座位5个.表示有5个可用资源.
当有人进去的时候,就相当于申请⼀个可用资源,可⽤座位就-1(这个称为信号量的P操作)
当有人出来的时候,就相当于释放⼀个可用资源,可⽤座位就+1(这个称为信号量的V操作)
如果计数器的值已经为0了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源.

Semaphore的PV操作中的加减计数器操作都是原⼦的,可以在多线程环境下直接使用.

示例: 模拟吃饭的场景

// 初始化一个信号量的对象, 指定系统可用资源的数量, 相当于一个饭店有5个餐位
        Semaphore semaphore=new Semaphore(5);
        Runnable runnable=new Thread(){
                @Override
                public void run() {
                    //1.尝试申请资源,寻找座位吃饭
                    System.out.println(Thread.currentThread().getName()+"尝试申请资源(寻找座位吃饭)");
                    try {
                        semaphore.acquire();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //2.获取到了资源
                    System.out.println(Thread.currentThread().getName()+"获取到了资源(拥有了座位)");

                    //3.利用资源处理业务
                    try {
                        //休眠一会,模拟业务处理消耗时间
                        System.out.println(Thread.currentThread().getName()+"吃饭ing......");
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //4.释放资源
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName()+"释放了资源(吃完饭走人)");

                }
            };

        //创建20个线程,模拟20个人吃饭场景
        for (int i = 0; i < 20; i++) {
            //创建线程并指定任务
            Thread thread=new Thread(runnable);
            //启动线程
            thread.start();
        }
    }

说明:

  • acquire用于申请资源
  • release用于释放资源

5. CountDownLatch

同时等待N个任务执行结束.

好像跑步⽐赛,10个选⼿依次就位,哨声响才同时出发;所有选⼿都通过终点,才能公布成绩。

示例: 模拟跑步比赛的场景

//指定参赛选手的个数(线程数)
        CountDownLatch countDownLatch=new CountDownLatch(10);

        System.out.println("各就各位,预备...");
        for (int i = 0; i < 10; i++) {
            Thread player = new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "开跑.");
                    // 模拟比赛过程, 休眠2秒
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "到达.");
                    // 标记选手已达到终点,让countDownLatch的计数减1, 当计数到0时,表示所有的选手都到达终点,比赛结束
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "player" + i);
            // 启动线程
            player.start();
        }

        TimeUnit.MILLISECONDS.sleep(10);
        System.out.println("===== 比赛进行中 =====");
        // 等待比赛结束
        countDownLatch.await();
        // 颁奖
        System.out.println("比赛结束, 进行颁奖");

图解:

在这里插入图片描述

说明:

  • countDownLatch的计数减1,当计数到0时,表示所有的选手都到达终点,比赛结束
  • await负责等待计算器为0

应用场景:
一个大的文件,可以分割成好多块,一个线程去下载一个小块,当所有的线程都执行完下载任务,再把各个小块的内容拼成一个完整的文件 (迅雷)


网站公告

今日签到

点亮在社区的每一天
去签到