多线程(十):总结

本章用来处理一下之前遗漏的很多问题,在多线程那一章,很多常见面试题都没有讲,这里再来补充一下。

HashTable, HashMap, ConcurrentHashMap 之间的区别

HashTable, HashMap, ConcurrentHashMap 都带有Map,它们其实都是 Map 的接口,都是以键值对的 形式来存储数据。

HashMap

HashMap是在JDK1.2中引入的Map的实现类。

HashMap是基于哈希表实现的,其主要的特点有:

  1. HashMap 的键值对均可以为 null(当key 为null 时,哈希会被赋值为0)
  2. 初始的size 默认为 16,每次以二倍的形式扩容,最大值为 2^30 
  3. 底层使用的数据结构为:数组 + 链表 + 红黑树
  4. 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀;计算index方法:index = hash & (tab.length – 1)
  5.  HashMap 效率非常高,但线程不安全

HashTable

Hash table,叫做散列表(也叫哈希表),其主要特点有:

  1. 底层是由 数组 + 链表 实现的
  2. 无论是 key 还是 value 都是 不允许为 null 的
  3. 虽然线程是安全的,但是只是简单得用 synchronized 给所有方法加锁,相当于是对this加锁,也就是对整个HashTable对象进行加锁(非常无脑)                                      因为是 无脑加锁,所以Java官方并不推荐使用,而建议不涉及到线程安全问题时使用:HashMap,遇到线程安全问题时 使用:ConcurrentHashMap
  4. 实现线程安全的方式是在修改数据时锁住整个HashTable,所以效率非常低
  5. 初始size为11,扩容:newsize = olesize*2+1
    计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length【我查的】

ConcurrentHashMap

  1. 底层数据结构:数组 + 链表 + 红黑树
  2. ConcurrentHashMap 的键值不可以为null
  3. ConcurrentHashMap 最重要的点要说 线程安全

    ConcurrentHashMap 相比比较于HashTable 有很多的优化

核心思想就是降低 锁冲突的概率:

具体的优化手段有:

(1)锁粒度的控制

ConcurrentHashMap 不是锁整个对象,而是使用多把锁,对每个哈希桶(链表)都进行加锁,只有当两个线程同时访问同一个哈希桶时,才会产生锁冲突,这样也就降低了锁冲突的概率,性能也就提高了

(2) 只给读加锁,不给写加锁

我们知道 写会造成冲突,而只读不会有影响。

(3)充分利用到了CAS的特性

比如更新元素个数,都是通过CAS来实现的,而不是加锁

(4)ConcurrentHashMap 对于扩容操作,进行了特殊优化

HashTable的扩容是这样:当put元素的时候,发现当前的负载因子已经超过阀值了,就触发扩容。

扩容操作时这样:申请一个更大的数组,然后把这之前旧的数据给搬运到新的数组上

但这样的操作会存在这样的问题:如果元素个数特别多,那么搬运的操作就会开销很大

执行一个put操作,正常一个put会瞬间完成O(1)

但是触发扩容的这一下put,可能就会卡很久(正常情况下服务器都没问题,但也有极小概率会发生请求超时(put卡了,导致请求超时),虽然是极小概率,但是在大量数据下,就不是小问题了)

ConcurrentHashMap 在扩容时,就不再是直接一次性完成搬运了

而是搬运一点,具体是这样的扩容过程中,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运一部分到新的空间上,直到最终搬运完成,就释放旧的空间在这个过程中如果要查询元素,旧的和新的一起查询;如果要插入元素,直接在新的上插入;如果是要删除元素,那就直接删就可以了

具体的可以参考下面两篇博客:

 ConcurrentHashMap_亦安✘的博客-CSDN博客

死锁的成因, 和解决方案

什么是死锁

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

产生死锁的三个经典案例

案例一:一个线程 一把锁

一个线程正常操作,不会发生线程安全问题,如果是说,针对一个线程,多次加锁,那么就会产生问题。

// 第一次加锁成功

lock();

// 再次尝试对其加锁,原来的锁还未被释放

lock();

// 加锁失败,造成阻塞等待

像这样,第二次加锁,再等待第一次加锁的资源释放,第一次加锁释放又在等待第二次加锁的完成,于是只能造成死锁。

对可重入锁和不可重入锁的补充

 如果同一个线程在重复获取同一把锁的过程中,形成了死锁。这把锁又被称为不可重入锁。而可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁,不会出现死锁的情况。synchronized 是可重入锁

案例二:(两个线程,两把锁)

简单理解就是 车钥匙 在 家里 , 家钥匙 在 车里。

简单用伪代码来距离:

Object locker1 = new Object();
Object locker2 = new Object();


// 线程 T1
synchronized (locker1) {
    synchronized (locker2) {

    }
}


// 线程 T2
synchronized (locker2) {
    synchronized (locker1) {

    }
}

