JUC(java.util.concurrent)中的常见类

文章目录

  • Callable接口
  • ReentrantLock
    • ReentrantLock 和 synchronized 的区别:
    • 如何选择使用哪个锁?
  • 信号量Semaphore
  • CountDownLatch
  • 多线程环境使用ArrayList
  • 多线程使用 哈希表
  • 相关面试题

JUC放了和多线程有关的组件

Callable接口

和Runnable一样是描述一个任务,但是有返回值,表示这个线程执行结束要得到的结果是啥

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

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

public class Test {
    private static int sum = 0;

    public static void main(String[] args) throws InterruptedException {
        //创建一个线程,让这个线程实现 1+2+3+...+1000
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int result = 0;
                for (int i = 1; i <= 1000; i++) {
                    result += i;
                }
                //此处为了把result告知主线程,需要通过成员变量
                sum = result;
            }
        });
        t.start();
        t.join();

        System.out.println(sum);
    }
}

这个代码让主线程和t 线程耦合太大了
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 Test {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for(int i = 1; i <= 1000; i++) {
                    result += i;
                }
                return result;
            }
        };
        //创建线程,把callable搭载到线程内部执行
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        
        System.out.println(futureTask.get());
    }
}

ReentrantLock

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

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(): 尝试加锁,如果锁已经被占用了,直接返回失败,而不会继续等待。还可以指定等待超时时间加锁, 如果获取不到锁, 等待⼀定的时间之后就放弃加锁.
  • unlock(): 解锁

ReentrantLock 和 synchronized 的区别:

  • synchronized 是⼀个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的⼀个类, 在 JVM 外实现的(基于 Java 实现).
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入⼀个 true 开启公平锁模式

在这里插入图片描述
ReentrantLock的参数是true就是公平锁,false或者不写就是非公平锁

  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
import java.util.concurrent.locks.ReentrantLock;

public class Test {
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock(true);
        
        try {
            //加锁
            locker.lock();
        } finally {
            //解锁
            locker.unlock();
        }
    }
}
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待⼀段时间就放弃.

  • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

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

信号量Semaphore

信号量就是一个计数器,描述了可用资源的个数

围绕信号量有两个基本操作

  1. P操作:计数器-1,申请资源
  2. V操作:计数器+1,释放资源

如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

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

代码示例
• 创建 Semaphore 实例, 初始化为 4, 表示有 4 个可用资源.
• acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)

import java.util.concurrent.Semaphore;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //4个可用资源
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");

        semaphore.release();
    }
}

在这里插入图片描述
总共四个可用资源,进行第五次P操作会阻塞直到其他线程执行V 操作

import java.util.concurrent.Semaphore;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //4个可用资源
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");

        semaphore.release();
    }
}

在这里插入图片描述
锁其实是特殊的信号量
如果信号量只有0 , 1两个取值,此时就称为"二元信号量",本质上就是一把锁

import java.util.concurrent.Semaphore;

public class Test {
    private 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();
                    count++;
                    semaphore.release();
                }
            } 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();
        t2.join();

        System.out.println("count = " + count);
    }
}

在这里插入图片描述

CountDownLatch

当我们把一个任务拆分成多个的时候,可以通过这个工具类识别任务是否整体执行完毕了

IDM这种比较专业的下载工具就是多线程下载,把一个大的文件拆分成多个部分,每个线程都独立和人家服务器建立连接,分多个连接进行下载,等所有线程下载完毕之后,再对结果进行合并。
这时候就需要识别出所有线程是否都执行完毕了,此处就可以使用CountDownLatch

代码示例

  • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
  • 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
  • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了
import java.util.concurrent.CountDownLatch;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);//有10个线程
        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread t = new Thread(()-> {
                try {
                    //假设这里进行一些"下载"这样的耗时操作
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
                System.out.println("线程结束" + id);
                latch.countDown();
            });
            t.start();
        }
        //通过 await 等待所有的线程调用countDown
        //await 会阻塞等待到countDown调用的次数和构造方法指定的次数一致的时候,await才会返回
        latch.await();
        System.out.println("所有线程结束");
    }
}

在这里插入图片描述
await 不仅仅能替代 join,还可以判断任务是否全部完成

多线程环境使用ArrayList

Vector每个方法都有synchronized加锁
如果ArrayList这样没加锁的集合类想达到类似于Vector的效果就可以用Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的⼀个基于 synchronized 进行线程同步的 List.

CopyOnWriteArrayList(写时拷贝)也是一种解决线程安全问题的做法
假设有个数组有1,2,3,4这四个数据
多个线程读取,一个线程将2改为200,这样就有可能读取不到2,这是bug

我们就可以用原来的数组去读,新建一个数组去修改,写完之后用新的数组的引用代替旧的数组的引用(引用赋值的操作是原子的)

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。CopyOnWrite容器也是⼀种读写分离的思想,读和写不同的容器。

  • 优点:

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

  • 缺点:
  1. 占用内存较多.
  2. 新写的数据不能被第⼀时间读取到.

