Java EE多线程进阶


前言

java ee多线程进阶常用于面试(堪称八股文),实际工作生活用的比较少。如果仅想学习多线程,或多线程入门,可移步笔者java ee多线程详解。

提示:以下是本篇文章正文内容,下面案例可供参考

一、常见锁策略

锁策略和普通程序员没什么关系,和“实现锁的人”有关系
这里提到的锁策略,和java本身没关系,适用于所有和“锁”相关的情况。

1.1乐观锁vs悲观锁

乐观锁:预期锁冲突的概率很高
悲观锁:预期锁冲突的概率很高

举例说明:
现在疫情嘛,
乐观态度:下一波疫情即使来了,但是菜应该还是可以买的到的,现在不提前屯菜

悲观态度:下一波疫情来了,可能会买不到菜,我在疫情前,提前屯菜。

简言之:
悲观锁,做的工作更多,更低效
乐观锁,做的工作更少,更高效

1.2读写锁vs普通互斥锁

对于普通的互斥锁,只有两个操作:加锁,解锁
只要两个线程针对同一个对象加锁,就会产生互斥

对于读写锁来说,分成三个操作:
加读锁:如果代码中只进行读操作,加读锁
加写锁:如果代码中进行了修改操作,加写锁
解锁

ps:读锁和读锁之间,是不存在互斥的;
读锁和写锁、写锁和写锁之间才需要互斥

1.3重量级锁vs轻量级锁

重量级锁,就是做了更多的事情,开销更大
轻量级锁,就是做的事情更少,开销更小

通常情况下:
悲观锁都是重量级锁
乐观锁都是轻量级锁
(不绝对)

在使用的锁中,如果锁是基于内核的一些功能来实现的(比如调用了操作系统提供的mutex接口),此时一般认为是重量级锁。(操作系统的锁会在内核中做很多的事情,比如让线程阻塞等待…)

如果锁是纯用户态实现的,此时一般认为是轻量级锁
(用户态的代码更可控,也更高效)

1.4挂起等待锁vs自旋锁

挂起等待锁,往往是通过内核的一些机制来实现的,往往较重(是重量级锁的一种典型实现)

自旋锁,往往是通过用户态代码来实现的,往往较轻(是轻量级锁的一种典型实现)

1.5公平锁vs非公平锁

公平锁:多个线程在等待一把锁的时候,遵循先来后到

非公平锁:多个线程等待一把锁的时候,不遵循先来后到(每个等待的线程获取到锁的概率相等)

举例说明:
你排队做核酸,谁先到谁先做——这是公平的
你排队做核酸,大家一拥而上,不管先来后到——这是不公平的

对操作系统来说:本身线程之间的调度就是随机的,操作系统提供的mutex这个锁,就属于非公平锁。

ps:考虑到相同优先级的情况,实际开发中很少会手动修改线程的优先级(改了也基本体会不到)

1.6可重入锁vs不可重入锁

一个线程针对一把锁,连续加锁两次,如果会死锁,就是不可重入锁,否则就是可重入锁。

二、CAS

2.1什么是CAS

CAS :compare and swap
它要做的就是,拿着寄存器/某个内存中的值,和另一个内存的值进行比较,如果值相同了,就把另一个寄存器/内存的值,和当前这个内存进行交换

eg:
在这里插入图片描述

现在内存里有个变量v,变量有个旧的预期值A,然后我现在要修改这个变量。我们先比较一下V里面的值和A是不是一样,如果一样就把B值放到变量V里面

也可以来看一段伪代码加深一下理解:
在这里插入图片描述
此处所谓的CAS,指的是,CPU提供了一个单独的CAS指令,通过这条指令,就完成了上述伪代码描述的过程

我们再来看刚才的伪代码,既有读操作,又有写操作,而且读和写还不是原子的——明显是线程不安全的。

但是我们如果是CAS,上述伪代码是一条指令,那就相当于是原子的了(cpu上执行的指令是一条一条执行的,指令是最小单位),此时线程就安全了。

