目录

阶段性小结

1.常见的锁策略

1.1乐观锁vs悲观锁

悲观锁:

乐观锁:

1.2重量级锁vs轻量级锁

重量级锁:

轻量级锁:

课件内容(了解即可)

1.3挂起等待锁vs自旋锁(Spin Lock)

挂起等待锁:

自旋锁:

synchronized 是 悲观锁 / 乐观锁 呢??

1.4普通互斥锁vs读写锁

普通互斥锁:

读写锁:

1.5可重入锁vs不可重入锁

可重入锁:

不可重入锁:

1.6公平锁vs非公平锁

公平锁:

非公平锁:

1.7相关面试题

课件内容

2.synchronized原理

2.1基本特点

2.2加锁工作过程

1)偏向锁

2)轻量级锁(课件内容)

3)重量级锁(课件内容)

2.3其他的优化操作

锁消除

答疑:

锁粗化

2.4相关面试题(课件内容)


书接上文:Java EE:2.多线程-初阶(第九弹)~~

阶段性小结

多线程初阶

1.线程的原理、进程和线程的关系

2.多线程的使用、Thread类的用法

3.线程安全机制

4.等待通知机制

5.多线程的代码案例

定时器

1)定义类描述任务

2)使用优先级队列

3)schedule方法

4)扫描线程,复杂执行队列中的任务

以上问题还涉及到“忙等”→用 wait / notify 优化

多动手敲一敲~~学编程,不是学知识,而是学手艺~~

多线程

初阶:贴合实际工作情况

进阶:贴合面试

当前面试的环境,非常卷

经常会出现一些,工作中用不到,但面试还要考的东西=>八股文(背)

1.常见的锁策略

如果你自己需要实现一把锁(你认为 标准库 给你提供的锁不够用),你需要关注锁策略

其实 synchronized 已经非常好用了,足矣覆盖绝大多数的使用场景

此处的“锁策略”不是和 Java 强相关的,其他语言,但凡涉及到并发编程、涉及到锁,都可以谈到这样的锁策略

这个锁在加锁的时候,有啥特点、有啥行为~

1.1乐观锁vs悲观锁

不是针对某一种具体的锁~~

而是某个具体锁具有“悲观”特性 或者 “乐观”特性~~

悲观锁:

悲观:加锁的时候,预测接下来的锁竞争的情况非常激烈,就需要针对这样的激烈情况额外做一些工作

比如说有一把锁,有20个线程尝试获取锁,每个线程加锁的概率都很高,一个线程加锁的时候,很可能锁被另一个线程占用着,此时就要用到悲观锁的策略

乐观锁:

乐观:加锁的时候,预测接下来的锁竞争的情况不激烈,就不需要做额外工作

比如说有一把锁,只有两个线程尝试获取这个锁,每个线程加锁的概率都很低,一个线程加锁的时候,大概率另一个线程没有和它竞争,此时就要用到乐观锁的策略

闲聊:比如说 offer 的竞争~~

大厂的 offer 好不好拿??

秋招的大厂 offer ,非常难

实习的大厂 offer ,相对简单很多,出手越早越简单

答疑:

Q1:我大三上学期就去找实习??我感觉我还没复习好,我再看看,等几个月再投??

这个跟你复不复习没关系,主要看竞争的激烈程度(供需关系)

秋招,一个大厂 offer ,通常几百人,上千人在竞争

比如网易,一场秋招的笔试,往往就是几万人一起参加,最终网易也就招聘几百人~~

实习,大三下学期三四月投实习,意味着大部分同学都没有反应过来

只有少数,先飞的鸟和你竞争

一个岗位可能就只有几个人和你竞争

如果你是大三上学期,9月、10月、11月投实习,意味着正常大学生都没有这个意识

企业那边的情况:放出的招聘信息,压根没人投

如果人家需要紧急招聘5个实习生,结果很可能,0份简历

如果你投了,哪怕你就会 hello word ,人家也能要~~

Q2:大三学校会放行吗?

大家要能够认清,正常的学校是不会放人的~~

学校领导无法区分你是去正经公司实习,还是被骗到传销窝点~~

如果大家以后真的有好的实习 offer ,只有“溜”这一个途径~~

Q3:那叫家里人和学校请假呢?

请长假会判定留级

留级是你整个职业生涯的污点

因为在中国,大学生4年按时毕业,是再正常不过的了

如果你5年毕业,HR 眼中会认为你大概率是存在严重问题的

