> 文章列表 > 多线程小知识

多线程小知识

多线程小知识

线程

  • 多线程环境使用哈希表
    • HashMap
    • Hashtable
    • ConcurrentHashMap
  • 死锁
    • 死锁是什么
      • 两者之间的死锁
      • 多人之间的死锁
    • 死锁产生的四个必要条件:
      • 互斥使用
      • 不可抢占
      • 请求和保持
      • 循环等待
  • 不同的锁
    • ReentrantLock
    • Synchronized和ReentrantLock之间的区别:
    • 具体选择
    • Synchronized
    • 加锁工作过程
      • 偏向锁
      • 轻量级锁
      • 重量级锁
      • 锁消除
      • 锁粗化
  • 线程池
    • ThreadPoolExecutor构造方法
    • 执行流程
  • CAS && ABA
      • CAS
      • ABA

前几次给小小的给多线程做了一些小铺垫,现在我们就来稍微唠唠多线程中一些比较有意思的知识!

多线程环境使用哈希表

HashMap

原来我们学习的集合类,大部分都不是线程安全的,HashMap也不是线程安全的类,为此我们搞出了Hashtable以及ConcurrentHashMap

Hashtable

HashTable知识简单粗暴地给一些重要方法加上了锁,换言之,只要在多线程环境下使用了Hashtable,就会有阻塞的情况发生.
多线程小知识
多线程小知识

我们之间说过了只要是使用了synchronized,那么你写出来的代码大概率就和高效无缘了,之前我们写懒汉模式的时候,还要处处计较,我们当然可以无脑加锁,但是那样子就不专业了.
这边给put和get方法加锁,从效果上来看其实就是给Hashtable对象加锁了,我们使用的较多的方法就是这几个,所以一个Hashtable对象只有一把锁,锁冲突就会比较激烈,于是乎ConcurrentHashMap应运而生.

ConcurrentHashMap

我们要知道一个普通的哈希表的结构是,每个节点下面会挂着一串链表,每当我们想要插入或者是查询的时候,都要对数据进行哈希,找到对应链表的起始位置,然后慢慢遍历.
我们的ConcurrentHashMap并不是针对整个对象进行加锁,而是细化到了针对到了每个链表,只有当对同一个链表进行和写有关的操作的时候,才会发生锁竞争,这也就一定程度上提高了效率.

死锁

我们之前遇到到死循环和死递归,死锁就是把自己给锁死了.

死锁是什么

两者之间的死锁

我把车钥匙放在家里了,又把家里的钥匙放在了车里.
这样子是不是两者就僵住了啊.

多人之间的死锁

这里比较经典的问题就是哲学家就餐问题:
链接: 哲学家就餐
简单说一下,一座子的人都比较死板,吃饭都要先拿左手边的筷子,然后再拿右手边的筷子,同时这一桌子的人都比较绅士,如果右手边的筷子被占用,那么就要等到筷子不被占用为止,只有吃好了,他们才会放下筷子.
多线程小知识

这样子如果极端情况下,这一桌子老哥同时拿起左手边的筷子,那么这一桌子的人这辈子都没办法吃完这一顿饭了.

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

互斥使用

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

不可抢占

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

请求和保持

当资源请求者在请求其他的资源的同时保持对原有资源的占有。

循环等待

即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路.
我们想要破局只要把上面任意一点突破了就行.
就比如哲学家就餐问题,我们只要稍加变通,给每根筷子编号,把拿起走左手边的改成拿起号码小的,这样子就可以了.

不同的锁

ReentrantLock

这把锁代表着可重入锁,比我们之前的synchronized多了几个方法:
lock(): 加锁, 如果获取不到锁就死等.
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
unlock(): 解锁

Synchronized和ReentrantLock之间的区别:

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

具体选择

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

Synchronized

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

加锁工作过程

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。

偏向锁

并不是真正加锁,只是在内部给了一个标志位,避免了部分开销

轻量级锁

轻量级锁大概率是通过自旋来实现的,竞争并不是很激烈,自选一会也就拿到了

重量级锁

竞争比较激烈了,自旋也不好使了,开销还大,那就老老实实挂起等待吧

锁消除

就比如在单线程环境下使用StringBuffer,本身单线程就没有多线程安全那一套,那么这里的synchronized那一套只会平白徒增消耗,我们就把它给优化掉了

锁粗化

这里的粗和细指的是锁的粒度,也就是加锁时间的长度,如果加锁粗,那么同一段代码下我们进行加锁的次数也就少了,这样子开销也就少了,我个人感觉就是如果一段代码需要加锁的地方比较多,那么我们可以使用锁粗化,避免频繁加锁销毁锁带来的开销.

线程池

线程是轻量级进程,轻量就体现在除了第一次调用线程,线程的创建调度销毁所需要的开销都远远小于进程,但是凡事都是相对的,我们的线程池所需要的开销又要小于线程,池这个概念我们并不陌生,之前我们讲的字符串常量池也是一种池.
所谓池就是一盘大杂烩,就线程池来讲,你创建好的线程都会放在线程池里面,即使后面你想要销毁线程也不会真正销毁,想要调度创建线程了,首先先去线程池里面看看,看看之前有没有创建过线程,这样子又减少了一部分开销,但是这样的存储方式必然会占用内存

ThreadPoolExecutor构造方法

多线程小知识
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

  1. corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)
  2. maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).
  3. keepAliveTime: 临时工允许的摸鱼时间.
  4. unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
  5. workQueue: 传递任务的阻塞队列
  6. threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
  7. RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.
    AbortPolicy(): 超过负荷, 直接抛出异常. 把球丢给程序猿,您自己来
    CallerRunsPolicy(): 调用者负责处理
    DiscardOldestPolicy(): 丢弃队列中最老的任务.
    DiscardPolicy(): 丢弃新来的任务.

执行流程

多线程小知识

CAS && ABA

CAS

CAS:compare and swap
多线程小知识
CAS也可以从某种程度上保证原子性的问题,因为CAS整个工作流程是一条指令:如果我们取到的内存的值和我们所期望的值一样,那么首先第二个参数和第三个参数就会交换,然后再把第二个参数写回到第一个参数里面.
实际上我们要保证CAS的原子性还要基于一部分的代码
多线程小知识
只有当CAS返回为true的时候才会交换,但实际上由于多线程,不可能每一次我们我们拿到的值都和期待的一样,这样子我们就会持续进入循环,每次都会要再去获取一遍内存中的值,这样子不论如何最终我们都会执行到一次CAS

ABA

ABA问题其实也是走了一个极端,加入就在我们刚刚执行完一次CAS的同时,我们又有一个操作把本该结束循环的条件又给修改了,这样子这次CAS我们又会多执行一次,是不是听上去并没有什么的,那如果是转账呢?我们先转了10万给别人,然后同时我们又收到了十万块钱,循环的条件是money!=10万,显然我们还会执行第二次,这不就是天大的问题了吗?
如何解决ABA问题也不难,每次我们针对内存进行操作完成之后,再给版本号进行自增如果寄存器中的版本号小于内存中的版本号,那么这次操作就作废

好了今天的内容差不多就到这里了,我希望我能通过最简单的语言教会给你们基本内容.
百年大道,你我共勉!