2.2CAS是如何实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
(1) java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
(2)unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
(3)Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子
性。
简而言之,是因为硬件予以了支持,软件层面才能做到。

2.3CAS有哪些应用

1.基于CAS能够实现“原子类”
java标准库中提供了一组原子类,针对常用的int,long,int array…进行了封装,可以基于CAS的方式进行修改,并且线程安全

使用示例:

public static void main(String[] args) throws InterruptedException {
        AtomicInteger num=new AtomicInteger(0);
        Thread t1=new Thread(()->{
            for(int i=0;i<50000;i++){
                num.getAndIncrement();//相当于num++;
            }
        });
        t1.start();

        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                num.getAndIncrement();
            }
        });
        t2.start();
        t1.join();
        t2.join();

        //通过get方法得到原子类 内部的数值
        System.out.println(num.get());//打印100000,不存在线程安全问题
    }

这个代码就不存在线程安全问题,基于CAS实现++操作,这里面就可以保证线程安全,又比synchronized高效(synchronized会涉及到锁的竞争,两个线程要相互等待)

CAS不涉及线程阻塞等待。

//原子类的一些其他基础操作
        //++num
        num.incrementAndGet();
        //--num
        num.decrementAndGet();
        //num--
        num.getAndDecrement();
        //+=10
        num.getAndAdd(10);

伪代码实现及其解释:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

在这里插入图片描述
(图片来自比特就业课)
该代码的核心就是,如果value被其他线程改过了,我们可以通过对比value和oldvalue,如果发现不一致,我们就让oldvalue再读一遍value值。

我们再来看一个示意图,来解释一下为什么上面的++操作是线程安全的:

如图,有线程t1和t2,他们的执行顺序如下
在这里插入图片描述
假设我们现在内存里有一个value=0
在这里插入图片描述

t1进行load,把内存里的0加载到cpu上,
在这里插入图片描述
t2进行load,把内存上的0加载到cpu上,
在这里插入图片描述
t1执行CAS,将内存里的0和cpu的0比较,发现相等,然后把cpu上的0+1,变成1
在这里插入图片描述
cpu上值变成1之后,再与内存值进行交换
在这里插入图片描述
接下来t2执行CAS,将内存上值与cpu上值进行比较,发现1和0不相等,返回false,进入下次循环(再次load和cas)
在这里插入图片描述
t2第二次进行load,将内存里的1,加载到cpu上
在这里插入图片描述
t2进行CAS,比较内存上值和cpu上值,发现1和1相等,然后cpu上1++,变成2
在这里插入图片描述
cpu上值变成2之后,再与内存值交换
在这里插入图片描述

2.基于CAS能够实现“自旋锁”

自旋锁伪代码实现及其解释:

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

在这里插入图片描述

ps:自旋锁是一个轻量级锁,也可以视为一个乐观锁。
当前这把锁虽然没能立即拿到,预期很快就能拿到(假设锁冲突不激烈)
短暂的自旋几次,浪费点cpu,问题不大,好处就是只要锁一解放,就可以立即拿到锁。

2.3CAS的ABA问题

我们面试中,面试官关于CAS的主要问题就是:“如何理解CAS中的ABA问题”

CAS中的关键就是:先比较、再交换
比较其实是在比较当前值和旧值是不是相同,把这两个值相同,就相当于中间没有发生过改变

但这样的结论存在漏洞:当前值和旧值可能是中间确实没改变过,也有可能变了,但是最终又变了回来。

这样的漏洞,在大多数情况下没有什么影响,但是极端情况下也会引起bug

而这种问题就被称为ABA问题,简言之就是旧值是A,当前值也是A。但是你不知道这个A是一直是A;还是从A变成了B,然后又变为了A

我们举一个典型例子解释一下为什么会出现bug:
假设我们现在有个人要去取钱,他的账号余额为100,他现在要取50块钱:
在这里插入图片描述
现在他按取款键的时候,机器卡了一下,他下意识按了两次取款键。但是机器卡了一下,还是反应过来他按了两次取款键。