1.2重量级锁vs轻量级锁

重量级锁+轻量级锁 和 悲观锁+乐观锁 在很大程度上是重合的

悲观锁和乐观锁描述的是加锁时候遇到的场景

那么重量级锁和轻量级锁就是遇到场景之后的解决方案

重量级锁:

在悲观的场景下,就要付出更多的代价=>更低效

轻量级锁:

在乐观的场景下,付出的代价就会更小=>更高效

课件内容(了解即可)

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

CPU提供了“原子操作指令”

操作系统基于CPU的原子指令,实现了 mytex 互斥锁

JVM 基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类

注意,synchronized 并不仅仅是对 mutex 进行封装,在 synchronized 内部还做了很多其他的工作

重量级锁

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

大量的内核态用户切换

很容易引发线程的调度

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

轻量级锁

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

少量的内核态用户态切换

不太容易引发线程调度

理解用户态vs内核态

参考:Java EE:2.多线程-初阶(第八弹)

用户态=配套的应用程序

内核态=内核

1.3挂起等待锁vs自旋锁(Spin Lock)

这就相当于在重量级锁+轻量级锁的基础上做了进一步的细化

挂起等待锁:

挂起等待锁=>重量级锁的典型实现=>操作系统内核级别的,加锁的时候发现竞争,就会使该线程进入阻塞状态,后续就需要内核进行唤醒了

自旋锁:

自旋锁=>轻量级锁的典型实现=>应用程序级别的,加锁的时候发现竞争,一般也不是进入阻塞,而是通过“忙等”的形式来进行等待

答疑:

Q1:之前不是讲“忙等”不好吗??

这个是应对乐观锁的场景,本身遇到锁竞争的概率就很小

而且真的遇到锁竞争,在短时间内就能拿到锁,所以问题不大~~

Q2:自旋锁和定时器那个是不是差不多?

其实定时器和自旋锁不太相关,只不过这两者都涉及到忙等而已~~

因为定时器这个东西并不涉及到去竞争某个资源~~所以这俩东西联系到一起还是有点牵强~~

自旋锁伪代码👇

while (抢锁(lock) == 失败) {}

拿追女神举🌰:

①自旋的方式:女神,只是你眼中的女神=>乐观

今天来问问女神,你做我女票好不好

女神表示,她有男朋友,你是个好人~~

你表示,没关系,我愿意等~~(甘当备胎)

等的过程中,三天两头的,“联络”一下女神~~

很快,有一天,你发现女神和男票吵架了

机会来了!!

在你的关心和煽风点火之下,女神成功和男票分手

你就趁机上位!

其实也消耗不了多久,很快就能有机会,只要发现机会,就能立即上位

整个锁的等待时间,其实并不长~~

这种就适合锁竞争不激烈的场景,获取锁的周期更短,能够及时获取锁,这个过程会一直消耗CPU~~

②挂起等待锁:女神,是大家眼中的女神=>悲观

你去问女神,做我女票好不好

女神表示,她有男朋友,你是个好人~~

你表示,没关系,我愿意等~~

等的过程中,不再联系女神~~

于是你安心认真学习敲代码~~

几个月过去了、甚至几年过去~~

偶然间,听说了,女神单身了的消息(当你听到这个消息的时候,女神可能都已经分手过几次了~~)

你再去找到女神,你做我女票好不好~~

好就成了,不好,那就接着以上述的方式等~~

这个过程中,你这边不再继续在女神身上消耗精力了~~

这种就适合锁竞争激烈的场景,获取锁的周期更长,很难做到及时获取,但是这个过程就不必一直消耗CPU,把CPU省出来做别的事情~~


答疑:自旋没太明白

挂起等待锁:

在操作系统中,让线程阻塞,后续也是由系统唤醒

比如,线程1在14:00的时候因为加锁失败,进入阻塞

14:05的时候,对应的锁被释放了

14:10的时候,操作系统才唤醒这个线程1,线程1才拿到锁~~

在14:00-14:10这个过程中,线程1全程阻塞,不消耗CPU

自旋锁:

不涉及内核操作

线程2在14:00的时候,因为加锁失败,等待(不会放弃CPU)

14:01 问问,锁是否可以加,还是不能加

14:02 问问,锁是否可以加,还是不能加

14:03 问问,锁是否可以加,还是不能加

……

14:05 问问,其他线程释放了锁,当前的线程2就可以拿到锁了