上述过程,没有任何加锁和阻塞等待,也能确保读线程不会读出"错误的数据"

有些服务器程序,需要更新配置文件/数据文件,就可以采取上述策略

显卡渲染画面到显示器就是按照写时拷贝的方式,在显示上一个画面的时候,在背后用额外的空间生成下一个画面,生成完毕就用下一个画面代替上一个画面

多线程使用 哈希表

HashMap 是不带锁的
Hashtable 虽然带锁,但线程不一定更安全,只是简单的把关键方法加上了 synchronized 关键字.

  • 一个Hashtable就只有一把锁,如果多线程访问同⼀个 Hashtable 就会直接造成锁冲突.
  • size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
  • ⼀旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低

标准库提供了更好的代替: ConcurrentHashMap

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是是用 synchronized, 但是不是"一把全局锁", 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率。不同线程针对不同的链表进行操作是不涉及锁冲突的(不涉及修改"公共变量",也就不涉及到线程安全问题),这样大部分的操作没有锁冲突,就只是偏向锁
  • 像size方法,即使插入/删除的元素是不同链表上的元素,也会涉及到多线程修改同一个变量。引入CAS的方式来修改size,提高了效率也避免了加锁的操作
  • 优化了扩容方式: 化整为零

HashMap要在一次put的过程中完成整个扩容的过程,就会使put操作效率变得很低。 ConcurrentHashMap在扩容的时候就会搞两份空间,一份是扩容之前的空间,一份是扩容之后的空间。后续每个来操作 ConcurrentHashMap 的线程,都会把一部分数据从旧空间搬运到新空间,分多次搬运。

搬的过程中:

  • 插入操作就插入到新的空间里面
  • 删除操作就是新的旧的空间里面的都要删除掉
  • 查找就是新的旧的空间都要查找

相关面试题

  1. 线程同步的方式有哪些?

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

  1. 为什么有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例,

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

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

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

  1. 谈谈 volatile关键字的用法?

volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第⼀时间读取到最新的值.

  1. Java多线程是如何实现数据共享的?

JVM 把内存分成了这几个区域:方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.

  1. 在多线程下,如果对⼀个数进行叠加,该怎么做?
  • 使用 synchronized / ReentrantLock 加锁
  • 使用 AtomInteger 原子操作.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

leetcode-每日一题

3101. 交替子数组计数https://leetcode.cn/problems/count-alternating-subarrays/ 给你一个 二进制数组 nums 。 如果一个 子数组 中 不存在 两个 相邻 元素的值 相同 的情况&#xff0c;我们称这样的子数组为 交替子数组 。 返回数组 nums 中交替子数组的数量。 示例 …

Linux字符设备驱动

一、字符设备驱动结构 1. cdev结构体 在Linux内核中&#xff0c;使用cdev结构体来描述一个字符设备 struct cdev {struct kobject kobj; //内嵌kobject对象struct module *owner; //所属的模块const struct file_operations *ops; //该设备的文件操作结构体struct list_head…

确认下单:购物车页面点击 去结算 按钮发起两个请求trade(显示购物车的商品信息和计算商品的总金额)findUserAddressList

文章目录 1、确认下单&#xff1a;购物车页面点击去结算1.1、在OrderController类中创建 trade 方法1.2、在CartController类中创建 checkedCartInfos1.3、CartServiceImpl 实现 checkedCartInfos的业务功能1.4、在service-cart-client模块下定义远程openFeign接口1.5、在SpzxO…

Java - 程序员面试笔记记录 实现 - Part3

4.1 线程与进程 线程是程序执行的最小单元&#xff0c;一个进程可以拥有多个线程&#xff0c;各个线程之间共享程序的内存空间以及一些进程级资源&#xff0c;但拥有自己的栈空间。 4.3 Java 多线程 方法一&#xff1a;继承 Thread 类&#xff0c;重写 run 方法&#xff1b;…

qt QGridLayout 简单实验1

1.概要 2.实验 2.1 实验1 简单实验跨行 2.1.1 代码 #ifndef WIDGET_H #define WIDGET_H#include <QWidget>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);~W…

Golang语法规范和风格指南(一)——简单指南

1. 前引 一个语言的规范的学习是重要的&#xff0c;直接关系到你的代码是否易于维护和理解&#xff0c;同时学习好对应的语言规范可以在前期学习阶段有效规避该语言语法和未知编程风格的冲突。 这里是 Google 提供的规范&#xff0c;有助于大家在开始学习阶段对 Golang 进行一…

如何魔改vnstat-docker项目使其支持每1分钟采样?

文章目录 一、概述二、官网参考1. 官网地址2. 查看打包过程3.打包命令 三、修改过的文件四、部署运行1. 编排文件2. 运行效果 一、概述 接前文 网络流量监控神器vnStat初探 我们已经了解了vnStat的作用、使用和docker部署。 同时也了解到官方版本支持的采样统计间隔最小为5分…

【Unity】unity学习扫盲知识点