这就相当于,一次取钱操作,执行了两遍(两个线程,并发的去执行取钱这个操作),但是我们希望的是只成功取钱一次。

如果基于CAS的方式来实现这里的取款
我们写一个简单的伪代码:
在这里插入图片描述
我们用图示模拟一下伪代码:

现在我们有两个线程t1和t2,分别代表第一次取钱和第二次取钱,然后内存里100表示账户余额
在这里插入图片描述
t1执行load,把100从内存读到cpu上
在这里插入图片描述
t2执行load,把100从内存读到cpu上
在这里插入图片描述
t1执行cas,发现内存里100和cpu上100比较发现一样,于是把cpu上的值100减50,变成50
在这里插入图片描述
再把cpu上值和内存上值进行交换
在这里插入图片描述
t2再进行cas,发现cpu上是100,内存上是50,值不同,于是返回false(由于此处代码没有使用循环,我们判定一次失败就直接结束了)

按照上述分析,此处就是两次操作,实际只有一次成功。

但是,上面这种例子的前提是没有引入ABA问题,我们再来看一下ABA问题介入下的情况:

假设这个人取款的一瞬间,有人给他又转了50块钱
在这里插入图片描述
我们回溯到t1 CAS刚结束
在这里插入图片描述
这时有人给他转账50元
那内存里的50要变成100了
在这里插入图片描述
然后t2再cas,发现内存里值100和cpu上值100一样,要把CPU上的100减50

在这里插入图片描述
再与内存上的100交换,于是内存里变成了50
在这里插入图片描述
这不出大问题了嘛?我本来账号里有100块钱,我只想取50块钱,然后账号里剩余50,别人再给我转50,我账号里应该还有100块,但是ABA问题一出现,我账户里凭空消失50,这要搁现实里,如果钱数量大,银行不被人劈成两半?

三、Synchronized 原理

3.1基本特点

synchronized是一个自适应锁,即是乐观锁,也是悲观锁

synchronized不是读写锁,是普通的互斥锁

synchronized既是轻量级锁,也是重量级锁

synchronized的轻量级锁部分基于自旋的方式实现,重量级锁的部分基于挂起的等待实现

synchronized是非公平锁

synchronized是可重入锁

3.2加锁工作过程

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁状态。会根据情况,进行依次升级
**这个过程也称作“锁膨胀/锁升级”**体现了synchronized 能够“自适应”这样的能力

我们先来看一下synchronized的变换过程:

在这里插入图片描述
最开始没有用synchronized时候,就是无锁状态
偏向锁

当首个线程加锁,就会进入偏向锁状态
ps:偏向锁不是真的加锁,只是做了一个标记。
举例说明:
我现在是一个高段位妹妹,然后我看上了一个有钱的小帅哥,作为一个高段位妹妹,我想拿下他轻而易举。
但是考虑到我本身是个海王,可能玩几天我就不喜欢这个小帅哥了,但是这小哥哥对我纠缠不休,就比较麻烦了。
于是我只是和小哥哥搞暧昧,但不确定关系
这样的话,我下次想换哥哥就直接把他甩了就行

这就是偏向锁,并不是真的加锁,只是做了一个标记。
好处就是,我们后续没有竞争就避免了加锁解锁的开销(没看上别的小哥哥就和这个小帅哥一直暧昧)
但是如果有特殊情况,比如有别的女的也看上这个小哥哥了,我的占有欲就促使我立即和这个哥哥确认关系,以此来对别的女的进行反击。

总结就是:
如果没有别的女的和我竞争,就一直不去确认关系(节省了确立关系/分手的开销)

换到我们锁这边

如果没有其他的线程来竞争这个锁,就不必真的加锁(节省了加锁解锁的开销)

轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).

重量级锁
如果锁竞争进一步加剧,就会进入重量级锁状态

