javaEE 初阶 — 多线程— JUC(java.util.concurrent) 的常见类

news2024/10/6 16:25:33

文章目录

  • 1. Callable 接口
    • 1.1 Callable 的用法
  • 2. ReentrantLock
    • 2.1 ReentrantLock 的缺陷
    • 2.1 ReentrantLock 的优势
  • 3. 原子类
  • 4. 信号量 Semaphore
  • 5. CountDownLatch
  • 6. 相关面试题

1. Callable 接口


类似于 Runnable 一样。
Runnable 用来描述一个任务,描述的任务没有返回值。
Callable 也是用来描述一个任务,描述的任务是有返回值的。

如果需要使用一个线程单独的计算出某个结果,此时使用 Callable 是比较合适的。

1.1 Callable 的用法


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

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

public static void main(String[] args) throws InterruptedException {
    Result result = new Result();
    
    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();
            }
        }
    };
    t.start();

    synchronized (result.lock) {
        while (result.sum == 0) {
            result.lock.wait();
        }
        System.out.println(result.sum);
    }
}


可以看到,上述代码需要一个辅助类 Result,还需要使用一系列的加锁和 wait notify 操作,代码复
杂,容易出错。


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

package thread;

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

public class ThreadDemo9 {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        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> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();

        Integer result = futureTask.get();
        System.out.println(result);
    }
}


call() 相当于是 Runnable 的 run() 方法,run方法返回的是 void 此处返回值泛型参数。

FutureTask 表示这是未来的任务。
可以把 FutureTask 简单理解为点餐时的小票,这个小票就是 FutureTask。
后面我们可以随时凭这张小票去查看自己点的餐做出来了没。

get() 方法就是获取结果。
get 会发生阻塞,直到 callable 执行完毕,get 才阻塞完成,才获取到结果。

可以看到,使用 Callable 和 FutureTask 之后,代码简化了很多,也不必手动写线程同步代码了。

2. ReentrantLock


这里的 ReentrantLock 是标准库给我们提供的另一种锁,也是可重入的。

synchronized 是直接基于代码块的方式来加锁和解锁的。
ReentrantLock 使用了 lock 方法和 unlock 方法来加锁和解锁。

package thread;

import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo10 {
    
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        
        reentrantLock.unlock();
    }
}


reentrantLock.lock()reentrantLock.unlock() 之间的代码就被锁给保护起来了。
但是这样的写法有很大的弊端:

unlock 有可能会执行不到

2.1 ReentrantLock 的缺陷


如果代码中间存在 return 或者异常,就有可能会导致 unlock 不能顺利执行。

package thread;

import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo10 {

    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        int num = 0;
        reentrantLock.lock();

        if (num == 0) {
            return;
        }

        if (num == 1) {
            return;
        }

        throw new Exception();

        reentrantLock.unlock();
    }
}


这时就要把 unlock 写在 finally

package thread;

import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo10 {

    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        int num = 0;
        try {
            if (num == 0) {
                return;
            }

            if (num == 1) {
                return;
            }

            throw new Exception();
        } finally {
            reentrantLock.unlock();
        }
    }
}

2.1 ReentrantLock 的优势


1、ReentrantLock 提供了公平锁版本的实现。

  ReentrantLock reentrantLock = new ReentrantLock(true);
 ReentrantLock reentrantLock = new ReentrantLock();

ReentrantLock 括号里加上 true 表示这是一个公平锁。
什么都不加,或者加 false 表示这是一个非公平的锁。


2、更加灵活的阻塞等待方式

对于 synchronized 来说,提供的加锁操作就是 “死等” ,只要获取不到锁,就会一直等待。
而 ReentrantLock 提供更加灵活的等待方式:trylock

这里的 trylock 分为有参数和无参数的版本。

无参数版本:能加锁就加,加不上就放弃。

有参数的版本:指定了超时时间,加不上锁就等待一段时间。如果时间到了也没加上就放弃。


3、ReentrantLock 提供了一个更加强大的等待机制。

synchronized 搭配的是 wait 和 notify ,notify 的时候随机唤醒一个 wait 的线程。

ReentrantLock 搭配的是一个 Condition 类,进行唤醒的时候可以指定唤醒的线程。

3. 原子类


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

原子类有以下几个:

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReferenc

以 AtomicInteger 举例,常见方法有:

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

基于 CAS 确实是更高效的解决了线程安全问题,但是 CAS 不能代替锁。
CAS 的适用范围是有限的,不像锁的适用范围那么广。

4. 信号量 Semaphore