不涉及到系统内核的线程调度,第一时间能拿到锁

以上过程,都是在锁的内部实现的,咱们使用的时候无非就是一个 lock / synchronized 操作,咱们也无法在应用程序这里获取到 synchronized 的底层状态~~


synchronized 是 悲观锁 / 乐观锁 呢??

既是又是~~自适应~~

设计 JVM 的大佬为咱们操碎了心~~

大佬的初心,就是别让程序员操心这些事情,都在 JVM 内部做好了~~

JVM 内部,会统计每个锁 竞争的激烈程度

如果竞争不激烈,此时 synchronized 就会按照轻量级锁(自旋)方式处理

如果竞争激烈,此时 synchronized 就回去按照重量级锁(挂起等待)方式处理

闲聊:学C++的同学就要操心了~~

std::mutex 就是重量级锁~~

加锁,就会挂起等待

不太适合乐观的场景

乐观的情况还得程序员自己实现(标准库没有提供)

虽然大佬们是希望咱们不关注这些细节,但遗憾的是,面试官没有放过咱们~~

1.4普通互斥锁vs读写锁

普通互斥锁:

synchronized 就是一个普通互斥锁,只有加锁和解锁两种方式

读写锁:

有三种方式:读方式加锁、写方式加锁、解锁


我们知道:

多个线程读取一个数据,本身就是线程安全的~~

多个线程读取,一个线程修改,肯定会涉及到线程安全问题

而读写锁就适合处理这种“读多写少”的情况:

大部分操作在读,少数操作在写

如果你把读和写都加上普通的互斥锁,意味着锁冲突非常严重

读写锁,就能够确保:读锁和读锁之间,不是互斥的(不会产生阻塞,线程安全)

写锁和读锁之间  以及  写锁和写锁  之间,才会互斥

因此,读写锁能够在保证线程安全的前提下,降低锁冲突的概率,提高效率


读多写少这种情况,在服务器开发中,是非常常见的场景~~

比如说,教务系统~~布置作业~~

老师会通过教务系统,往数据库中写一个记录(给作业表,添加一个记录,班级和一个具体的作业内容)=>写操作,一个人写

同学们也会通过教务系统,查看作业=>读操作,几十号人读

再或者说当时学MySQL时的索引~~查询操作~~也是类似道理


Java标准库提供了 ReentrantReadWriteLock 类,实现了读写锁

ReentrantReadWriteLock是外部类,ReadLock 和 WriteLock 是内部类

ReentrantReadWriteLock.ReadLock 类表示一个读锁,这个对象提供了 lock / unlock 方法进行加锁解锁

ReentrantReadWriteLock.WriteLock 类表示一个写锁,这个对象也提供了 lock / unlock 方法进行加锁解锁

1.5可重入锁vs不可重入锁

synchronized 是“可重入锁”

可重入锁:

一个线程,一把锁,连续加锁多次,不会死锁

实现的核心要点:

1.锁要记录当前是哪个线程拿到的这把锁

2.使用计数器,记录当前加锁了多少次,在合适的适合进行解锁

不可重入锁:

一个线程,一把锁,连续加锁多次,会死锁

此处关于可重入锁和不可重入锁不再过多赘述,大家可参考之前讲过的内容进行理解👇

Java EE:2.多线程-初阶(第四弹):synchronized 锁+内存可见性

1.6公平锁vs非公平锁

当女神和男朋友分手之后~~

谁应该上位呢??怎样做才是公平呢??

1.按照先来后到,谁最先追女神,谁上位

2.概率均等

这两种都可以定义成公平~~

但最终的公平,是由Java大佬决定的~~把第1种,先来后到,定义成了公平~~

公平锁:

非公平锁:

注意:

1.锁默认情况下是非公平锁,就相当于“概率公平”,因为操作系统针对线程的调度是随机的

2.如果要想实现公平锁,就需要依赖额外的数据结构,比如说队列,记录一下各个线程获取锁的先后顺序

1.7相关面试题

1.悲观vs乐观

2.重量vs轻量

3.挂起等待vs自旋

4.互斥vs读写

5.可重入vs不可重入

6.公平vs非公平

对于以上6种类型的锁,在面试题中,也就两种方式考察:

1.一般就是问概念

2.面试官问到某个问题的时候,用到上述术语

其他的锁,同样也能套入到上述的这些词中

课件内容

Q1:你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