3.3其他优化操作

3.3.1锁粗化

锁粗化的反义词也叫锁细化

这里的粗细指的是“锁的粒度”
粒度也就是加锁代码涉及到的范围,
加锁代码的范围越大,认为锁的粒度越粗
加锁代码的范围越小,认为锁的粒度越细

示例如下:
在这里插入图片描述

会有同学问:“到底锁粒度粗好还是细好?”

如果锁粒度较细,多个线程之间的并发性就更高
如果锁粒度较粗,加锁解锁的开销就更小

Ps:编译器会有一个优化,会自动判断:
如果某个地方的代码锁的粒度太细,就会进行粗化

3.3.2锁消除

有些代码,明明不用加锁,结果你给加上锁了,编译器就会发现这个加锁没什么用,就会直接把锁给去掉了

eg:比如StringBuffer、vector…这种是在标准库中进行了加锁操作,在单线程中如果你用了上述的类,就会单线程进行加锁解锁,但这样的操作没有意义,编译器就会自己进行锁消除。

四、Callable接口

Callable是一个接口(interface),也是一个创建线程的方式。

而创建线程,我们可能大多数想到的是Runnable,但是Runnable不太适合让线程计算出一个结果。
比如我们现在计算1+2+3+…1000
不适用Callable

static class Result {
    public int sum = 0;
    public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
    Result result = new Result();
    Thread t = new Thread() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
           }
            synchronized (result.lock) {
                result.sum = sum;
                result.lock.notify();
           }
       }
   };
    t.start();
    synchronized (result.lock) {
        while (result.sum == 0) {
            result.lock.wait();
       }
        System.out.println(result.sum);
   }
}

可以看到, 上述代码需要一个辅助类 Result, 还需要使用一系列的加锁和 wait notify 操作, 代码复杂, 容易出错

Callable就是要解决Runnable不方便返回结果这个问题

public static void main(String[] args) {
        //通过Callable来描述这样一个选择的任务
        Callable<Integer> callable=new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum=0;
                for(int i=0;i<=1000;i++){
                    sum+=i;
                }
                return sum;
            }
        };
        //为了让线程执行callable中的任务,光使用构造方法还不够,还需要一个辅助的类
        FutureTask<Integer> task=new FutureTask<>(callable);
        //创建线程,来描述这里的计算工作
        Thread t=new Thread(task);
        t.start();

        //如果线程的任务没有执行完,get就会阻塞
        //一直阻塞到任务完成,结果就算出来了
        try {
            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }

运行结果如下:
在这里插入图片描述

ps:关于为什么中途要有一个FutureTask
在这里插入图片描述

五、JUC(java.util.concurrent) 的常见类

java里面有一个非常重要的包叫JUC,也就是java.util.concurrent

java.util我们很熟悉了,平时用的什么集合类都是这个里面的
concurrent是什么意思呢?并发的意思

而并发出现了,我们就知道肯定和多线程有关了

5.1ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

基础用法
lock(): 加锁, 如果获取不到锁就死等.

trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.

unlock(): 解锁

public static void main(String[] args){
        ReentrantLock locker=new ReentrantLock();
        //加锁
        locker.lock();
        //解锁
        locker.unlock();
    }

上面的代码也很明显的看出:
ReentrantLock就是把加锁解锁分开,
synchronized就是把加锁解锁放一起了

但是我们用的久的话,其实还是发现synchronized还是更好一些,因为你加锁后的代码一旦报了异常,你到时候执行不到unlock,就一直解锁不了(出现死锁)

和synchronized的区别:(3和4重点记忆,其他的了解即可)
1.synchronized是一个关键字, ReentrantLock是一个标准库中的类
2. synchronized不需要手动释放,出了代码块锁自动释放。ReentrantLock必须手动释放锁,并且需要谨防忘记释放
3. synchronized如果竞争锁的时候失败就会阻塞等待。ReentrantLock除了阻塞等待外还会trylock,如果失败就会直接返回
4. synchronized是一个非公平锁,ReentrantLock提供了非公平和公平锁两个版本!在构造方法中,通过参数来指定是公平/非公平
5. 基于synchronized衍生出来的是等待机制,是wait notify,功能相对有限。基于ReentrantLock衍生出来的等待机制是Condition类,功能更丰富一下

