【多线程】线程安全的集合类 与 死锁

请添加图片描述

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇


💤一. 线程安全的集合类

原来的集合类, 大部分都不是线程安全的.

Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的(ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder)
 
此处可以具体参照我写的 synchronized 关键字文章里的原因👉点击我直接传送👈


❔1. 多线程环境使用 ArrayList

  1. 自己使用同步机制 (synchronized 或者 ReentrantLock)

看我上篇文章详解ReentrantLock 👉点我传送👈

  1. Collections.synchronizedList(new ArrayList);
  • synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
  • synchronizedList 的关键操作上都带有 synchronized
  1. 使用 CopyOnWriteArrayList
  • CopyOnWrite容器即写时复制的容器。
  • 如果出现修改操作,就把 ArrayList 进行复制
  • 先拷贝一份数据,新线程修改副本,再用副本替换原有的数据(拷贝的成本可能会很高,当元素过多的时候!)

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

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

 
缺点:

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

❓2. 多线程环境使用哈希表

  • HashMap(线程不安全的)
  • Hashtable(线程安全的,不推荐使用)
  • ConcurrentHashMap(推荐使用的)
  1. Hashtable

只是简单的把关键方法加上了 synchronized 关键字.

这相当于直接针对 Hashtable 对象本身加锁

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

一把锁锁住了很多个哈希桶,每个哈希桶上面又有一个链表,假设俩链表上俩元素,如果多个线程要操作这俩个元素,因为是被一把大锁锁住了,就会产生竞争。但是仔细分析之后发现,这俩操作在不同的哈希桶中,之间并不相关。对于两个不同的哈希桶上的元素,不牵扯修改同一个变量,因此就不会发生线程安全问题。如果两个修改落到同一个哈希桶上才会产生线程安全风险。因此 ConcurrentHashMap 相比之下做出了重大改进!

一个 Hashtable 只有一把锁,两个线程访问 Hashtable 中任意数据都会产生锁竞争

  1. ConcurrentHashMap

ConcurrentHashMap 把锁的粒度细化了,在每一个哈希桶上都加了锁。

ConcurrentHashMap 每个哈希桶都有一把锁,只有两个线程访问的恰好是同一个哈希桶上的数据才会产生锁冲突

优化特点:

  1. 把锁的粒度进行细分了,每个哈希桶,都有一把锁(每个链表的头结点),降低了锁冲突的概率。(最重要)
  2. 激进的操作,读没加锁,写才加锁。
  3. 更充分的使用了 CAS 特性,更高效的操作,比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况
  4. 针对扩容场景进行了优化(化整为零)
  • 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
  • 扩容期间, 新老数组同时存在.
  • 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.
  • 搬完最后一个元素再把老数组删掉.
  • 这个期间, 插入只往新数组加.
  • 这个期间, 查找需要同时查新数组和老数组

上述都是基于 Java 8 的,在 Java 1.7 里不是每个桶一个锁,而是 “分段锁”,一个锁管若干个桶。(在 Java 8 之前都是 Java 1.x ,到了 Java 8 及其以后就省去了 1.,如 Java 8,Java 9,…)

面试题:
 
Hashtable和HashMap、ConcurrentHashMap 之间的区别?
 
HashMap 的 key 允许为 null,其他的两个不能为 null (错误回答!!!是区别,但不是主要区别)
 
主要在线程安全上回答:

  1. HashMap 线程不安全的,Hashtable 和 ConcurrentHashMap 是线程安全的!
  2. Hashtable 是使用一把大锁,锁冲突的概率很高,ConcurrentHashMap 则是每个哈希桶一把锁,锁冲突概率大大降低了。
  3. 详细说 ConcurrentHashMap 其他的优化策略。
  4. HashMap key 允许为 null,另外两个不允许。

💦二. 死锁

❗️1. 死锁是什么

尝试加锁的时候发现上次锁没有及时释放(因为一些原因,bug),导致加锁加不上

死锁是多线程代码中常见的 bug

  1. 一个线程一把锁(可重入锁)

线程 1 针对锁 A 连续加锁两次,如果是不可重入锁,就死锁

  1. 两个线程两把锁

线程 1 获取到锁 A
线程 2 获取到锁 B
线程 1 尝试获取锁 B,线程 2 尝试获取锁 A,就发生死锁了。

  1. N 个线程 M 把锁

“哲学家就餐问题”:
 
①:有个桌子, 围着一圈 哲学家, 桌子中间放着一盘意大利面. 每个哲学家两两之间, 放着一根筷子.
②:每个 哲学家 只做两件事: 思考人生 或者 吃面条. 思考人生的时候就会放下筷子. 吃面条就会拿起左右两边的筷子(先拿起左边, 再拿起右边).
③:如果 哲学家 发现筷子拿不起来了(被别人占用了), 就会阻塞等待.
④:[关键点在这] 假设同一时刻, 五个 哲学家 同时拿起左手边的筷子, 然后再尝试拿右手的筷子, 就会发现右手的筷子都被占用了. 由于 哲学家 们互不相让, 这个时候就形成了 死锁

在多个线程多把锁的情况下,死锁是一个概率性问题,绝对不能忽视


❕2. 如何避免死锁

由上总结死锁的四个条件:

  • 互斥使用:线程 1 拿到锁 A ,其他线程无法获取到 A 。
  • 不可抢占: 线程 1 拿到锁 A ,其他线程只能阻塞等待,等到线程 1 主动释放锁,而不是强行把锁抢走
  • 请求和保持:当线程 1 拿到锁 A 之后,就会一直持有这个获取到锁的状态,直到主动释放
  • 循环等待:线程 1 等待 线程 2,线程 2 又尝试等待线程 1。(和代码编写密切相关)

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。其中最容易破坏的就是 “循环等待”.

我们该如何打破循环等待呢?

针对多把锁,进行编号 1 2 3 4 …,约定在获取多把锁的时候,要明确获取锁的顺序是从小到大的顺序。比如线程要拿到 1 2 两把锁,就先获取 1 再获取 2 ,如果要拿到 2 4 两把锁,就先获取 2 再获取 4 。只要所有的线程都遵守这个顺序,就不会死锁!!就不会出现循环等待!!!(简单并且靠谱)

当以后学习操作系统,也会涉及到哲学家就餐 / 死锁问题,教科书上也会有一个避免死锁的办法,“银行家算法” ,也能解决死锁问题,但是非常复杂,不建议使用。

  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Redamancy丶早晚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值