操作系统上提到的信号量和此处这个信号量是一个东西,只不过此处的这个信号量是 java 把操作系统原生的信号量封装了一下。

信号量在生活中经常可以见到。

比如说停车场,因为停车场的空闲位置个数都是固定的,当位置到达上限的时候就不能停车了。

停车场的入口位置会有一个牌子,牌子上显示空闲位置的个数。
每当有车进去,牌子上的显示的个数就减少一个;有车出来,个数就加一个。
这个牌子就相当于是一个计数器,当这个计数器为 0 的时候,也就是停车场空闲位置达到上限了。

当没有位置的时候要停车,就只能在这里等待或者去别处找停车场。

信号量本质上就是一个 计数器,描述了 “可用资源的个数”

P操作(acquire) :申请一个可用资源,计时器就要 -1。
V操作() :释放一个可用资源,计数器个数就要 +1。

如果此时的计数器为 0 了,继续执行 P 操作,就会发生阻塞等待。

考虑一个计数初始值为 1 的信号量。
针对这个信号量的值,就只有1 和 0 两种取值(信号量不能是负的)

执行一次 P 操作,1 就变成了 0 。
执行一次 V 操作,0 就变成了 1 。

如果已经执行过一次 P 操作了,继续执行 P 操作,就会阻塞等待。

有没有让你想到锁,(锁可以视为是计数器为 1 的信号量,二元信号量)
锁是一种信号量的特殊情况,信号量是锁的一般表达。

Semaphore 的使用

package thread;

import java.util.concurrent.Semaphore;

public class ThreadDemo11 {

    public static void main(String[] args) throws InterruptedException{
        Semaphore semaphore = new Semaphore(3); //指定计数器个数是3
        semaphore.acquire(); //执行P操作
        System.out.println("P操作一次");

        semaphore.acquire(); //执行P操作
        System.out.println("P操作一次");

        semaphore.acquire(); //执行P操作
        System.out.println("P操作一次");

        semaphore.acquire(); //执行P操作
        System.out.println("P操作一次");
    }
}




当前的计数器个数是 3 ,当计数器为 0 的时候继续执行 acquire 操作就会阻塞等待。

package thread;

import java.util.concurrent.Semaphore;

public class ThreadDemo11 {

    public static void main(String[] args) throws InterruptedException{
        Semaphore semaphore = new Semaphore(3); //指定计数器个数是3
        semaphore.acquire(); //执行P操作
        System.out.println("P操作一次");

        semaphore.acquire(); //执行P操作
        System.out.println("P操作一次");

        semaphore.acquire(); //执行P操作
        System.out.println("P操作一次");

        semaphore.release(); //执行V操作

        semaphore.acquire(); //执行P操作
        System.out.println("P操作一次");
    }
}




当计数器为 0 的时候,执行 V 操作后,计数器的个数就加了一个。

5. CountDownLatch


假如有一个跑步比赛。



这场跑步比赛,开始的时间确定的(发号枪)
但是结束的时间是不明确的。(所有的选手都冲过终点后)

为了等待跑步比赛的结束,就引入了这个 CountDownLatch
主要是有两个方法:

1、await (a 是 all wait 是等待),主线程来调用这个方法。

2、countDown 表示选手冲过了终点线。


CountDownLatch 在构造的时候,会指定一个计数(选手的个数)

例如,指定四个选手进行比赛,初始情况下,调用 await 就会阻塞。
每个选手都冲过终点,都会调用 countDown 方法。

第三次调用 countDown ,await 没有任何影响。
第四次调用 countDown ,await 就会被唤醒,返回。(解除阻塞队列)
此时就可以认为是整个比赛都结束了。

package thread;

import java.util.concurrent.CountDownLatch;

public class ThreadDemo12 {

    public static void main(String[] args) throws InterruptedException{
        CountDownLatch latch = new CountDownLatch(10);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    latch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }

        // 必须等到 10 人全部回来
        latch.await();
        System.out.println("比赛结束");
    }
}

6. 相关面试题


1、介绍下 Callable 是什么

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

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

Callable 通常需要搭配 FutureTask 来使用。FutureTask 用来保存 Callable 的返回结果。
因为 Callable 往往是在另一个线程中执行的,啥时候执行完并不确定。
FutureTask 就可以负责这个等待结果出来的工作。


2、线程同步的方式有哪些?

synchronized,ReentrantLock,Semaphore 等都可以用于线程同步。