ps:日常开发中,绝大数情况下,synchronized就够你用了

5.2原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference

以 AtomicInteger 举例,常见方法有

addAndGet(int delta);   i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;

5.3线程池

虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.
线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 “池子”
中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.

详情请见笔者java ee 多线程案例,里面有线程池详解,这里不做过多赘述

5.4信号量 Semaphore

Semaphore是一个更广义的锁,锁是信号量里面的第一种特殊情况,叫作“二元信号量”

我们举个例子:

我们开车去停车场,停车场入口一般会有告示:当前还有x车位。
每次有车开出去,x++
每次有车开进来,x- -

这个告示就是信号量,描述了可用车位的个数

放到我们计算机里来说
信号量就是描述了可用资源的个数
每次申请一个可用资源,计数器- -(又称p操作)
每次释放一个可用资源,计数器++(又称v操作)
当信号量的计数器为0,再次进行p操作,就会阻塞等待
(相等于停车场已经满了,没有车位了,你想进去停车只能等)

锁就可以视为二维信号量,可用资源就一个,计数器的取值只有0和1

信号量就是把锁推广到了一般情况,可用资源更多的时候,如何处理的

代码示例如下:

public static void main(String[] args) throws InterruptedException {
        //初始化的值表示可用资源有4个
        Semaphore semaphore=new Semaphore(4);

        //申请资源,p操作
        semaphore.acquire(2);//表示1次申请2个资源
        System.out.println("申请成功");

        semaphore.acquire(2);
        System.out.println("申请成功");

        semaphore.acquire();//如果不加参数就是1次申请1个资源
        System.out.println("申请成功");
        //因为前面已经把4个资源全申请完了
        //所以这里不会打印申请成功,这里会陷入阻塞

        //释放资源,V操作
        semaphore.release(2);//表示1次释放2个资源
    }

运行结果如下:
在这里插入图片描述

5.5CountDownLatch

你可以理解为“终点线”

比如一场跑步比赛:
我们如果要判定一个比赛结束,不是第一个人跑完,而是最后一个人跑完。

这样的案例在开发中也是存在的,比如多线程下载:
我们要下载一个比较大的文件,如果把文件分成几个部分,用多线程下载,速度就会明显提升。而下载完成的判定是所有的线程都完成自己的下载,才是整个下载完成。

CountDown就是给每个线程里面去调用,就表示到达终点了

await是给等待线程去调用,当所有任务都到达终点了,await就从阻塞中返回,就表示任务完成了

代码示例如下:

public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch=new CountDownLatch(10);
        for(int i=0;i<10;i++){
            Thread t=new Thread(() ->{
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName()+"我已到达终点");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }

        //等待所有线程到达
        latch.await();//当这些线程没有全执行完,await就阻塞,所有线程都执行完了,await才返回

        System.out.println("所有线程已全部执行完毕");
    }

运行结果如下:
在这里插入图片描述

六、线程安全的集合类

原来的集合类, 大部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的.

6.1多线程环境使用ArrayList

1.自己使用同步机制 (synchronized 或者 ReentrantLock)
前面做过很多相关的讨论了. 此处不再展开.

2.Collections.synchronizedList(new ArrayList);
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized

3.使用 CopyOnWriteArrayList
写时拷贝,在修改的时候,会创建一份副本
比如有一个ArrayList
如果我们是多线程去读这个ArrayList,此时没有线程安全问题,完全不需要加锁,也不需要其他方面的控制,如果有多线程去写,就是把这个ArrayList给复制了一份,先修改副本

