多线程强化(下)

简介: 多线程强化(下)

七、CountDownLatch



CountDownLatch:count down — 倒计时,latch — ( n. 门闩 )


我们可以将 CountDownLatch 机制想象成一个倒计时的仪器,在运动会的跑步比赛中,如果选手跑完所有路程,最终一定会 " 撞终点线 ",每个选手撞一次线,就有人会记录,或许是红外装置能够检测到。不管怎样,只要一个选手撞线了,说明一个人就跑完了赛道,那么就减一。而 CountDownLatch 也是一样的,只要一个任务完成了,利用 countDown() 方法计数一次,底层就减一。


程序清单7:


import java.util.concurrent.CountDownLatch;
public class Test {
    public static void main(String[] args) throws InterruptedException {
        //构造 CountDownLatch 实例, 括号中的 6 表示有 6 个任务需要完成
        CountDownLatch latch = new CountDownLatch(6);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("起跑!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //相当于计数器自减
                //每个任务完成之后,就减一次
                latch.countDown();
                System.out.println("撞线!");
            }
        };
        for (int i = 0; i < 6; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
        //任务在执行中的时候,await 方法阻塞等待,直到所有任务都执行完成
        latch.await();
        System.out.println("比赛结束!");
    }
}


输出结果:


9df43a4adff041d5a11902b24873c9f7.png


JUC 包中的常见类



JUC:java.util.concurrent ,concurrent ( adj. 并发的 )

当前介绍的 JUC 包中的这些东西,主要是在工作中用的比较多。但在面试中出现的不是特别多( 也是会出现,但相比于前面的内容,出现概率会少一些 )


  • ReentrantLock 类:可重入互斥锁,通常使用 lock 和 unlock 加锁解锁
  • 原子类:底层使用 CAS 机制实现,原子类具有原子性,可以避免加锁
  • 线程池:ThreadPoolExecutor 和 Executors,前者复杂版本,后者简单版本
  • 信号量:Semaphore
  • CountDownLatch类:倒计时计数器


八、Java 标准库中的线程安全类



这里的集合类,大部分是线程不安全的。( 不能在多线程环境下去并发地修改同一个对象 )


例如:


ArrayList

LinkedList

HashMap

TreeMap

HashSet

TreeSet

StringBuilder


但还有些是线程安全的。


例如:

Vector (不推荐使用)

Hashtable (不推荐使用)

ConcurrentHashMap

StringBuffer


其中 Vector 和 Hashrable 为很多方法都加上了 synchronized,而大多数情况下,在我们使用单线程的时候,synchronized 就会带来负面影响。

还有的虽然没有加锁,但仍然是线程安全的,例如:String,因为 String 类描述的是一个不可变对象,所以不涉及 “修改”,那么加锁和无锁并没有区别。


1. 多线程环境使用哈希表


在上面的时候,我们提到 HashMap 是线程不安全的类,而 Hashtable 是线程安全的类,但不推荐使用,那我们怎么才能折中一下呢,使用既是线程安全,又是可以用的类呢?


我们先来看看 Hashtable 类:


这是我从源代码中,查看的 Hashtable 类,它在底层中,只是简单地把关键的方法加上了 synchronized 关键字,我只截取了部分加锁的方法,而还有的一些方法加了 synchronized ,有些方法没有加。


f138fa90456f4bddbe16d8a4950e767c.png


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


那么问题就来了,一个 Hashtable 实际上就只有一把锁,多个线程访问 Hashtable 中的数据时,都会发生锁竞争,那么多个线程竞争同一把锁,一定会造成一个线程成功获取到锁,而其他线程阻塞等待,所以说,这个 Hashtable 带来的锁冲突还是比较激烈的,同时,发生锁冲突的概率也是比较大。


2. ConcurrentHashMap 类的优化


而 ConcurrentHashMap 实现的哈希桶是直接给每个哈希桶 ( 每个链表 ),分配了一个锁 ( 以每个链表的头结点对象作为锁对象 )


ConcurrentHashMap 相比于 Hashtable 做出了一系列的改进和优化,以 Java1.8 为例:


① 读操作没有加锁 ( 但使用了 volatile 保证从内存读取结果 ),只对写操作进行加锁,加锁的方式仍然是用 synchronized,但不是将整个对象加锁,而是 " 锁桶 " (用每个链表的头结点作为锁对象),大大降低了锁冲突的概率。


② 充分利用 CAS 机制,比如 size 属性通过 CAS 来更新,避免出现重量级锁的情况。


③ 优化了扩容方式,化整为零。发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去,扩容期间,新老数组同时存在。后续每个来操作 ConcurrentHashMap 的线程,都会参与搬运的过程,每个操作负责搬运一小部分元素,搬完最后一个元素后,再把老数组删掉。这个期间,插入只往新数组加,同时,查找需要同时查新数组和老数组。


90fb0cedd5dc46df873f7db5dffcbba3.png


3. 相关面试题


(1) ConcurrentHashMap 的读是否要加锁,为什么?


读操作没有加锁,目的是为了进一步降低锁冲突的概率,为了保证读到刚修改的数据,搭配了volatile 关键字。


(2) 介绍下 ConcurrentHashMap的锁分段技术?


这个是 Java1.7 中采取的技术,Java1.8 中已经不再使用了,简单地说,就是把若干个哈希桶分成一个 " 段 " (Segment),再针对每个段分别加锁,目的也是为了降低锁竞争的概率,当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争。显然,这并没有 Java1.8 所使用的 " 锁桶 " 的并发程度高。