悲观锁认为多个线程访问同一个共享变量冲突的概率较大,会在每次访问共享变量之前都去真正加锁

乐观锁认为多个线程访问同一个共享变量冲突的概率不大,并不会真的加锁,而是直接尝试访问数据,在访问的同时识别当前的数据是否出现访问冲突

悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex),获取到锁再操作数据,获取不到锁就等待

乐观锁的实现可以引入一个版本号,借助版本号识别出当前的数据访问是否冲突

Q2:介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁

读锁和读锁之间不互斥

写锁和写锁之间互斥

写锁和读锁之间互斥

读写锁最主要用在“频繁读,不频繁写”的场景中

Q3:什么是自旋锁,为什么要使用自旋锁策略呢?缺点是什么?

如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止

第一次获取锁失败,第二次的尝试会在极短的时间内到来,一旦锁被其他线程释放,就能第一时间获取到锁

相比于挂起等待锁:

优点:没有放弃CPU资源,一旦锁被释放就能第一时间获取到锁,更高效,在锁持有时间比较短的场景下非常有用

缺点:如果锁的持有时间较长,就会浪费CPU资源

Q4:synchronized 是可重入锁吗?

是可重入锁

可重入锁指的是连续两次加锁不会导致死锁

实现方式:

1.记录该锁持有的线程身份

2.计数器记录加锁次数

2.synchronized原理

2.1基本特点

synchronized 是自适应的,不是读写锁,是可重入锁、非公平锁

2.2加锁工作过程

接下来讲一下 synchronized 自适应的过程(也叫“锁升级”)

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

1)偏向锁

还是拿出之前讲线程池时的例子:小美(茶化之前),要谈男朋友,又希望把男朋友更换的快一点~~

1.和当前男朋友分手

当时说的是,小美需要zuo一zuo,打一打拳,逐渐消耗他的耐心~~,再谈分手,而且一定要哭的梨花带雨,让他认为都是他的问题~~

2.和下一个小哥哥培养感情(通过 池 ,优化这一步的性能)
那第一步能不能优化呢??当然可以!!😏😏

当小美最开始谈这个男朋友的时候,就和他,做各种情侣之间做的事情,但是从来不和他确认关系=>就是搞暧昧

这就使得他能够很好的满足小美对于 男朋友 的需求~~

有一天,小美 对他厌烦了~~,这个时候就不必麻烦了~~直接和他说,咱们不要再见面了~~

他问,就说“咱们只是普通朋友呀~~”,直接让他闭嘴

当然,这种搞暧昧的手段,也有副作用~~

万一,有别的妹子,也接近这个小哥哥~~这个小哥哥也可以瞬间把小美踹掉,然后用同样的话“咱们只是普通朋友”来应对小美~~

于是小美就想了个办法,她只要发现,有妹子试图接近小哥哥,只要一有这样的苗头,小美就立即和小哥哥确认关系,并且朋友圈官宣~~让其他妹子可以滚远了~~

上述过程,就是偏向锁的过程~~,本质上也是懒汉模式思想的体现,不到万不得已不去加锁,尽量省下加锁的开销

1.进行 synchronized 时,刚一上来,不是真加锁,而是只是简单做一个标记(搞暧昧),这个标记,非常轻量,相比于加锁解锁来说,效率高很多~~

2.如果没有其他线程来竞争这个锁,最终当前线程执行到解锁代码,也就只是简单清除上述标记即可~~(不涉及真加锁,真解锁)(搞暧昧,不真确立关系,后续分手很快)

3.如果有其他线程来竞争,就抢先一步,在另一个线程拿到锁之前,抢先拿到锁,真加锁,此时 偏向锁 就升级到=>轻量级锁,其他线程只能阻塞等待

①无锁=>偏向锁:代码进入 synchronized 的代码块

②偏向锁=>轻量级锁:拿到偏向锁的线程运行过程中,遇到了其他线程尝试竞争这个锁

③轻量级锁=>重量级锁:JVM发现,当前竞争锁的情况非常激烈

当前 JVM 中,只提供了“锁升级”不能“锁降级”,个人猜测,可能是,实现了锁降级,收益不大,使 JVM 相关代码的复杂度更高~~

2)轻量级锁(课件内容)

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

此处的轻量级锁就是通过 CAS 来实现

①通过 CAS 检查并更新一块内存(比如 null=>该线程引用)

②如果更新成功,则认为加锁成功

③如果更新失败,则认为锁被占用,继续自旋式的等待(并不放弃CPU)

