zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【多线程】JUC

多线程 juc
2023-09-27 14:28:31 时间

请添加图片描述

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇


🎭一. Callable 接口

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

Runnable 非常相似,都是可以在创建线程的时候,来指定一个" 具体的任务 "。不同的是 Callable 指定的任务是带返回值的,Runnable 则不带返回值。

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

  • 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
  • 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
  • 把 callable 实例使用 FutureTask 包装一下.
  • 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
  • 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
public class Demo28 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        //套上一层,目的是为了获取到后续结果
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t = new Thread(task);
        t.start();

        //在线程 t 执行结束之前,get 会阻塞,直到 t 执行完了,结果算好了
        //get 才能返回,返回值就是 call 方法 return 的内容
        System.out.println(task.get());
    }
}

在这里插入图片描述

理解 Callable

  • Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务.
  • Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为 Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
  • FutureTask 就可以负责这个等待结果出来的工作

理解 FutureTask

想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你一张 “小票” . 这个小票就是
FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没.


🎪二. JUC(java.util.concurrent) 的常见类

📍1. ReentrantLock

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

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

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
public static void main(String[] args) {
    ReentrantLock locker = new ReentrantLock();
    try{
        //加锁
        locker.lock();
        
        //代码逻辑... 如果中间出异常了,可能就执行不到  unlock
    }finally{
        //解锁
        locker.unlock();
    }
}

ReentrantLock 和 synchronized 的区别:

  • synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个, 在 JVM 外实现的(基于 Java 实现,提供 lock 方法加锁,unlock 解锁).
  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式
ReentrantLock locker = new ReentrantLock(true);
  • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

大部分情况下,使用锁还是以 synchronized 为主,特殊情况下才会使用 ReentrantLock


🏧三. 原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

以 AtomicInteger 举例,常见方法有:

  • addAndGet(int delta); i += delta;
  • decrementAndGet(); --i;
  • getAndDecrement(); i–;
  • incrementAndGet(); ++i;
  • getAndIncrement(); i++;

🚧四. 线程池

虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.

线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 “池子” 中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.

ExecutorServiceExecutors

代码示例:

  • ExecutorService 表示一个线程池实例.
  • Executors 是一个工厂类, 能够创建出几种不同风格的线程池.
  • ExecutorService 的 submit 方法能够向线程池中提交若干个任务.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
   }
});

Executors 创建线程池的几种方式:

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

Executors 本质上是 ThreadPoolExecutor 类的封装.(标准库中,最核心的线程池类)

ThreadPoolExecutor

ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.

ThreadPoolExecutor 的构造方法(在帮助文档里面找)
在这里插入图片描述

理解 ThreadPoolExecutor 构造方法的参数:
 
corePoolSize: 核心线程数(正式员工,线程无论是否空闲,都会始终存在)
 
maximumPoolSize: 最大线程数(临时工 / 实习生,繁忙的时候才创建,空闲的时候就销毁)
 
keepAliveTime: 临时工允许的空闲时间.
 
unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
 
workQueue: 传递任务的阻塞队列(虽然线程池内部可以内置任务队列,但是我们也能自己定义队列交给线程池来使用,体现出了线程池的扩展性)
 
threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
 
RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.

  • AbortPolicy(): 超过负荷, 直接抛出异常. (不干活,摆烂了)
  • CallerRunsPolicy(): 交给调用者负责处理.(把责任返回了)
  • DiscardOldestPolicy(): 丢弃队列中最老的任务.
  • DiscardPolicy(): 丢弃新来的任务.

实际工作中,一般建议使用线程池的时候,尽量还是用 ThreadPoolExecutor ,复杂版本的,这里的参数都显式的手动来传参。

当我们使用线程池的时候,线程数目,如何设置?设置几比较合适?
 
当你回答出具体的数字就错了,不同的场景,不同的程序,不同的主机配置,都会有差异!
 
可以回答找到合适线程数的方法:压测(性能测试)
 
针对当前的程序进行性能测试,分别设置不同的线程数目,分别进行测试,在测试过程中,会记录当前程序的 时间,CPU 占用,内存占用...,根据压测结果,来选择咱们觉得最合适当前场景的数目。

在程序中:

  1. CPU 密集型(线程数最多也就是 CPU 核心数,设置再多没意义了)
  2. IO 密集型(线程数可以超过 CPU 核心数的,等待 IO 过程不吃 CPU)

实际开发中,一个程序都是既需要 CPU 也需要等待 IO,这两者不同的时间比例,就会影响到线程数设置成多少合适。


🚦五. 信号量 Semaphore

信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器

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

可以把信号量视为是一个更广义的锁,当信号量的取值 0-1 的时候就退化成了一个普通的锁。

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

代码示例:

  • 创建 Semaphore 示例, 初始化为 3, 表示有 3 个可用资源.
  • acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
  • 申请资源四次然后打印出来
public static void main(String[] args) throws InterruptedException {
    //构造方法传入有效资源的个数
    Semaphore semaphore = new Semaphore(3);

    //p 操作 ,申请资源
    semaphore.acquire();
    System.out.println("申请资源");
    semaphore.acquire();
    System.out.println("申请资源");
    semaphore.acquire();
    System.out.println("申请资源");
    semaphore.acquire();
    System.out.println("申请资源");

    //v 操作 , 释放资源
    semaphore.release();
}

在这里插入图片描述

结果只打印了 3 次,有效资源数为 3 ,第四次在阻塞等待。


⛽️六. CountDownLatch

相当于,在一个大的任务被拆分成若干个子任务的时候,用这个来衡量啥时候是这些子任务都执行结束。

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

例如进行跑步比赛,CountDownLatch 描述啥时候是所有选手都到达终点
 
例如进行一次下载(多线程下载),CountDownLatch 描述当前所有的线程都下载完毕

代码示例:

public class Demo31 {
    public static void main(String[] args) throws InterruptedException {
        //模拟一个跑步比赛
        //构造方法中设定有几个选手参赛
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(()->{
                try {
                    Thread.sleep(3000);
                    System.out.println("到达终点");
                    //countDown 相当于 "撞线"
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
        //await 在等待所有的线程 "撞线"
        //调用 CountDown 的次数达到初始化的时候设定的值
        //await 就返回,否则 await 就阻塞等待
        latch.await();

        System.out.println("比赛结束");
    }
}

在这里插入图片描述