线程抢占式执行,假设 T1 和 T2 同时执行,T1拿到了 locker1 ,T2 拿到了locker2 ,都卡在了第一步,要想拿到 另一把锁,必须得让对方先释放,双方都无法释放,那么就造成了死锁。

案例三:(N个线程,M把锁)

哲学家吃面条问题

5位哲学家围着一张桌子,桌子上有几碗面条。这5位哲学家的左右手两边各有一根筷子(注意是一根,不是一双,两根筷子才是一双,才能拿来吃面,一根筷子无法吃面)

 5位哲学家相当于是5个线程,这些线程只有分别拿到左右手旁的两根筷子(各自要求的两把锁),才能完成进程,并释放自己所占用的锁。 

然后呢,在某一时刻,哲学家都想吃面条:他们同时拿起了自己右手边的那根筷子。5位哲学家、5根筷子,他们每个人都只拿了一根筷子(获取到了一个锁) 。于是他们每个人都完成不了各自的进程,也无法释放他们所占用的锁(筷子),都吃不到面条。

这又是一个死锁问题。

 解决办法

那么怎么解决呢?和上面死锁的解决方案相同——我们要分析为什么会出现死锁,就是因为线程对锁的互相等待,线程一要获取的锁被线程二占用着,但同时线程二要获取的锁又被线程一占用着,于是他们两个都无法获取到完整的锁,无法完成各自的进程,并释放锁。都处于一个循环等待的过程。

要解决死锁问题,重点就是解决循环等待问题。如果每个线程都按一定的顺序来获取对应的锁,比如在上面的栗子中,我们给5根筷子(5把锁)按从1到5的顺序进行编号,哲学家只能拿到到左右两边锁编号最小的那把锁。(已经拿到的锁不用进行编号的比较)

 形成死锁的四个条件

  1. 互斥性:当多个线程对同一把锁,有竞争。在某一时刻,最终只有一个线程可以拥有这把锁
  2. 不可抢夺性:当一个线程已经获取到了锁A,其他线程要想获取锁A,这个时候只能等该线程把A释放了之后再获取,不能中途抢夺别的线程的锁。
  3. 请求和保持性:当一个线程获取到了锁A,除非该线程自己释放锁A,否则该线程就一直保持占有锁A
  4. 循环等待性:在死锁中往往会出现,线程A等着线程B释放锁,同时线程B又在等着线程A来释放他所占有的锁,结果A、B的锁都无法正常释放,也都无法完成各自的进程,陷入了一个循环等待的状态

当上述四个条件某一条被破坏之后,死锁就解决了。

synchronized

synchronized的特点:

  1. 既是乐观锁,又是悲观锁
  2. 既是轻量级锁,又是重量级锁
  3. 轻量级锁基于自旋锁,重量级锁基于挂起等待锁
  4. 不是读写锁
  5. 是可重入锁
  6. 是非公平锁

synchronized关键字通常使用在下面四个地方:

  • synchronized修饰实例方法。

  • synchronized修饰静态方法。

  • synchronized修饰实例方法的代码块。

  • synchronized修饰静态方法的代码块。

锁升级

Java 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

如下图:

在这里插入图片描述

锁策略, cas 和 synchronized 优化过程

可以参考之前写过的文章:

 多线程(八):常见锁策略_我可是ikun啊的博客-CSDN博客

synchronized 和 ReentrantLock 之间的区别

相同点:

  1. synchronized 和 ReentrantLock 都是 Java 中提供的可重入锁

不同点:

  1. 用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块;ReentrantLock 只能用于代码块;
  2. 获取和释放锁的机制不同:进入synchronized 块自动加锁和执行完后自动释放锁; ReentrantLock 需要显示的手动加锁和释放锁;
  3. 锁类型不同:synchronized 是非公平锁; ReentrantLock 默认为非公平锁,也可以手动指定为公平锁;
  4. 响应中断不同:synchronized 不能响应中断;ReentrantLock 可以响应中断,可用于解决死锁的问题;
  5. 底层实现不同:synchronized 是 JVM 层面通过监视器实现的;ReentrantLock 是基于 AQS 实现的。

线程池的执行流程和拒绝策略

线程池的执行流程:

  1. 当新加入一个任务时,先判断当前线程数是否大于核心线程数,如果结果为 false,则新建线程并执行任务;
  2. 如果结果为 true,则判断任务队列是否已满,如果结果为 false,则把任务添加到任务队列中等待线程执行
  3. 如果结果为 true,则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务
  4. 如果结果为 true,执行拒绝策略。

拒绝策略:

  1. AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务;
  2. CallerRunsPolicy:把任务交给添加此任务的线程来执行;
  3. DiscardPolicy:忽略此任务(最新加入的任务);
  4. DiscardOldestPolicy:忽略最先加入队列的任务(最老的任务)。

线程池的执行流程图:

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值