JUC常见类

news2024/12/23 18:00:27

背景

JUC是java.util.concurrent的简称,这个包里面存放的都是和多线程相关的类,在面试中非常的重要

目录

1.Callable接口

2.ReentrantLock

3.信号量Semaphore

4.CountDownLatch

5.集合的线程安全问题

1.Callable接口

1.1.认识Callable接口

(1)Callable接口,是用于描述一个线程任务。

(2)Callable接口和Runnable接口非常的类似。用来描述线程任务的时候,都是使用匿名内部类,但是Runnable接口是重写run方法,而Calleble接口是重写call方法

(3)Runnale描述的任务没有返回值,但是Callable描述的接口是有返回值的。所以Calleble的存在就会有它特别的意义,也能干一些Runnale不能干的事情

看完上面的内容,你就大概了解了Callable接口大概是干什么用的,下面就来学会如果使用吧。

1.2.使用Callable接口创造线程

通过线程完成的任务:将一个变量从0累加到5000,并且在线程外面打印出来。

为了凸显Callable接口的独特优势,我们先使用Runnable接口完成任务。

(1)Runnable接口

有下面两种写法,都是ok的;但是还是推荐第一种,第二种是为了和Callable做对比

第一种: 

    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<5000;i++) {
                    count++;
                }
            }
        });
        t.start();
        t.join();
        System.out.println(count);
    }

第二种:

    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<5000;i++) {
                    count++;
                }
            }
        };
        Thread t = new Thread(runnable);
        t.start();
        t.join();
        System.out.println(count);
        
    }

Runnale接口完成任务的逻辑:

局限性非常的明显,就是只能使用全局变量,并且线程内的变量外部无法获取,提高了耦合。

所以,我们的Callable接口就登场了。

(2)Callable接口

代码:

 public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int count = 0;
                for(int i=0;i<5000;i++) {
                    count++;
                }
                return count;
            }
        };
        FutureTask futureTask = new FutureTask(callable);
        Thread t = new Thread(futureTask);
        t.start();
        t.join();
        System.out.println(futureTask.get());
    }

虽然这个代码相比Runnable的代码看起来复杂了一点,但是降低了耦合,提高了可阅读性,也称为“异步编程”

我们解释一下代码:

举我们生活中吃饭的例子:futureTask就类似我们拿到的待餐的小票,而小票里面的内容(参数)就描述了我们要吃的东西(要完成的任务);小票有两份,一份会交给后厨,一份则在你手中,过后就可以根据小票取餐(拿到返回值)。

这就是使用Callable接口创造线程的方式。

2.ReentrantLock

这是一个可重入锁,学习新知识第一步,先认识发音

下面只是简单的介绍即可,点到为止

2.1.ReentrantLock的使用

(1)不靠谱的加锁

 public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock();
        //1.加锁
        locker.lock();
        //2.内容
        
        //3.解锁
        locker.unlock();
    }

代码解释:

为什么说这种加锁的写法是不靠谱的呢?因为在中间写代码的内容里面,可能会出现一些意外,比如直接程序退出,或者抛出异常,而导致没有解锁。

所以,都是采取下面的写法

(2)靠谱的加锁

 public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock();//创建锁对象
        try {
            //1.加锁
            locker.lock();
            //2.内容
            
        }finally {
            //3.解锁
            locker.unlock();
        }
    }

我们借鉴异常章节的写法,将关锁操作写在finally内部;于是,无论代码怎么执行,finally内部的代码是一定会被执行到的,于是,就不会担心不解锁的操作了。

2.2.ReentratLock的优势

这里介绍的优势是相对sychronized来说的,但是我们日常写代码还是用sychronized就可以了,准没有问题。

(1)提供公平锁的选择

sychronized锁,是非公平锁;而ReentratLock却可以选择是否公平。

 public static void main(String[] args) {
        ReentrantLock locker1 = new ReentrantLock();//非公平锁
        ReentrantLock locker2 = new ReentrantLock(true);//公平锁
    }

通过参数,就可以选择是否公平

(2)tryLock操作