(3) ConcurrentHashMap 在 JDK1.8 中做了哪些优化?


取消了分段锁,直接给每个哈希桶 ( 每个链表 ) 分配了一把锁,( 以每个链表的头结点对象作为锁对象 )


(4) HashMap、Hashtable、ConcurrentHashMap 之间的区别和联系


HashMap:线程不安全,key 允许为 null


Hashtable:线程安全,使用 synchronized 锁 Hashtable 对象,效率较低,key 不允许为 null


ConcurrentHashMap:线程安全,使用 synchronized 来锁每个链表头结点,锁冲突概率低,充分利用 CAS 机制,优化了扩容方式,key 不允许为 null


九、死锁



说明


下面的一些代码只是为了演示死锁的情况,可能表达方式上会有些不恰当的地方,但我旨在说明逻辑和对应的原理。


1. 第一种情况


一个线程一把锁


6b7a65e9ee224d18ac4355d4c24f8f56.png


synchronized 内部已经记录了当前的锁是由哪个线程持有的。因此当再次尝试加锁的时候,就会进行判定,看看当前尝试加锁的线程是否就是持有锁本身的线程。如果是,就不会阻塞,而是把引用计数给自增。所以说,由于 synchronized 内部的这个机制,就决定了它是一个可重入锁,因此,上面的代码实际上会被优化。


2. 第二种情况


两个线程两把锁


7b0d49d1ab774282a3726e80945f6867.png


3. 第三种情况


多个线程多把锁




4. 是什么造成了死锁


我们必须明确,死锁产生的四个必要条件:


① 互斥使用:即当资源被一个线程使用( 占有 )时,别的线程不能使用。

② 不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

③ 请求和保持:即当资源请求者在请求其他资源的同时,也保持对原有资源的占有。

④ 循环等待:起初,线程1 占有锁1 的资源,线程2 占有锁2 的资源。而接着线程1 尝试获取锁2,线程2 尝试获取锁1,这就陷入了循环等待,或者说休眠状态。


当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。其中最容易破坏的就是第④个条件,( 循环等待 ),而前三个条件,在大部分情况下,不好干预。


5. 如何避免死锁


① 尽量避免复杂的设计,避免在某个锁的代码中,再尝试获取其他锁,即尽量不要嵌套锁。


② 如果有些应用场景下,必须使用锁的嵌套,那么,一方面我们要保证持有锁的时间足够短,代码足够简单;另一方面要保证按照统一的顺序来进行加锁。而这样的固定顺序,其实就是破坏了死锁的 " 循环等待 " 条件。


4530133ad6c04a118ee07baee939dedb.png


比方说,在刚刚的 " 哲学家就餐的问题上 ",我们为筷子编号,A 先使用 12 筷子,C 先使用 34 筷子,E 先使用 56 筷子,等他们吃好了,再让其他人用这些筷子…这就破坏了循环等待,最起码,这保持了一些线程在同步执行!

目录
相关文章
|
开发工具 git C++
『实用教程』使用Visual Studio自带的Git管理回滚代码版本
使用Visual Studio自带的Git管理回滚代码版本
2165 0
『实用教程』使用Visual Studio自带的Git管理回滚代码版本
|
11月前
|
缓存 NoSQL 中间件
redis高并发缓存中间件总结!
本文档详细介绍了高并发缓存中间件Redis的原理、高级操作及其在电商架构中的应用。通过阿里云的角度,分析了Redis与架构的关系,并展示了无Redis和使用Redis缓存的架构图。文档还涵盖了Redis的基本特性、应用场景、安装部署步骤、配置文件详解、启动和关闭方法、systemctl管理脚本的生成以及日志警告处理等内容。适合初学者和有一定经验的技术人员参考学习。
756 7
|
15天前
|
存储 弹性计算 人工智能
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
2025年9月24日,阿里云弹性计算团队多位产品、技术专家及服务器团队技术专家共同在【2025云栖大会】现场带来了《通用计算产品发布与行业实践》的专场论坛,本论坛聚焦弹性计算多款通用算力产品发布。同时,ECS云服务器安全能力、资源售卖模式、计算AI助手等用户体验关键环节也宣布升级,让用云更简单、更智能。海尔三翼鸟云服务负责人刘建锋先生作为特邀嘉宾,莅临现场分享了关于阿里云ECS g9i推动AIoT平台的场景落地实践。
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
|
7天前
|
云安全 人工智能 安全
Dify平台集成阿里云AI安全护栏,构建AI Runtime安全防线
阿里云 AI 安全护栏加入Dify平台,打造可信赖的 AI
|
10天前
|
人工智能 运维 Java
Spring AI Alibaba Admin 开源!以数据为中心的 Agent 开发平台
Spring AI Alibaba Admin 正式发布!一站式实现 Prompt 管理、动态热更新、评测集构建、自动化评估与全链路可观测,助力企业高效构建可信赖的 AI Agent 应用。开源共建,现已上线!
931 29
|
9天前
|
机器学习/深度学习 人工智能 搜索推荐
万字长文深度解析最新Deep Research技术:前沿架构、核心技术与未来展望
近期发生了什么自 2025 年 2 月 OpenAI 正式发布Deep Research以来,深度研究/深度搜索(Deep Research / Deep Search)正在成为信息检索与知识工作的全新范式:系统以多步推理驱动大规模联网检索、跨源证据。
672 52