一篇文章带你搞懂多线程面试相关的一些问题

目录

1.Callable接口

1.1使用Callable接口来创建线程

1.1相关面试题:

介绍下 Callable 是什么

2.JUC常见的类(java.util,concurrent)

2.1ReentrantLock

 ReentrantLock和sychronized的区别

3.信号量

4.CountDownLatch

5.线程安全的集合类

5.1多线程下使用ArrayList

5.2多线程下使用队列

5.3多线程环境使用哈希表

5.3.1 Hashtable

5.3.2 CoucurrentHashMap

5.4一些面试题

5.4.1.ConcurrentHashMap的读是否要加锁,为什么?

5.4.2ConcurrentHashMap在jdk1.8做了哪些优化?

5.4.3Hashtable和HashMap、ConcurrentHashMap 之间的区别?


1.Callable接口

1.1使用Callable接口来创建线程

  我们在创建一个线程的时候有以下几种方法:
1.继承Thread类

2.实现runnable接口

3.使用lambda表达式

4 基于callable

5基于线程池

在这里面 Runnable关注的是过程而不是结果,它并没有返回值,所以如果别的地方要用到它的返回值,就得使用别的办法 如:

 public static int b = 0;
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (int i = 1; i <=10 ; i++) {
                    a+=i;
                }
                b = a;
            }
        };
        runnable.run();
        System.out.println(b);
    }

我们希望计算从1~10的累加,但是Runnable接口并没有返回值,所以我们得用一个成员变量来赋值才行,这样写固然可以,但是不够优nia

我们。可以用别的方法来创建一个带返回值的线程:

 public static void main(String[] args) throws Exception {
//创建的时候可以指定返回值类型
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int a = 0;
                for (int i = 1; i <=10 ; i++) {
                    a+=i;
                }
                return a;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable); //使用FutureTask类来接收callable对象
        Thread t = new Thread(futureTask); //将futureTask对象传给Thread里面
        t.start();
        int n = futureTask.get();
        System.out.println(n);
    }

理解Callable

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

1.1相关面试题:

介绍下 Callable 是什么


Callable 是一个 interface . 相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算
结果.
Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务.

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

2.JUC常见的类(java.util,concurrent)

2.1ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock 也是可重入锁. "Reentrant" 这个单词的原意就是 "可重入
 

ReentrantLock 的用法:
lock(): 加锁, 如果获取不到锁就死等.
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
unlock(): 解锁
 

 public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {
            //主要的逻辑代码
        }finally {
            lock.unlock();
            //注意要释放锁
        }
    }

面试题:

 ReentrantLock和sychronized的区别

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


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

3.信号量

  信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器
用一个通俗易懂的例子来理解信号量:

信号量就是,我去饭店吃饭,饭店里面的座位数,要是座位被坐满了。那么我就进不去了,一旦有人离开了座位(v操作)那么就是释放资源,这个时候。我就可以进去了,而不是在门口阻塞等待。

而我进去了以后坐上座位,属于申请资源。(p操作),可用资源数(座位个数)就减少了。一旦到了0,那么就不能在被申请了。

Java中 使用Semphore类来封装了信号量机制

代码实例:

首先我们可以不用信号量 用两个线程来分别对count++ 

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

 得到结果:

可以看出和我们预期的结果是不一样的,因为两个抢占式运行,并且count++这个操作并非是原子性的,所以会这样。

接下来我们引用信号量机制 ,并且给它的初始值设为1

public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore =new Semaphore(1);

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                semaphore.release();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                semaphore.release();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);

无论我们执行多少次 结果都是我们预期的。

4.CountDownLatch


  同时等待 N 个任务执行结束.
好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩
构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
 

        public static void main(String[] args) throws Exception {
            CountDownLatch latch = new CountDownLatch(10);
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep((long) (Math.random() * 10000));
                        latch.countDown();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            for (int i = 0; i < 10; i++) {
                new Thread(r).start();
            }
          // 必须等到 10 人全部回来
            latch.await();
            System.out.println("比赛结束");
        }

5.线程安全的集合类

原来的集合类, 大部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的

5.1多线程下使用ArrayList

1.使用synchronized或者ReentrantLock

2.Collections.synchronizedList(new ArrayList);
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized

3.使用 CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,
复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会
添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
优点:
在读多写少的场景下, 性能很高, 不需要加锁竞争.
缺点:

1. 占用内存较多.
2. 新写的数据不能被第一时间读取到
 

5.2多线程下使用队列

1.ArrayBlockingQueue 基于数组实现的阻塞队列 是线程安全的

2,LinkedBlockingQueue 基于链表实现的阻塞队列

3.PriorityBlockingQueue 基于堆实现的 带有优先级的阻塞队列

4.TransferQueue 最多只包含一个元素的阻塞队列

5.3多线程环境使用哈希表