自旋操作是一直让 CPU 空转,比较浪费 CPU 资源

因此此处的自旋不会一直持续进行,而是达到一定的时间 / 重试次数,就不再自旋了,也就是所谓的“自适应”

3)重量级锁(课件内容)

如果竞争进一步激化,自旋不能快速获取到锁状态,就会膨胀为重量级锁

此处的重量级锁就是指用到内核提供的 mutex

①执行加锁操作,先进入内核态

②在内核态判定当前锁是否已经被占用

③如果该锁没有被占用,则加锁成功,并切换回用户态

④如果该锁被占用,则加锁失败,此时线程进入锁的等待队列,挂起,等待被操作系统唤醒

⑤经历了一系列的沧海桑田,这个锁被其他线程释放了,操作系统也会想起这个挂起的线程,于是唤醒这个线程,尝试重新获取锁

2.3其他的优化操作

锁消除

也是编译器优化的一种体现

编译器会判定,当前代码逻辑是否真的需要加锁,不需要,但你写了 synchronized ,就会把 synchronized 给消除掉

答疑:

Q1:会出现逻辑错误导致线程安全吗?

这种消除是比较保守的,100%确认你这个代码确实是单线程操作的时候,才会真正触发消除

像一些判定不清楚的情况,不会触发的

Q2:那直接到处 synchronized?

到处 synchronized ,意味着优化机制,只能把其中一部分,它能明确判定的给优化掉,还会有很多不应该使用,但是编译器也优化不掉

所以目前还是不能完全依赖编译器优化

Q3:优化后效率会提高?

即使只是一个偏向锁,肯定也不如完全不加锁~~

锁粗化

锁的粒度:粗和细~~

加锁和解锁之间:

包含的代码越多=>粒度越粗

包含的代码越少=>粒度越细

这个粗和细,不是代码行数,而是实际执行的指令/时间

一个代码中,反复针对粒度的代码加锁,就可能被优化成更粗粒度的加锁(避免每次解锁之后再加锁而增加锁竞争)

比如,有一天,你的领导给你安排3个工作,当你把这几个工作都做完之后,你要给领导汇报~~

那你肯定不能做完一个汇报一个(第一次第二次也就罢了,第三次领导不呲儿你算他素质高~~),咱们肯定是都做完了之后一次性给领导全面汇报~~

闲聊:

在以后的工作中,一定要及时汇报!!!

产出的结果,一定要及时让领导看见~~(刷存在感),未来的升职加薪,都是领导主观感受~~

还有就是汇报工作:“能见面则见面,能电话不邮件~~”,这是重要的原则~~

因为你给领导打电话,相当于对领导加锁,此时领导需要全心全意应付你,没法做别的事情了~~对于整体的效率必然造成影响~~

很多同学,实习的时候,不了解这些,经常自己一个人吭哧吭哧研究一些东西,结果搞岔~~

积极保持沟通,尤其是有的领导比较忙,不一定能顾得上你,你一定要积极主动,当面的积极沟通~~

如果你的领导特别忙,就趁着一起吃饭的时候,或者下班一起出门的时候……赶紧跟上去说几句~~

其次,你问的问题,一定是深度思考过的、有价值的问题~~

别问那种,百度一搜就能搜到的问题~~

不能转正一般都不是因为你菜,而是因为其他的原因,最主要的原因就是沟通费劲~~

比如说像下面的代码,synchronized 就不应该加到 for 外面,因为 for 中的 i <50000 ; i++ ,希望能够并发执行的,只是 count++ 不能并发~~

而下述这种情况就可以优化👇

synchronized (cur){
    count++;
}
synchronized (cur){
    count++;
}
synchronized (cur){
    count++;
}

可以看到,synchronized 的策略是比较复杂的,在背后做了很多事情,目的就是为了让咱哪怕啥也不懂,也不至于写出特别慢的程序,Java大佬为了咱可真是操碎了心~~

2.4相关面试题(课件内容)

Q1:什么是偏向锁??

偏向锁不是真的加锁,而只是在锁的对象头中记录一个标记(记录该锁所属的线程),如果没有其他线程参与竞争锁,那么就不会真正执行加锁操作,从而降低程序开销

一旦真的涉及到其他的线程竞争,再取消偏向锁状态,进入轻量级锁状态

Q2:synchronized 实现原理是什么??

参考上述 synchronized 原理全部内容

更多推荐