JavaEE:多线程进阶(JUC [java.util.concurrent] 的常见类)

文章目录


JUC

什么是JUC

JUC的全称为: java.util.concurrent.

JUC是Java并发工具包的一部分。它提供了一组并发编程工具和类,用于处理多线程编程和并发任务。

Callable 接口

这个接口非常类似于Runnable接口.

关于Runnable我们都知道,通过Runnable可以表示一个具体的任务,我们通过Runnable的run方法来描述接下来的代码要做什么事情.

Callable也是类似的效果,但是与Runnable相比,Callable提供了call方法,与Runnable的run方法相比,call方法带有返回值,而run方法是void.

因此,如果你是期望创建线程,让这个线程来返回一个结果,那么使用Callable要比Runnable要更方便一些.

比如说创建一个线程,让这个线程计算 1+2+3+...+1000.

使用Runnable的代码:

java 复制代码
package javaEE.thread;

public class B {
    
    private static int result;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            int sum = 0;
            for (int i = 0; i < 1000; i++) {
                sum += i;
            }
            result = sum;
        });
        t.start();
        t.join();
        System.out.println(result);
    }
}

使用Callable的代码:

java 复制代码
package javaEE.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class C {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() {
                int sum = 0;
                for (int i = 0; i < 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        // FutureTask的作用:
        // 后续我们需要使用FutureTask来拿到最后的结果.
        // futureTask.get()就能拿到call方法的返回值
        // futureTask.get()带有阻塞功能,当线程t还没执行完,get就会阻塞
        // 直到t执行完毕,get才能返回.get就相当于"带有返回结果"的join.
        System.out.println(futureTask.get());
    }
}

注意:Thread不能传入Callable作为参数~

需要使用FutureTask传入Thread.

理解 Callable

Callable和Runnable相对,都是描述一个"任务",Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务.

Callable通常需要搭配FutureTask来使用,FutureTask用来保存Callable的返回结果,因为Callable往往是在另一个线程中执行的,啥时候执行完并不确定.

FutureTask就可以负责这个等待结果出来的工作.

理解FutureTask

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

ReentrantLock

ReentrantLock为可重入互斥锁.

ReentrantLock是一种经典风格的锁,它提供lock和unlock方法来完成加锁和解锁.

java 复制代码
package javaEE.thread;

import java.util.concurrent.locks.ReentrantLock;

public class D {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        
        ReentrantLock locker = new ReentrantLock();
        
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                locker.lock();
                count++;
                locker.unlock();
            }
         });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                locker.lock();
                count++;
                locker.unlock();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

使用ReentrantLock时要保证unlock能被执行到~

我们在开发过程中大部分情况下都是使用synchronized就可以了,那为啥还要搞一个ReentrantLock呢?

ReentrantLock相比synchronized还是有一些区别差异的.

  1. synchronized属于是关键字(底层是通过JVM的C++代码实现的).

    ReentrantLock则是标准库提供的类,它是通过Java代码来实现的.

  2. synchronized通过代码块控制加锁解锁,ReentrantLock通过调用lock/unlock方法来完成.unlock可能会遗漏,所以一般要把unlock放到finally中.

  3. ReentrantLock提供了tryLock这样的加锁风格 ,前面介绍的加锁,都是发现锁被别人占用了,就阻塞等待.
    tryLock在加锁失败的时候,不会阻塞,而是直接返回,通过返回值来反馈是加锁成功还是失败.(相当于给了程序员更多的可操作空间)

  4. ReentrantLock还提供了公平锁的实现.默认为非公平锁,我们可以在构造方法中传入参数,把它设置成公平锁.

  5. ReentrantLock还提供了功能更强的"等待通知机制".

    synchronized是通过Object的wait/notify来实现等待-唤醒,每次唤醒的是一个随机等待的线程.

    ReentrantLock搭配Condition类实现等待-唤醒,可以更精确的控制唤醒某个指定的线程.

ReentrantLock和synchronized分别在什么场景下使用?

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

信号量 Semaphore

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

系统申请资源时计数器+1(也称为"P"操作),释放资源时计数器-1(也称为"V"操作).

如果计数器为0了,系统还尝试申请资源,此时就会出现阻塞,直到有其他线程释放资源.

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

操作系统本身提供了信号量实现,JVM把操作系统的信号量封装了一下~

java 复制代码
package javaEE.thread;

import java.util.concurrent.Semaphore;

public class E {
    public static void main(String[] args) throws InterruptedException {
        // 括号里写的就是可用资源个数,计数器的初始值.
        Semaphore semaphore = new Semaphore(3);
        semaphore.acquire(); // 申请资源
        System.out.println("申请资源");
        semaphore.acquire(); // 申请资源
        System.out.println("申请资源");
        semaphore.acquire(); // 申请资源
        System.out.println("申请资源");
        semaphore.release(); // 释放资源
        System.out.println("释放资源");
        semaphore.acquire(); // 申请资源
        System.out.println("申请资源");
    }
}

我们也可以通过Semaphore来实现类似加锁的效果.

java 复制代码
package javaEE.thread;

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;

public class D {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
//        ReentrantLock locker = new ReentrantLock(true);
        // 设置可用资源数为1
        Semaphore semaphore = new Semaphore(1);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
//                locker.lock();
                try {
                    semaphore.acquire(); // 申请一个资源
                    count++;
                    semaphore.release(); // 释放一个资源
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
//                locker.unlock();
            }
         });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
//                locker.lock();
                try {
                    semaphore.acquire(); // 申请一个资源
                    count++;
                    semaphore.release(); // 释放一个资源
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
//                locker.unlock();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

运行结果为:100000.也是正确的~

CountDownLatch

同时等待N个任务结束.

很多时候,我们需要把一个大的任务,拆成多个小任务,使用多线程/线程池执行.

如何衡量,所有的任务都执行完毕了?

此时我们就可以使用CountDownLatch来达成这个效果.

java 复制代码
package javaEE.thread;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class F {
    public static void main(String[] args) throws InterruptedException {
        // 通过线程池创建2个线程
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 构造方法的数字,就是拆分出来的任务个数
        CountDownLatch countDownLatch = new CountDownLatch(4);

        for (int i = 0; i < 4; i++) {
            int id = i;
            executorService.submit(() -> {
                System.out.println("任务" + id + "开始执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("任务" + id + "结束执行");
                // 完毕 over!!
                countDownLatch.countDown();
            });
        }
        // 当countDownLatch收到了4个"完成",此时所有的任务就完成了.
        // await => all wait
        // await 这个词也是计算机术语,它在Python/js 中的意思是async wait(异步等待).在这里不是这个意思哦~
        countDownLatch.await();
        System.out.println("所有任务执行完毕");
    }
}

本文到这里就结束了~

相关推荐
Ajiang28247353041 小时前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空1 小时前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
冰帝海岸5 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象5 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了6 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
----云烟----6 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024066 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
小二·6 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端