HashMap并不是线程安全的

在多线程下可以使用:

Hashtable

CoucurrentHashMap

5.3.1 Hashtable

把关键方法都加上了synchornized关键字

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

5.3.2 CoucurrentHashMap

相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例
读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然
是是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 大大降
低了锁冲突的概率.
充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
优化了扩容方式: 化整为零
发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
扩容期间, 新老数组同时存在.
后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小
部分元素.
搬完最后一个元素再把老数组删掉.
这个期间, 插入只往新数组加.
这个期间, 查找需要同时查新数组和老数组
 

每个链表都有一把单独的锁

5.4一些面试题

5.4.1.ConcurrentHashMap的读是否要加锁,为什么?

不需要锁,目的是为了进一步降低锁冲突的概率,为了保证读到刚修改的关键字,可以使用volatile关键字

5.4.2ConcurrentHashMap在jdk1.8做了哪些优化?

取消了分段锁,直接给每个哈希桶都分配了一把锁。

将原来数组+链表的方式改进成了数组+链表/红黑树的形式。当数组长度大于64,链表长度大于8 到时候就会转变为红黑树

5.4.3Hashtable和HashMap、ConcurrentHashMap 之间的区别?

首先,HashMap并不是线程安全的,而Hashtable和ConcurrenHashMap都是线程安全的。

而Hashtable是很简单粗暴的在它里面的方法上直接加sychornized关键字,并且整个哈希表也只有一个锁。这会引起一系列的性能问题。

CoucurrentHashMap就很聪明的,在哈希表的每个哈希桶(链表)上加上一把锁,并且将扩容这个操作给优化了,它并不是直接一次性扩容到一个新的哈希表中在把旧表里的数据在哈希到新表里,而是在每次插入操作时,分批进行操作。这就避免了突然扩容导致性能需求急剧增加,导致服务器卡死的状况发生。

还有就是HashMap key允许为null

而Hashtable和ConcurrentHashMap的key值是不允许为null的。

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

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

相关文章

Flink-容错机制

Flink中的容错机制 流式数据连续不断地到来&#xff0c;无休无止&#xff1b;所以流处理程序也是持续运行的&#xff0c;并没有一个明确的结束退出时间。机器运行程序&#xff0c;996 起来当然比人要容易得多&#xff0c;不过希望“永远运行”也是不切实际的。因为各种硬件软件…

ioDraw在线图表工具 - 轻松制作专业图表,只需3步!

还在花大量时间手动画图表&#xff1f;还在为图表样式而烦恼&#xff1f;ioDraw为你提供一站式解决方案&#xff01;ioDraw在线图表工具实现了AI自动生成图表&#xff0c;让你轻松制作专业图表&#xff0c;只需3步&#xff01; 1. 录入数据 只需将你的数据告诉ioDraw AI助手&…

alibaba.item_get API:电商行业中的数据驱动决策支持

alibaba.item_get API 是阿里巴巴提供的一个用于获取商品详情的接口。在电商行业中&#xff0c;数据驱动的决策支持是非常重要的&#xff0c;而这个 API 可以帮助你获取到商品的各种详细信息&#xff0c;从而为你的决策提供支持。 具体来说&#xff0c;通过使用 alibaba.item_…

【Oracle】期末复习题

目录 一. 单选题&#xff08;共164 题&#xff09; 二. 多选题&#xff08;共14 题&#xff09; 三. 填空题&#xff08;共4 题&#xff09; 四. 分析题&#xff08;共五题&#xff09; 一&#xff09;考生子系统 三&#xff09;考试存储方案 四&#xff09;铁路12306 …

条款24:若所有参数皆需类型转换,请为此采用非成员函数

设计一个表示有理数的类时&#xff0c;允许从整数隐式转换为有理数是有用的&#xff1a; class Rational { public:Rational(int numerator 0, // 该构造函数没有explicit限制;int denominator 1); int numerator() const; int denominator() const; const Rational opera…

CAS的超~详细介绍

什么是CAS CAS全称Compare and swap,是一种比较特殊的CPU指令. 字面意思:"比较并交换", 一个CAS涉及到以下操作: 我们假设内存中的原数据为V,旧的预期值A,需要修改的新值B. 1.比较A和V是否相等(比较) 2.如果相等,将B写入V.(交换) 3.返回操作是否成功. 伪代码 下面…

基于位的权限系统

基于位的权限系统是一种利用二进制位运算进行权限管理的技术。在这种系统中&#xff0c;不同的权限被编码为2的幂次方 (例如1、2、4、8等)&#xff0c;每个权限对应一个独立的二进制位&#xff08;可想而知运算速度是非常快的&#xff09;。通过将这些权限值组合在一起形成一个…

day13 滑动窗口最大值 前K个高频元素

