多线程部分总结

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

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

ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用

CAS 机制. 优化了扩容方式. key 不允许为 null

死锁的成因, 和解决方案.

        

死锁是什么

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线 程被无限期地阻塞,因此程序不可能正常终止。死锁是一种严重的 BUG!! 导致一个程序的线程 "卡死", 无法正常工作!

如何避免死锁

死锁产生的四个必要条件:

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

不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样 就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让 死锁消失。

        

最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号

(1, 2, 3...M).

N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.

总结synchronized

 Synchronized 具有以下特性(只考虑 JDK 1.8):

    1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
    2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
    3. 实现轻量级锁的时候大概率用到的自旋锁策略
    4. 是一种不公平锁
    5. 是一种可重入锁
    6. 不是读写锁                                                                                                                                                                                                                                                            加锁工作过程                                                                                                                       JVM synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。

 

synchronized的锁机制

锁消除

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除

锁粗化

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化. 

 常见的锁策略

乐观锁 vs 悲观锁

悲观锁:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这 样别人想拿这个数据就会阻塞直到它拿到锁。

乐观锁:

假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并 发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.

读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。读写锁特别适合于 "频繁读, 不频繁写" 的场景中. (这样的场景其实也是非常广泛存在的).Synchronized 不是读写锁.

重量级锁 vs 轻量级锁

锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.

重量级锁: 加锁机制重度依赖了 OS 提供了 mutex

大量的内核态用户态切换很容易引发线程的调度

这两个操作, 成本比较高. 一旦涉及到用户态和内核态的切换, 就意味着 "沧海桑田".

轻量级锁: 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.

少量的内核态用户态切换. 不太容易引发线程调度.

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.

自旋锁(Spin Lock

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃    CPU,需要过很久才能再次被调度.

但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.

一旦锁被其他线程释放, 就能第一时间获取到锁.synchronized   中的轻量级锁策略大概率就是通过自旋锁的方式实现的.

公平锁 vs 非公平锁

公平锁: 遵守 "先来后到". B C 先来的. A 释放锁的之后, B 就能先于 C 获取到锁.

非公平锁: 不遵守 "先来后到". B C 都有可能获取到锁.

synchronized 是非公平锁.

可重入锁 vs 不可重入锁

可重入锁的字面意思是可以重新进入的锁,即允许同一个线程多次获取同一把锁

比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入 (因为这个原因可重入锁也叫做递归锁

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括

synchronized关键字锁都是可重入的。

Linux 系统提供的 mutex 是不可重入锁.synchronized 是可重入锁

CAS

CAS: 全称Compare and swap,字面意思:”比较并交换,一个 CAS 涉及到以下操作: 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B

    1. 比较 A V 是否相等。(比较)
    2. 如果比较相等,将 B 写入 V。(交换)
    3. 返回操作是否成功。

CAS 是怎么实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

java CAS 利用的的是 unsafe 这个类提供的 CAS 操作;

unsafe CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。

简而言之,是因为硬件予以了支持,软件层面才能做到

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值