• 多线程高频考点(如何解决死锁,谈谈HashMap HashTable ConcurrentHashMap的区别)


    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


    CountDownLatch

    类似于一个跑步比赛,当比赛开始的时候,这个比赛啥时候结束,当最后一个选手撞线就结束
    JUc提供了一个类来解决上述问题
    使用的时候先设置一下有几个选手
    每个选手撞线了,就调用一下countDown方法(计数器–,线程阻塞)
    当撞线的次数达到了选手的个数,就任务比赛结束了(await被唤醒)

      public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(10);
            for (int i = 0; i < 10; i++) {
                Thread t = new Thread(() -> {
                    System.out.println("选手出发! " + Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("选手到达! " + Thread.currentThread().getName());
                    //撞线
                    countDownLatch.countDown();
                });
                t.start();
            }
            //await是进行阻塞等到,会等到所有选手都撞线之后,才解除阻塞
            countDownLatch.await();
            System.out.println("比赛结束");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    写时拷贝:copyonwriteArrayList一写多读不加锁保证线程安全
    如何避免读到修改一数据:
    在这里插入图片描述

    旧方法修改不一定是原子的,可能读到修改了一半的数据,解决方法拷贝一份,在新的修改,引用指向新的顺序表,释放旧的内存

    重点难点:

    一、谈谈HashMap HashTable ConcurrentHashMap的区别

    1.线程安全角度:

    Hashmap不安全
    Hashtable, CoucurrentHashtable安全

    2.多线程下的优化:(CoucurrentHashMap好处)

    1.锁粒度的控制:

    HashTable 直接在方法上加synchroinzed,相当于是对this加锁,相当于是针对哈希表对象来加锁,一个哈希表,只有一个锁多个线程,无论这些线程都是如何操作的这个哈希表,都会产生锁冲突

    CoucurrentHashMap每个哈希桶都有自己的锁,大大降低了锁冲突的概率,性能也就大大提高了
    在这里插入图片描述

    2.CoucurrentHashMap做了一个激进的操作:

    只是个写操作加锁,读操作不加锁了
    如果一个线程读,一个线程修改,也没有锁冲突
    是否担心读的是修改一半的数据呢
    不必担心,CoucurrentHashMap设计的时候保证读的是整个数据(要么是旧版本,要么是新版本,不会说读了修改一半的数据),另外广泛使用volatile保证内存可见性,读到数据是及时的

    3.充分的利用到了cas特性

    比如像维护个数,都是通过cas来实现,而不是加锁
    包括还有些地方使用cas实现的轻量级锁来实现

    总之,concurrentHashMap思路是能不加锁,就不加锁,尽一切可能来减低锁冲突的概率

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

    化整为零(类似于拷贝)
    HashTable的扩容方式是当put时候发现负载因子已经超过阈值,就触发扩容,申请一个更大数组,,然后把之前旧的数据给搬到新的数组上

    很大的问题:
    如果元素个数特别多,开销就很大,触发put这一操作可能会卡很久

    CoucurrentHashMap在扩容的时候,就不再是直接一次性完成搬运了,而是搬运一点
    扩容的时候,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运到一部分到新的空间上,直到搬运完成
    如果要查询元素旧的新的一起查
    插入元素,直接往新的上插入
    如果是删除元素,直接删了不用搬运了

    分段锁:是旧版本的CouCurrentHashMap,是好几个链表公用同一个锁(java8开始就没了,锁冲突概率高,代码实现复杂)

    5.key值

    HashMap key允许为null
    HashTable 和 CouCurrentHashMap key值不能为null

    二、如何解决死锁

    常见死锁场景

    1.一个线程,一把锁,连续加锁俩次,如果不可重入,死锁

    例子:
    count++;

    2.俩个线程,俩把锁

    例子:1.钥匙锁车里,车钥匙锁家里
    2.吃饺子:拿醋给酱油,拿酱油给醋
    3.程序员修复一码通
    死锁代码:

    //演示死锁
    public class demo34 {
        public static void main(String[] args) {
            Object locker1=new Object();
            Object locker2=new Object();
            Thread t1=new Thread(()->{
                System.out.println("t1尝试获取locker1");
                synchronized (locker1){
                    try {
                        Thread.sleep(500);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("t1尝试获取 locker2");
                    synchronized (locker2){
                        System.out.println("t1 获取俩把锁成功");
                    }
                }
            });
            Thread t2=new Thread(()->{
                System.out.println("t2 尝试获取locker1");
                synchronized (locker2){
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("t2 尝试获取 locker2");
                    synchronized (locker1){
                        System.out.println("t2获取俩把锁成功");
                    }
                }
            });
            t1.start();
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    产生死锁条件有双重循环,加锁顺序

    3.多个线程多把锁:

    哲学家进餐问题:

    在这里插入图片描述
    五个哲学家,五根筷子,
    假设哲学家要么思考人生,啥事不做(阻塞)
    要么吃面条,先拿左手筷子,再拿起右手筷子,吃一会就放下(线程执行)
    由于线程调度是随机的,哲学家们,啥时候吃面,啥时候思考人生不确定
    在这里插入图片描述

    极端情况下:同时拿起左手筷子,就死锁了

    解决方案:打破循环等待

    约定:俩把锁必须先获取编号小端,后获取编号大的,不是先左后右,有效避免循环等待

    银行家算法,统称分配

  • 相关阅读:
    【Linux系统化学习】进程的状态 | 僵尸进程 | 孤儿进程
    高可靠性部署系列(2)--- IPS双机热备
    基于Echarts实现可视化数据大屏机械设备监测大数据统计平台HTML页面
    SpringBoot项目整合MybatisPlus持久层框架+Druid数据库连接池
    基于A*、RBFS 和爬山算法求解 TSP问题(Matlab代码实现)
    国内首发可视化智能调优平台,小龙带你玩转KeenTune UI
    传来喜讯,优维又获奖了!!!
    了解5个区别,FPmarkets用烛台和Renko图实现交易翻倍
    Java与Redis的集成
    网络安全运维工程师(NISP-SO)需要掌握那些知识点
  • 原文地址:https://blog.csdn.net/panpanaa/article/details/127677495