题目1&#xff1a;239 滑动窗口最大值 题目链接&#xff1a;239 滑动窗口最大值 题意 长度为K的滑动窗口从整数数组的最左侧移动到最右侧&#xff0c;每次只移动1位&#xff0c;求滑动窗口中的最大值 不能使用优先级队列&#xff0c;如果使用大顶堆&#xff0c;最终要pop的…

React Native集成到现有原生应用

本篇文章以MacOS环境开发iOS平台为例&#xff0c;记录一下在原生APP基础上集成React Native React Native中文网 详细介绍了搭建环境和集成RN的步骤。 环境搭建 必须安装的依赖有&#xff1a;Node、Watchman、Xcode 和 CocoaPods。 安装Homebrew Homebrew是一款Mac OS平台下…

Java SE入门及基础(15)

Java 中的标号&#xff08;标签 label&#xff09; 1. 语法规则 标号名称 : 循环结构 2. 作用 标号的作用就是给代码添加一个标记&#xff0c;方便后面使用。通常应用在循环结构中&#xff0c;与break 语句配合使用 3. 应用场景 有如下菜单&#xff1a; 实现其中返回主菜…

C++ | 四、指针、链表

指针 指针用来储存地址定义方式&#xff0c;int *ptr;&#xff0c;使用*来表示所定义的变量是指针取地址符&#xff0c;ptr &a;&#xff0c;通过&来取得一个普通变量的地址&#xff0c;并储存到指针中取值&#xff08;解引用&#xff09;&#xff0c;想要取得一个指针…

qt.qpa.plugin: Could not find the Qt platform plugin “windows“ in ““

系统环境&#xff1a;Win10家庭中文版 Qt : 5.12.9 链接了一些64位的第三方库&#xff0c;程序编译完运行后出现 qt.qpa.plugin: Could not find the Qt platform plugin "windows" in "" 弹窗如下&#xff1a; 网上搜了一些都是关于pyQt的&#xff0c…

C++刷题 -- 栈和队列

C刷题 – 栈和队列 文章目录 C刷题 -- 栈和队列1.用栈实现队列2.用队列实现栈3.有效的括号4.前 K 个高频元素 1.用栈实现队列 力扣链接 一个栈自然实现不了队列功能&#xff0c;需要使用两个栈一个输入栈&#xff0c;一个输出栈队列是先入先出&#xff0c;当队列push操作&…

外包干了4年,废了···

有一说一&#xff0c;外包没有给很高的薪资&#xff0c;是真不能干呀&#xff01; 先说一下自己的情况&#xff0c;大专生&#xff0c;19年通过校招进入湖南某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0…

专业、高效的解决方案:为企业业务提供强大支持

随着市场竞争的日益激烈&#xff0c;企业要想在众多竞争对手中脱颖而出&#xff0c;必须具备强大的核心竞争力。而这种竞争力的源泉&#xff0c;往往来自于企业所提供的产品和服务。为了确保产品和服务的质量&#xff0c;企业需要不断地进行技术创新和管理创新&#xff0c;以满…

pytest pytest-cov生成代码覆盖率报告

pytest-cov 是一个用于 pytest 的插件&#xff0c;它可以生成代码覆盖率报告。代码覆盖率是一个度量&#xff0c;表示在测试过程中执行了代码的哪些部分。这是一个非常有用的工具&#xff0c;因为它可以帮助你理解你的测试是否全面&#xff0c;是否有遗漏的代码部分。 pytest-c…

性能测试jmeter

选的这些怎么添加 在一个列表里面 方法调用${__time(YMD)} 两个下划线&#xff0c;后跟函数名&#xff0c;小括号内是输入参数&#xff0c;整个用大括号包裹。 注意POST一定要在消息体数据里面写,不能再参数里面 否则报错:loginOut,没cookie等

express服务连接mysql数据库

下载mysql2依赖包 npm i mysql2 创建mysql连接实例并暴露出去 const mysql require(mysql2)const mysqlMode mysql.createPool({host: 127.0.0.1, //服务端hostuser: root, //用户名称,mysql一般默认rootpassword: 123456, //密码database: sqlTest1, //数据库名字…

了解Dubbo配置:优先级、重试和容错机制的秘密【五】

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 了解Dubbo配置&#xff1a;优先级、重试和容错机制的秘密【五】 前言Dubbo高级配置概述不同配置覆盖关系重试与容错处理机制负载均衡机制 前言 Dubbo作为一款强大的分布式服务框架&#xff0c;其高级…

【Linux】Git - 新手入门

文章目录 1. git 版本控制器 - 该如何理解&#xff1f;2. git / gitee / github 区别&#xff1f;3. Linux 中 git 的使用3.1 安装 git3.2 使用 github 新建远端仓库3.2.1 账号注册3.2.2 创建代码仓库3.2.3 克隆仓库到本地3.2.4 .gitignore 文件 3.3 使用 git 提交代码到 githu…