1、建议检查下SystemInfo的引用。这个是什么 Unity的SystemInfo类提供了一种获取关于当前硬件和操作系统的信息的方法。这包括设备类型&#xff0c;操作系统&#xff0c;处理器&#xff0c;内存&#xff0c;显卡&#xff0c;支持的Unity特性等。使用SystemInfo类非常简单。它的…

Linux操作系统的引导过程

系统初始化进程与文件、systemd概述、单元类型、切换运行级别、查看系统默认默认运行、永久切换、常见的系统服务&#xff08;centos&#xff09;-CSDN博客 centos 7系统升级内核&#xff08;ELRepo仓库&#xff09;、小版本升级、自编译内核-CSDN博客 ss命令详细使用讲解文…

tongweb+ths6011测试websocket(by lqw)

本次使用的tongweb版本7049m4&#xff0c;测试包ws_example.war&#xff08;在tongweb安装目录的samples/websocket下&#xff09;&#xff0c;ths版本6011 首先在tongweb控制台部署一下ws_example.war,部署后测试是否能访问&#xff1a; 然後ths上的httpserver.conf的參考配…

游戏服务器搭建选VPS还是专用服务器?

游戏服务器搭建选VPS&#xff0c;VPS能够提供控制、性能和稳定性。它不仅仅是让游戏保持活力。它有助于减少延迟问题&#xff0c;增强您的游戏体验。 想象一下&#xff1a;你正沉浸在一场游戏中。 胜利在望。突然&#xff0c;屏幕卡住——服务器延迟。 很崩溃&#xff0c;对…

PageCache页缓存

一.PageCache基本结构 1.PageCache任务 PageCache负责使用系统调用向系统申请页的内存,给CentralCache分配大块儿的内存,以及合并前后页空闲的内存,整体也是一个单例,需要加锁. PageCache桶的下标按照页号进行映射,每个桶里span的页数即为下标大小. 2.基本结构 当每个线程的…

文件、文本阅读与重定向、路径与理解指令——linux指令学习(一)

前言&#xff1a;本节内容标题虽然为指令&#xff0c;但是并不只是讲指令&#xff0c; 更多的是和指令相关的一些原理性的东西。 如果友友只想要查一查某个指令的用法&#xff0c; 很抱歉&#xff0c; 本节不是那种带有字典性质的文章。但是如果友友是想要来学习的&#xff0c;…

Python 空间和时间高效的二项式系数(Space and time efficient Binomial Coefficient)

这里函数采用两个参数n和k&#xff0c;并返回二项式系数 C(n, k) 的值。 例子&#xff1a; 输入&#xff1a; n 4 和 k 2 输出&#xff1a; 6 解释&#xff1a; 4 C 2 等于 4!/(2!*2!) 6 输入&#xff1a; n 5 和 k 2 输出&#xff1a; 10 解释&#xff1a; 5 C …

moonlight+sunshine+ParsecVDisplay ipad8-windows 局域网串流

1.sunshine PC 安装 2.设置任意账户密码登录 3.setting 里 network启用UPNP IPV4IPV6 save apply 4.ParsecVDisplay虚拟显示器安装 5.ipad appstore download moonlight 6.以ipad 8 为例 2160*1620屏幕分辨率 7.ParsecVDisplay里面 custom设置2160*1620 240hz&#xff0c;…

python conda查看源,修改源

查看源 conda config --show-sources 修改源 可以直接vim .condarc修改源&#xff0c;

CSS中 实现四角边框效果

效果图 关键代码 border-radius:10rpx ;background: linear-gradient(#fff, #fff) left top,linear-gradient(#fff, #fff) left top,linear-gradient(#fff, #fff) right top,linear-gradient(#fff, #fff) right top,linear-gradient(#fff, #fff) left bottom,linear-gradient(…

CentOS7安装Mysql8.4.0

简介 本文介绍了Linux CentOS系统下Mysql8.4.0的下载和安装方法 环境 (rpm -q centos-release) centos-release-7-2.1511.el7.centos.2.10.x86_64 正文 一、去官网下载Mysql8.4.0 下载参考我另一篇mysql5.7.4的安装 CentOS7.9安装Mysql5.7-m14_centos下mysql5.7下载-CSDN博客…

flutter开发实战-Webview及dispose关闭背景音

flutter开发实战-Webview及dispose关闭背景音 当在使用webview的时候&#xff0c;dispose需要关闭网页的背景音或者音效。 一、webview的使用 在工程的pubspec.yaml中引入插件 webview_flutter: ^4.4.2webview_cookie_manager: ^2.0.6Webview的使用代码如下 初始化WebView…

AJAX-个人版-思路步骤整理版

前置知识&#xff1a;老式的web创建工程方法就是创建项目然后添加web工件&#xff0c;然后添加lib依赖如&#xff1a;tomcat,servlet&#xff0c;等。 传统请求 对于传统请求操作&#xff1a;整体流程也就是创建静态页面&#xff0c; <!DOCTYPE html> <html lang&q…