这是一个什么操作呢?比如我们的sychronized锁,当A线程拿到锁之后,B线程再想获取锁,就会阻塞等待,这个等待是死等。但是ReentratLock中的tryLock操作,本质上也是加锁操作,如果该锁被占用了,此时就会直接返回失败,不会阻塞等待,如果该锁没有被占用,则会加锁成功。下面介绍tryLock的使用。

 public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock();
        Thread t1 = new Thread(()->{
            if(locker.tryLock()) {
                System.out.println("A线程获取到锁");
            }else {
                System.out.println("A线程没有获取到锁");
            }
        },"A");
        Thread t2 = new Thread(()->{
            System.out.println(locker.tryLock());
            if(locker.tryLock()) {
                System.out.println("B线程获取到锁");
            }else {
                System.out.println("B线程没有获取到锁");
            }
        },"B");
        t1.start();
        t2.start();

    }

很明显,线程B抢不过线程A,但是不会阻塞等待,而是直接退出了。

这就是tryLock的操作,还有一种带参数的tryLock,也就是等待一定的时间获取不到锁,就返回。

public static void main(String[] args) throws InterruptedException {
        ReentrantLock locker = new ReentrantLock();
        Thread t1 = new Thread(()->{
            if(locker.tryLock()) {
                System.out.println("A线程获取到锁");
            }else {
                System.out.println("A线程没有获取到锁");
            }
        },"A");
        Thread t2 = new Thread(()->{
            try {
                if (locker.tryLock(300, TimeUnit.MILLISECONDS)) {
                    System.out.println("B线程没有获取到锁");
                } else {
                    System.out.println("B线程获取到锁");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"B");
        t1.start();
        t2.start();

    }

A线程先拿到锁,此时B没有拿到,等待锁的释放;300ms内,A线程释放了锁,B线程也就拿到了锁。

(3)可唤醒指定线程

在sychronized中,是搭配wait和notify来等待和唤醒线程,这种唤醒是随机的;

但是ReentratLock中,是搭配Condition接口来指定唤醒线程的,这样就比上述的随机唤醒更强一些。

3.信号量Semaphore

3.1.信号量概念

(1)信号量,本质上就是一个计数器

(2)举例:例如底下停车场,当有一辆车进去后,门口牌子里面的内容:车位剩余容量就会-1;当有一辆车出库之后,车位容量+1,当没有车位时,就会阻塞等待。

(3)标准库中,提供了Semaphore这个类,来操作信号量。P操作:计数器-1,申请资源;V操作:计数器+1,释放资源。

3.2.信号量代码使用

(1)创建信号量对象

 Semaphore semaphore = new Semaphore(5);//设置计数器的容量

这就是创造了一个计数器,参数是这个计数器的最大容量(比如只能停五辆车);接下来就是要对这个就是计数器进行+1或者-1操作。

(2)类方法

方法签名说明
void acquire()使计数器+1
void release()使计数器-1
public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(5);//设置计数器的容量
        int i=0;
        semaphore.acquire();
        System.out.println("P操作"+": "+i++);
        semaphore.acquire();
        System.out.println("P操作"+": "+i++);
        semaphore.acquire();
        System.out.println("P操作"+": "+i++);
        semaphore.acquire();
        System.out.println("P操作"+": "+i++);
        semaphore.acquire();
        System.out.println("P操作"+": "+i++);
        semaphore.acquire();
        System.out.println("P操作"+": "+i++);
        
    }

计数器容量只有5,但是此时进行了六次P操作,最后一次P操作就会阻塞等待

这就是计数器的简单使用。

因为,当计数器的容量只有1的时候,其实可以当成锁来使用,换句话说,锁就是一种特殊的信号量

使用信号量保证线程安全:

 static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(1);
        Thread t1 = new Thread(()->{
            try {
                for (int i = 0; i < 50000; i++) {
                    semaphore.acquire();//计数器+1
                    count++;
                    semaphore.release();//计数器-1
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        Thread t2 = new Thread(()->{
            try {
                for (int i = 0; i < 50000; i++) {
                    semaphore.acquire();//加锁
                    count++;
                    semaphore.release();//解锁
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t1.join();
        System.out.println(count);
    }

4.CountDownLatch

4.1.概念

(1)CountDownLatch是一个非常使用的工具类

(2)举例:有一些下载速度非常的快工具(比如:IDM),会将一个下载任务分成多个部分,每个部分由一个线程去执行,当所有的线程都完成任务之后,下载任务才算完成。用来判别所有线程任务完成的这个行为,就是countDownLach所做的。

(3)在代码中,我们可以使用该类去操作一些相关的代码

4.2.代码使用

(1)类对象的的创建

 CountDownLatch downLatch = new CountDownLatch(10);//参数表示拆分成的任务数

这样就创造出来了对象

(2)代码主体

 public static void main(String[] args) throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(10);//参数表示拆分成的任务数
        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread t = new Thread(()->{
                System.out.println(id+"线程开始执行");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(id+"任务执行完毕");
                downLatch.countDown();//执行一次,相当于完成一个任务
            });
            t.start();
        }
        downLatch.await();//等待10个任务完成,才执行后续的逻辑
        System.out.println("10个任务执行完毕!");
    }

5.集合的线程安全问题

这里介绍的集合,就是我们之前在数据结构部分学习到的各种集合,如:顺序表、链表等等

5.1.背景知识

(1)不安全的集合

ArrayList、LinkedList、Queue、HashMap等,大多数的集合在多线程下使用,都是不安全的

(2)比较安全的集合

Vector、Stack、Hashtable自带sychronized、所以在多线程下,大部分是安全的。

为什么说不是一定安全的呢?原因是:这些集合加锁的位置是在每个方法中,比如两个线程同时插入/删除数据时,是会阻塞等待,属于线程安全的;但是一个线程取数据,一个线程删除数据,是线程不安全的。

所以,线程是否安全,还是要分场景和情况讨论。

下面介绍一下集合在多线程下该如何使用

5.2.多线程下的ArrayList

我们知道,ArraryList是没有任何锁的,但是想要在多线程中去使用它,该如何做呢?我们只需要使用标准库提供封装好的ArrayList即可,当然,也可以自己加锁,但是下面不介绍了

(1)Collections.sychronizedList(new ArrayList)

 public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        Collections.synchronizedList(arrayList);
        arrayList.add(10);
    }

这样就将普通的ArrayList进行了一定的加锁操作

(2)CopyOnWriteArrayList

字面意思:在写数据的时候复制新的容器

写时拷贝的操作:当我们往一个容器里面添加元素时,不是向旧的容器添加;而是新创造出一个新的容器,把原始数据拷贝进行,最后添加新的原始。当添加完原始之后,再将旧容器的引用指向新的容器。

优点:在读多写少的场景下,性能很高,不需要加锁竞争

缺点:占用内存多、新写的数据不能第一时间被读取到

多线程下使用队列,我们这里也不介绍了,而是直接使用前面学习到的阻塞队列即可

5.3.多线程下的哈希表

(1)背景

在多线程下,HashMap是线程不安全的,Hashtable虽然有锁,但是线程也不一定安全,并且不推荐使用

所以标准库中引入了:ConcurrentHashMap,这个哈希表就是在Hashtable的基础上进行的优化。

Hashtable:对每个方法都加锁。

ConcurrentHashMap:使用锁桶的方式。

(2)ConcurrentHashMap的三处优化

第一:加锁方式,采取锁桶的方式

哈希表里面的第一层是一个”数组“,每个数组背后是一个链表;而前面的Hashtable是对数组加锁,这里的ConcurrentHashMap是对每个链表加锁。

如果多个线程同时插入数据,并且在不同的链表上,则就相当于没有加锁,这样极大的提高了插入的效率。

第二:操作size,采取CAS的方式

在哈希表的容量上面,即使操作的不是同一个链表,size却是同一个,所以在操作size时,采取CAS的方式

第三:扩容,采取拷贝到新空间

扩容一般发生在插入数据之后,如果数据量巨大,在插入数据后进行普通的扩容,效率就会非常的慢,就显得非常的卡顿。所以这里采取特殊的扩容手段。

ConcurrentHashMap在扩容的时候,搞两份空间,一份是扩容前的,另一份是扩容后的;接下来就会每次从旧的空间搬运一部分数据到新的空间。

在般的过程中,如果发生插入操作:则会插入到新的空间中;发生删除操作:新的空间和旧的空间都删除;查找操作:新的空间和旧的空间都要查找。

以上就是ConcurrentHashMap做出的三个优化操作

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

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

相关文章

phpstudy 搭建 upload-labs 文件上传靶场

phpstudy 搭建靶场&#xff1a;下载安装好phpstudy后&#xff0c;下载靶场源码&#xff1a; upload-labs下载地址&#xff1a; https://github.com/c0ny1/upload-labs 下载完压缩文件&#xff0c;解压文件&#xff0c;解压后的文件夹命名为upload--labs 将解压后到文件夹放…

爱普生晶振在物联网LoRa通讯中的应用

LoRa 是LPWAN通信技术中的一种&#xff0c;是美国Semtech公司采用和推广的一种基于扩频技术的超远距离无线传输方案。这一方案改变了以往关于传输距离与功耗的折衷考虑方式&#xff0c;为用户提供一种简单的能实现远距离、长电池寿命、大容量的系统&#xff0c;进而扩展传感网络…

频分复用系统设计及其MATLAB实现

引言 随着通信技术的飞速发展&#xff0c;通信系统的容量需求不断增长。频分复用&#xff08;Frequency Division Multiplexing, FDM&#xff09;作为一种重要的多路复用技术&#xff0c;被广泛应用于现代通信系统中。本文将介绍频分复用系统的设计原理&#xff0c;并展示如何…

springboot 自动配置源码解读

什么是自动装配 当我们程序依赖第三方功能组件时&#xff0c;不需要手动将这些组件类加载到IOC容器中。例如 当程序需要用到redis时&#xff0c;在pom.xml文件中引入依赖&#xff0c;然后使用依赖注入的方式直接从IOC容器中拿到相应RedisTemplate实例。 SpringBootApplication …

cuda非root用户安装及mamba库安装报错问题

1、先检查是不是cuda版本的问题&#xff08;cuda≥11.6&#xff09; 非root用户安装指南&#xff1a; &#xff08;1&#xff09;先去NVIDIA官网CUDA Toolkit Archive | NVIDIA Developer 选一个版本 查看architecture的命令 uname -m 查看version的命令 lsb_release -a 下…

探索AIGC技术:创新、挑战与责任

&#x1f3a5; 个人主页&#xff1a;Dikz12&#x1f4d5;格言&#xff1a;那些在暗处执拗生长的花&#xff0c;终有一日会馥郁传香欢迎大家&#x1f44d;点赞✍评论⭐收藏 #如何看待AIGC技术&#xff1f; 目录 AIGC简单介绍 创新 责任 未来展望和挑战 AIGC简单介绍 A…

34.基础乐理-简谱需要移调吗?

首先需要具备 首调 与 固定调的知识&#xff0c;才能理解&#xff0c;以两只老虎为例子&#xff0c;如下图&#xff1a; 首调&#xff1a;可以看到C大调、D大调、E大调三种方式的乐谱&#xff0c;记录的数字&#xff0c;记录的唱名&#xff0c;都是1231&#xff0c;唯一不同的…

深度学习中的归一化:BN,LN,IN,GN的优缺点

目录 深度学习中归一化的作用常见归一化的优缺点 深度学习中归一化的作用 加速训练过程 归一化可以加速深度学习模型的训练过程。通过调整输入数据的尺度&#xff0c;归一化有助于改善优化算法的收敛速度。这是因为归一化后的数据具有相似的尺度&#xff0c;使得梯度下降等优化…

开源框架 NanUI 项目宣布将暂停开发,作者转行卖钢材

NanUI 界面组件是一个开源的 .NET 窗体应用程序界面框架&#xff0c;适用于希望使用 HTML5 / CSS3 等前端技术来构建 Windows 窗体应用程序用户界面的 .NET 开发人员。 该项目的作者林选臣日前在 GitHub 上发布了停更公告&#xff0c;称因去年被裁员失业&#xff0c;他目前已经…

VBA技术资料MF147:从Excel运行PowerPoint演示文稿

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

毫米波雷达多人呼吸心跳检测MATLAB仿真

本文基于TI的IWR1642毫米波雷达 2T4R MIMO阵列&#xff0c;通过实际采集数据算法仿真&#xff0c;实现多人呼吸心跳检测。 文章末尾给出了本文的仿真代码。 主要内容包含&#xff1a; &#xff08;1&#xff09;雷达参数设定 &#xff08;2&#xff09;ADC数据导入 &#xff08…

uniapp0基础编写安卓原生插件和调用第三方jar包和编写语音播报插件之使用jar包插件

前言 如果你不会编写安卓插件,你可以先看看我之前零基础的文章(uniapp0基础编写安卓原生插件和调用第三方jar包和编写语音播报插件之零基础编写安卓插件), 我们使用第三方包,jar包编写安卓插件 开始 把依赖包,放到某个模块的/libs目录(myTestPlug/libs) 还要到build…

采访OpenAI奥特曼:预计会有几个不同版本的通用人工智能,并且各有擅长 | 最新快讯

在我与山姆奥特曼&#xff08;Sam Altman&#xff09;的短暂会面中&#xff0c;有许多瞬间让我更加了解了这位 OpenAI CEO 的世界观。 第一个瞬间是他指着我的 iPhone SE 说&#xff1a;“这是最好的 iPhone。”不过&#xff0c;展示其更清晰愿景的瞬间是&#xff0c;他描绘了人…

运维的利器--监控--zabbix--第二步:建设--选择中文--导致乱码问题

文章目录 问题原因解决方法1解决方法2&#xff1a;修改图形的数据展示&#xff08;默认字体有问题&#xff09; 问题 点击对应主机的【图形】即可看到以下乱码情况 原因 上述的图标数据&#xff0c;下面的小白框表示乱码含义&#xff0c;是因为我们改了zabbix的 语言为中文…

c语言——二叉树

目录 目录 二叉树关键概念理解 一颗拥有1000个结点的树度为4&#xff0c;则它的最小深度是&#xff1f; 那么对于二叉树&#xff0c;只掌握这些是远远不够的&#xff0c;我们还需要掌握几个最基本的经典问题&#xff0c; 求二叉树大小 求叶子结点个数 求深度 求第k层的…

如何快速的追加文章的内容(在不知道内容的情况下)

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 1、打开工具&#xff0c;切换到文章模块下&#xff0c;快捷键&#xff1a;Ctrl1 2、新建一个文章做演示&#xff0c;001 3、添加一个内容&#xff0c;就随…

【IO操作】标准IO和文件IO

一.标准IO和文件IO的区别 &#xff08;1&#xff09;一般标准IO指的是C语言的IO操作&#xff0c;文件IO一般指的是Linux系统调用的IO操作。标准O因为是C语言提供的标准库&#xff0c;所以可以在其他操作系统平台编译后可以执行&#xff0c;但是文件IO只能在Linux下使用&#x…

C语言高效学习、提升方法

前言 已经用了十多年的C语言&#xff0c;回头看怎么学习、提升最快呢&#xff1f;个人觉得还是要从框架上去着手学习&#xff0c;用全局眼光去看、去学&#xff0c;主要是三部分&#xff1a;关键字及基本语法、重点功能点及标准库函数&#xff1b;只要把这三部分基本搞懂了&am…

AD | Altium Designer(原理图设计、电路仿真、PCB绘图)汉化版

Altium Designer(原理图设计、电路仿真、PCB绘图) 通知公告 Altium Designer(AD)是一种功能强大的电子设计自动化(EDA)软件。它主要用于设计和开发电子产品,如电路板(PCB)、集成电路(IC)和嵌入式系统。AD提供了完整的设计工具套件,包括原理图设计、PCB布局、仿真、设…

蓝桥杯单片机省赛——第八届“基于单片机的电子钟程序设计与调试”程序部分

往期回顾 第三届蓝桥杯单片机省赛 第四届蓝桥杯单片机省赛 第五届蓝桥杯单片机省赛 第六届蓝桥杯单片机省赛 第七届蓝桥杯单片机省赛 文章目录 往期回顾一、前期准备二、代码详情1.基础代码蜂鸣器/继电器/led/定时器之类的代码 2.按键详解按键写法讲解 3.驱动的处理驱动写法讲…