举例说明:
我现在有一个Arraylist {1,2,3,4}
要把1变成100,那么我们就是先复制一个副本{100,2,3,4},然后再让副本转正
(转正:原先有个引用指向{1,2,3,4},现在让这个引用指向{100,2,3,4})

优点:在修改的同时对于读操作,没有任何影响(优先还是读旧值)
ps:适合读多写少、数据量少的情况,不然你写的多,到时候拷贝的也多

缺点:1.占用内存较多. 2. 新写的数据不能被第一时间读取到

6.2多线程环境使用队列(了解)

  1. ArrayBlockingQueue
    基于数组实现的阻塞队列
  2. LinkedBlockingQueue
    基于链表实现的阻塞队列
  3. PriorityBlockingQueue
    基于堆实现的带优先级的阻塞队列
  4. TransferQueue
    最多只包含一个元素的阻塞队列

6.3多线程环境使用哈希表(重点)

哈希表本身是线程不安全的
在多线程环境下使用哈希表可以使用:
Hashtable
ps:(不推荐使用)

ConcurrentHashMap
ps:(推荐使用)

HashTable是如何保证线程安全的呢?
——给关键方法加锁
在这里插入图片描述
针对this来加锁,当有多个线程来访问这个HashTable的时候,无论是什么样的操作,无论什么样的数据,都会出现锁竞争,这样的设计就会导致锁竞争的概率非常大,效率就会比较低

举例说明:
在这里插入图片描述

放到我们HashTable里也是一样的
在这里插入图片描述

而 ConcurrentHashMap
就是把数组里的每个元素安排一把锁,当操作元素的时候,是针对这个元素所在的链表的头节点来加锁的。如果你两个线程操作是针对两个不同链表上的元素,没有线程安全问题。
在这里插入图片描述

(就类似老板把请假的批假权力下发给部门领导,不同部门的请假是找不同的部门领导,只有同一部门不同人请假才有可能发生锁冲突,这样锁冲突概率大大降低)

ps:由于hash表中,链表的数目非常多,每个链表的长度是相对短的,因此就可以保证锁冲突的概率非常低

改进要点小结:
1.ConcurrentHashMap减少了锁冲突,让锁加到了每个链表的头结点上(锁桶)
2.ConcurrentHashMap只是针对写操作加锁了,读操作没加锁,而只是使用了volatile
3.ConcurrentHashMap中更广泛的使用了CAS,进一步提高了效率
4.ConcurrentHashMap针对扩容,进行了巧妙的化整为零
举例说明:
如果元素多了,链表就会长,就会影响hash表的效率
就需要扩容,增加数组长度(数组长了,链表就短了)
扩容就需要创建一个更大的数组,然后把之前旧的元素给搬运过去。而这样的搬运操作非常耗时。

对于HashTable来说,只要你这次put触发了扩容就一次搬完,就会导致这次put非常卡顿。

对于ConcurrentHashMap来说,每次操作只搬运一点点,通过多次操作完成整个搬运的过程。也就是说,ConcurrentHashMap在搬运过程中,会同时维护一个新的HashMap和一个旧的,查找的时候既需要查旧的,也需要查旧的。插入的时候只插入新的。直到搬运完毕,销毁旧的HashMap

七、死锁

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

举例说明:
现在疫情期间,健康码系统出问题了导致健康码查看不了,维护这个系统的程序员回公司修代码
进公司被保安要求出示健康码:
保安:出示健康码再上楼
程序员:我得先上楼修代码,才能出示健康码
保安:出示健康码再上楼
程序员:我得先上楼修代码,才能出示健康码
保安:出示健康码再上楼
程序员:我得先上楼修代码,才能出示健康码
保安:出示健康码再上楼
程序员:我得先上楼修代码,才能出示健康码

如果这两个人一直这样下去,就是死锁了

死锁详见笔者java ee多线程详解文章,synchronized部分有详解,这里不过多赘述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断路器保护灵敏度校验整改及剩余电流监测试点应用站用交流系统断

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

劲夫学编程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值