3、为什么有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例。

  • synchronized 使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放,使用起来更
    灵活。
  • synchronized 在申请锁失败时,会死等,ReentrantLock 可以通过 trylock 的方式等待一段时
    间就放弃。
  • synchronized 是非公平锁,ReentrantLock 默认是非公平锁。可以通过构造方法传入一个
    true 开启公平锁模式。
  • synchronized 是通过 Object 的 wait / notify 实现等待-唤醒。每次唤醒的是一个随机等待的
    线程。ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线
    程。

4、AtomicInteger 的实现原理是什么?

基于 CAS 机制。

伪代码如下:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
        }
       return oldValue;
    }
}



5、信号量听说过么?之前都用在过哪些场景下?

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

使用信号量可以实现 “共享锁”,比如某个资源允许 3 个线程同时使用,那么就可以使用 P 操作作为
加锁,V 操作作为解锁,前三个线程的 P 操作都能顺利返回,后续线程再进行 P 操作就会阻塞等待,
直到前面的线程执行了 V 操作。


6、解释一下 ThreadPoolExecutor 构造方法的参数的含义

参考关于 ThreadPoolExecutor 的篇章

篇章链接:

https://blog.csdn.net/m0_63033419/article/details/128586070?spm=1001.2014.3001.5501

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/157124.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

我们一直在说数字化转型,什么才是数字化转型?

我们一直在说数字化转型&#xff0c;什么才是数字化转型&#xff1f;深度长文&#xff0c;4000字&#xff0c;融合了很多国内外专业期刊观点&#xff0c;一文讲清到底什么是企业数字化转型&#xff0c;心急的小伙伴可以先看目录&#xff1a; 关于定义——到底什么是“数字化转…

24 届秋招 | 高质量学习交流环境

大家好&#xff0c;我和一些计算机方向、背景非常优秀的、来自清华、新国立等知名大学的几位同学以及工作多年的高级研发工程师一起运营了一个知识星球。 星球里有大量国内top985、海外名校的同学在一起&#xff0c;目的是为了打造一个非常优质量的社群。 如果你也曾苦于在各…

PySimpleGUI图形化界面实现Office文件格式转换

PySimpleGUI图形化界面实现Office文件格式转换Python实现三种文件两个版本的格式转换1、doc与docx格式互相转换2、xls与xlsx格式互相转换3、ppt与pptx格式互相转换PythonPySimpleGUI实现综合版本Python实现三种文件两个版本的格式转换 1、doc与docx格式互相转换 这里主要运用…

excel求和技巧:如何忽略错误值进行求和

按照对应的订单号引用已有的收货金额&#xff0c;这种问题相信很多朋友都会处理&#xff0c;用VLOOKUP函数就能搞定。我们今天要讨论的是如何对含有错误值的数据进行求和。如果直接求和&#xff0c;得到的结果也是一个错误值&#xff0c;如下图&#xff1a;对于这种要对含有错误…

Linux驱动开发基础__ Linux中断系统中的重要数据结构

目录 1 整体概述 2 irq_desc 数组 3 irqaction 结构体 4 irq_data 结构体 5 irq_domain 结构体 6 irq_chip 结构体 1 整体概述 该文章内容&#xff0c;可以从 request_irq(include/linux/interrupt.h)函数一路分析得到。 能弄清楚下面这个图&#xff0c;对 Linux 中…

Domino Nomad Web 1.0.6!

大家好&#xff0c;才是真的好。 虽然Domino Notes 9.0.x版本早前宣布从本月开始停止市场商业推广&#xff0c;并逐步停止技术支持服。但没让人意外的是&#xff0c;12月5号&#xff0c;HCL更新了一版Domino Notes 9.0.1版本的补丁程序FP10IF10&#xff1a; 没有任何额外的说…

车规级CAN FD收发器SIT1044Q,能替代TJA1044吗?

国际知名品牌NXP推出的TJA1042Q、TJA1043Q、TJA1044Q、TJA1051Q等CAN FD收发器芯片&#xff0c;相信很多电子工程师并不陌生。这类芯片应用中&#xff0c;非常成熟稳定&#xff0c;深受汽车电子工程师的认可、支持和青睐。然而&#xff0c;在实际应用中&#xff0c;很多客户由于…

新建文本文档

Spring Boot 加载外部配置文件 Spring Boot 允许你从外部加载配置&#xff0c;这样的话&#xff0c;就可以在不同的环境中使用相同的代码。支持的外部配置源包括&#xff1a;Java属性文件、YAML文件、环境变量、命令行参数。 用Value注解可以将属性值直接注入到beans中。命令行…

【win11环境编译安装deformable Detr的MultiScaleDeformableAttention模块】

Microsoft Visual C 14.0 is required.1.Compiling CUDA operators2.安装Build Tools for Visual Studio3.安装合适的cuda4.编译1.Compiling CUDA operators cd ./models/ops sh ./make.sh # unit test (should see all checking is True) python test.pyNote: win11 or win10…

经认定的闵行区企业技术中心给予10万元资助

闵行区企业技术中心一、主管部门闵行区经济委员会二、政策依据《闵行区关于推进先进制造业高质量发展的若干产业政策意见》&#xff08;闵府规发〔2020〕5号&#xff09;《闵行区企业技术中心认定管理办法》&#xff08;闵经委规发〔2021〕2号&#xff09;《关于申报认定2022年…

搭建Go环境 03

1、windows下搭建go环境 1、介绍SDK 1、SDK&#xff08;软件开发工具包&#xff09; 2、SDK是给开发人员使用的&#xff0c;其中包含了对应开发语言的工具包 2、下载SDK Go官方镜像站(国内用户推荐): https://golang.google.cn/dl/ 官网下载 https://golang.org/dl/ # 安装事项…

【参考答案】java基础练习:变量、数据类型、输入、输出、运算符

练习1&#xff1a;判断输入的值是否是偶数&#xff0c;另外&#xff0c;要处理输入错误 (目的&#xff1a;熟悉输入、输出&#xff0c;特别是Scanner对象的方法) package com.qzcsbj;import java.util.Scanner;public class Test {public static void main(String[] args) {Sca…

Hudi集成Spark(一)Spark Shell方式

文章目录环境准备安装 Spark启动 Hadoop&#xff08;略&#xff09;spark-shell 方式启动 spark-shell插入数据查询数据查询数据更新数据增量查询指定时间点查询删除数据覆盖数据环境准备 安装 Spark 1&#xff09;Hudi 支持的 Spark 版本 HudiSupported Spark 3 version0.1…

131页8万字数字化矿山整体解决方案

【版权声明】本资料来源网络&#xff0c;知识分享&#xff0c;仅供个人学习&#xff0c;请勿商用。 【侵删致歉】如有侵权请联系小编&#xff0c;将在收到信息后第一时间删除&#xff01; 完整资料领取见文末&#xff0c;部分资料内容&#xff1a; 目 录 1、煤矿综合自动化系统…

两台 mac 通过 scp 命令快速传输数据

这两天由于电脑进水了&#xff0c;所以申请换了一台 mac 电脑&#xff0c;所以想把老电脑的数据拷贝到新电脑&#xff0c;折腾了半天&#xff0c;最后还是发现 scp 命令最好用。 使用 「scp 命令方式」之前尝试的其他方法 1、隔空投送 刚开始使用那个隔空投送功能&#xff0c;但…

数据结构与算法(一)——时间复杂度和空间复杂度

时间复杂度 1、概念引入 先说结论&#xff1a;时间复杂度是用来估计算法运行时间的一个式子&#xff08;单位&#xff09;。 例如&#xff1a;这四组代码&#xff0c;哪组运行时间最短&#xff1f; q&#xff1a;我们该用什么方式来体现算法运行的快慢&#xff1f; a&#…

安卓apk包破解

安卓apk包破解前言一、前置工作二、正式开始前言 拿到安卓的apk包如何&#xff0c;如何破解呢。流程如下 一、前置工作 拿到一个apk文件&#xff0c;修改其后缀为zip 例如&#xff1a; test.apk > test.zip 使用解压工具进行解压。解压后如图 获取到原始文件目录&…

Django项目——通过APIView实现API访问,增删改查数据库

前提 该文章在已有项目的基础上进行修改 https://blog.csdn.net/qq_38122800/article/details/128583379?spm1001.2014.3001.5502 1、配置序列化器 序列化器包含序列化和反序列化两个过程,简单点理解就是 序列化 : 将从数据库中查的数据变为前端页面可以接受的json数据 反…

机器学习的逻辑回归(Logistic)的实例————预测学生是否被录取

目录要求代码1. 导入模块2. 导入数据3. 求解theat的最优值,画出样本的位置和决策边界。4. 画出迭代函数随迭代次数变化的曲线,代价函数最终的收敛值5.比较三种学习率的代价函数随迭代次数变化的曲线5.1 学习率为0.00035.2 学习率为0.00055.3 学习率为0.00001要求 代码 1. 导入…

PySpark和RDD对象详解

目录 一.了解Spark、PySpark Spark是什么 Python on Spark Pyspark 小结 二.构建PySpark执行环境入口对象 PySpark的编程模型 小结 三.RDD对象 python数据容器转RDD对象 注意 演示 读取文件转RDD对象 演示 一.了解Spark、PySpark Spark是什么 定义:Apache Spark是用…