先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
正文
想必大家会发现,除了读写锁比较好理解之外,1,3,4 这三种锁策略,翻来覆去好像都是讲得同一件事。
其实也确实如此。只不过就是说法,越来越详细。
其中悲观和乐观锁的说法是最笼统的。
而重量级锁 和 轻量级锁 的 说法稍微详细了一点点。
最后的挂起等待所 和 自旋锁 的说法,就涉及到更具体的实现了。
所以上面的 1,3,4 策略,这三组锁策略,其实之间的描述是非常接近的。只不过就是把抽象的概念,一步步的变得详细,更狭义。
我们当前可以将它当做是同一个东西。
但是在面试的时候,这些锁策略都会随机的出现。
上述的这些锁策略之间,彼此并不是完全没有没有联系,而是有着千丝万缕的关系。
所以大家也不要去把它们分割开来,去看待。
其实它们就是说的就是同一件事。
只不过是一件事,站在不同的角度,不同的范围来去进行描述。
这两个锁的概念容易混淆。请注意理解
公平锁:多个编程等待一把锁的时候,谁是先来的,谁就能获取到这把锁。(遵守先来后到)
基础篇说到多个线程在竞争同一把锁,只有一个线程能够占用这把锁,其余的线程只能等待。那么,当我们占用锁的线程释放锁的时候,等待的线程,谁先来获取到这把锁。
非公平锁:多个线程在等待一把锁的时候,不遵守先来后到。
每个等待的线程获取到锁的概率都是均等的。
容易搞混的地方就在这,可能有些碰头认为“均等的”是公平的。
但是此处约定的是,遵守先来后到,才是公平的。
均等,反而是不公平的。
对于操作系统来说,本身线程之间的调度就是随机的(机会均等的),操作系统提供的 mutex 这个锁,就属于非公平锁。
但是有人可能会有疑问:线程之间不是存在优先级嘛?优先级难道不会影响调度吗?
是的,会影响。所以,我们这里考虑的是优先级相等的情况下。
其实开发中很少会手动修改线程的优先级。
改了之后,在宏观上的效果并不明显。
要想实现公平锁,反而要付出更多的代价。
需要整个队列,来把这些参与竞争的线程给排序一下(先来后到)。
一个线程,针对一把锁,咔咔连续加锁两次。
如果死锁,就是不可重入锁。
反之如果不死锁,就是可重入锁。
死锁,这个在多线程基础篇的 重点解析 synchronized 关键字 中的 可重入 讲过了这个。
大家要明白 锁策略不止这六种,但是这六种属于面试中高频出现的6种锁策略。
拓展一 :synchronized 与 锁策略的对应关系
前面讲过一把锁 synchronized。
如果我们把这把锁,往上面的策略里面套会是怎样的结果?
1、synchronized 既是一个乐观锁,也是一个悲观锁。
synchronized 会根据锁竞争的激烈程度,自适应。
2、synchronized 不是读写锁,只是一个普通互斥锁、
3、synchronized 既是一个重量级锁,也是一个轻量级锁。
synchronized 会根据锁竞争的激烈程度,自适应。
4、轻量级锁的部分是基于自旋锁来实现的,重量级锁的部分是基于挂起等待锁来实现的。
5、非公平锁
6、可重入锁
====================================================================
悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁。
乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.
读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在 “频繁读, 不频繁写” 的场景中
如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
相比于挂起等待锁,
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源
是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.【一个线程,针对一把锁的情况】
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.【解锁,就只直接计数自减,为0就解锁成功了】
==================================================================
CAS: compare and swap(比较 与 交换)
注意颜色,细品
再来看个例子
此处所谓的 CAS ,指的是 CPU 提供了一个单独的 CAS 指令,通过这一条CPU指令,就可以完成上述伪代码要做的所有事情。
我们此处讨论的CAS,其实讨论的就是这一条CPU的指令。
另外,这个代码很明显是线程不安全的。
下面再来看一个例子,来加深我们对 CAS 的理解
这里我们主要讲两个场景
1、基于CAS 能够实现“原子类”
Java标准库里提供了一组 原子类。
针对锁常用的一些,int、long、数组… 进行了封装,可以基于CAS 的方式进行修改,并且线程安全。
小拓展:原子类的一些其他基础方法,让一个原子类像一个普通的整数一样进行运算。而且,运算过程还是线程安全的。
虽然原子类放在进阶内容,但是不可否认这个原子类其实是一个工作中高频使用的东西。
原子类背后具体是怎么实现的?
2、基于 CAS 能够实现“自旋锁”
如何理解 CAS中的 ABA 问题?- 面试主要问的问题
ABA问题:就是CAS中的关键【先比较,在交换】
而这里的比较,其实是在比较 当前值 和 旧值 是不是相同的。
把这两个值相同的情况,就视为中间没有发生过改变。
但是这里的结论存在这漏洞。
当前值 和 旧值 相同,可能是中间确实没改变过。
也有可能是改变了,但是变回来了。
【最终的值虽然和旧值相同,但是它确实改变了】
而当前就很草率的决定,只要值相同就没有发生改变。
这样的漏洞,在大多数情况下,其实没有影响。
但是,在极端情况也会引起bug。
这种问题,就被称为 ABA 问题。
所谓的ABA 指的是:本来旧的值A,当前值也是A。
结果你不知道当前的A,它是一直都是A,还是从A变成了B,再从B又变回了A。
所以这就叫ABA问题。
如何处理ABA问题
引入一个“版本号”,这个版本号,只能变大,不能变小。
在修改变量的时候,比较就不是比较变量本身了,而是版本号了。
版本号不一定,非的是加,也可以是减,只要一直是往着一个方向进行就可以了。
另外,这里不一定非得“版本号”,也可以使用“时间戳”,日期时间肯定是一直往前走的。
所以使用 时间戳也是没有问题的。
拓展:
这种基于 版本号 的方式 来进行多线程数据的控制,也是一种乐观锁的典型实现。
1、数据库里
在数据库里面,并发的去通过事务来访问表的时候,这也会涉及到类似加锁的一些多线程操作
2、版本管理工具(SVN)
它就是通过版本号来进行多人开发的协同。
如果别人改过了,就需要先去进行一个拉去数据,再重新提交。
1、 讲解下你自己理解的 CAS 机制
CAS全称 Compare and swap, 即 “比较并交换”. 相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑.
2、ABA问题怎么解决?
给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增;
如果发现当前版本号比之前读到的版本号大, 就认为操作失败
=====================================================================================
这也是属于我们编译器优化,以及说 JVM ,操作系统,它们的一些优化策略所涉及到一些小细节。
这些东西,其实说白了:如果我们不需要去实现 JVM 和 编译器,就并不需要去理解。
但奈何,现在面试都卷到这个份上,那么我们还是得学习一下。
结合上面的锁策略, 我们就可以总结出, Synchronized 具有以下特性(只考虑 JDK 1.8):
1、 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2、 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
3、 实现轻量级锁的时候大概率用到自旋锁策略
4、 是一种不公平锁
5、 是一种可重入锁
6、 不是读写锁
7、实现重量级锁的时候大概率会用到 挂起等待锁。
要知道 Java的版本是非常多的。
在这些版本变迁的过程中, 很多地方都有了不少的变化。
我们 主要以 JDK 1.8 / Java8 和 Java11 为主。
因为这是企业常用的两个版本。
下面这几个,其实也是属于编译器、操作系统、JVM、CPU,它们之间来相互配合完成的一些具体的优化手段。
1、锁膨胀/锁升级
体现了 synchronized 能够 “自适应” 这样的能力。
所以,当我们使用 synchronized 进行加锁的时候,它会根据实际情况来进行逐步升级的。
如果当前没有线程跟它竞争,它就始终保持在偏向锁的状态。
如果有其他现场称跟它竞争,它会升级成一个自旋锁/轻量级锁。
【如果锁竞争就保持轻微的情况下,它就会一直抱着一个 自旋锁的状态】
如果锁竞争进一步加剧,它就会进一步的升级到 重量级锁。
synchronized 就有这样的一个自适应的过程。
【ps:能自动升级,也能自动降级】
2、锁粗化
有锁粗化,也就有锁细化。
此处的粗细指的是“锁的粒度”。
粒度:加锁代码涉及到的范围。
加锁代码的范围越大,认为锁的粒度就 越粗。
加锁代码的范围越小,认为锁的粒度就 越细。
锁粗化,就是把 频繁反复的去进行加锁,合并成一次加锁。
3、锁消除
有些代码,明明不用加锁,结果你给加上锁了。
编译器在编译的时候,发现这个锁好像没有存在的必要,就直接把锁给去掉了。
就比如你当前的代码是处于单线程的情况,你还咔咔的顿加锁操作。
这个时候,编译器就会你创建的锁,都去掉。
有的人可能会有疑问:单线程的代码,有谁会去加锁的?
其实有时候加锁操作并不是很明显,稍不留神就可能会做出这种错误的决定。
不过呢,我们的编译器很难给力,会把我们就把多余的锁进行删除去掉。
保证了我们代码的执行效率。
===========================================================================
JUC: java.util.concurrent
concurrent : 并发
与多线程相关的操作,都在这个包里。
Callable 是一个 interface . 也是一种创建线程的方式。
谈到创建多线程,就会想到Runnable 接口。
但是Runnable 有个问题:不适合于 让线程计算出一个结果,这样的代码。
例如:像创建一个线程,让这个线程计算 1+2+3+…+1000
如果要基于 Runnable 来实现,就很麻烦。
而 Callable 就是要解决 Runnable 不方便返回结果的这个问题。
下面我们来根据实际代码,来看一下 Callable 是解决这个问题的。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test2 {
public static void main(String[] args) {
// 通过 Callable 来描述一个任务
Callable callable = new Callable() {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1;i <= 1_000;i++){
sum+=i;
}
return sum;
}
};
// 为了让线程执行 callable 中的任务
// 光使用构造方法还不够,还需要一个辅助的类: FutureTask
FutureTask task = new FutureTask(callable);
// 创建线程,来完成这里的计算工作
Thread t = new Thread(task);
t.start();
// 凭着task(小票)来获取 call方法的结果(自己的麻辣烫)
// get 操作,
// 如果线程的任务没有执行完,get就会先陷入阻塞
// 一直阻塞到,任务完成了,得出结果为止。
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
ReentrantLock其实就是可重入锁
我们都知道 synchronized 也是一个可重入锁。
现在又蹦出一个 ReentrantLock。
有的人可能会有疑问:这就是既生瑜何生亮啊!
为什么有了 synchronized,还需要 ReentrantLock呢?
这是因为 ReentrantLock 可以做到一些 synchronized 实现不了的功能。
也就是说 ReentrantLock 提供了 一些 synchronized 没有的功能。
ReentrantLock 的 基础用法
ReentrantLock 主要提供了2个方法:
1、lock:加锁
2、unlock:解锁
ReentrantLock把加锁和解锁两个操作给分开了。
这里就和 synchronized 的 差别就很大了。
synchronized 是 加锁和解锁两个操作 给整合在了一起。
那么,加锁和解锁两个操作,是分开好,还是合在一起好?
如果大家多敲一点代码,就能很明显的感觉到 还是合在一起。【synchronized 胜出】
这是因为 分开的做法不太好用。很容易,最后忘记解锁操作 unlock。
一旦没有 unlock ,就容易出现死锁。
通常为了保证 unlock 的执行,我们是像下面这样去写的。
另外,当多个线程竞争同一把锁的时候,就会阻塞。
【这一点和 synchronized一样】
ReentrantLock 和 synchronized 区别
1、synchronized 是一个关键字,ReentrantLock 是一个标准库的类。
关键字就意味着:其背后的逻辑是 JVM 内部实现的。(C++代码实现的)
类:背后的逻辑是 Java代码实现的
2、synchronized 不需要手动释放锁,出了代码块,锁就自然释放了。
ReentrantLock 必须要手动释放锁,要谨防忘记释放。
(重点)3、synchronized 如果竞争锁失败,就会阻塞等待。
ReentrantLock 除了会阻塞等待,还有一手:trylock【失败了,就直接返回】
trylock给我们加锁操作增添了一份灵活性,并不需要完全去进行阻塞死等,可以根据我们的需要,来选择等还是不等,还是说等,以及等多久、
所以 trylock 给了我们更加灵活的回旋余地。
这是synchronized 所不具备的!
(重点)4、synchronized 是一个非公平锁。
而 ReentrantLock 提供了 非公平锁 和 公平锁 两个版本!!!
在构造方法中,通过参数来指定 当前是公平锁,还是非公平锁。
5、基于 synchronized 衍生出来的等待机制,是 wait 和notify。功能是相对有限的。
基于 ReentrantLock 衍生出来的等待机制,是 Condition 类(又可称为条件变量)。
功能上要更丰富一些。
在日常开发中,绝大部分情况下,synchronized 就够用了
之所以 ReentrantLock 会存在,这是由于历史的原因。
因为早期的 synchronized 的功能,并没有现在这么强大。
所以,我们使用 ReentrantLock 对其功能进行扩充。
随着发展,synchronized 的功能已经足够强大了!
所以,除了极个别的情况下,会使用到 ReentrantLock 。
Semaphore 是一个更广义的锁。
锁是信号量里第一种特殊情况,叫做“二元信号量”。
举个例子:开车
开车经常会遇到一个情况,停车。
停车肯定不能乱停,只能停在指定地点。
比如:停车场
当我们开到停车场的时候,我们如何去判断 停车场是否有空位,车能否停进去?
也很简单,停车场入口,一般都有一个牌子。
上面写着“当前空闲 xx 个车位”。
每次有一辆车开进去,车位数 -1
每次有一辆车开出来,车位数 +1;
此时,我们认为这个牌子就是信号量,它描述了可用资源(车位)的个数。
每次申请一个可用资源,计数器就 -1(称为 P 操作)
每次释放一个可用资源,计数器就 +1(称为 V 操作)
当信号量的计数已经是 0 了,再次进行 P 操作,就会阻塞等待。
【牌子显示空闲车位为 0,没位置停车了,那就只能等了。】
这里的 P 和 V 是哪个英文单词的缩写?
很遗憾,这里没有对应的英文单词。
因为提出“信号量”的人,叫做“迪杰斯特拉”(数学家)。
数据结构中的图 里面有一个算法叫做“迪杰斯特拉算法”,能够计算两点之间的距离。这也是他提出的。
注意!他是一个芬兰人,P 和 V 是芬兰语的单词缩写。
意思在英文中对应的单词,P:acquire 申请,V:release 释放
即 P 和 V 操作,又被称为 acquire 和 release 操作。
回过头来,信号量表示的是可用资源的个数。
那么,它和锁有什么关系?
锁可以视为“二元信号量”:可用资源就一个,计数器的取值,非0即1(二元)。
想一下锁是怎么工作?
最后
本人也收藏了一份Java面试核心知识点来应付面试,借着这次机会可以送给我的读者朋友们:
目录:
Java面试核心知识点
一共有30个专题,足够读者朋友们应付面试啦,也节省朋友们去到处搜刮资料自己整理的时间!
Java面试核心知识点
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
每次申请一个可用资源,计数器就 -1(称为 P 操作)
每次释放一个可用资源,计数器就 +1(称为 V 操作)
当信号量的计数已经是 0 了,再次进行 P 操作,就会阻塞等待。
【牌子显示空闲车位为 0,没位置停车了,那就只能等了。】
这里的 P 和 V 是哪个英文单词的缩写?
很遗憾,这里没有对应的英文单词。
因为提出“信号量”的人,叫做“迪杰斯特拉”(数学家)。
数据结构中的图 里面有一个算法叫做“迪杰斯特拉算法”,能够计算两点之间的距离。这也是他提出的。
注意!他是一个芬兰人,P 和 V 是芬兰语的单词缩写。
意思在英文中对应的单词,P:acquire 申请,V:release 释放
即 P 和 V 操作,又被称为 acquire 和 release 操作。
回过头来,信号量表示的是可用资源的个数。
那么,它和锁有什么关系?
锁可以视为“二元信号量”:可用资源就一个,计数器的取值,非0即1(二元)。
想一下锁是怎么工作?
最后
本人也收藏了一份Java面试核心知识点来应付面试,借着这次机会可以送给我的读者朋友们:
目录:
[外链图片转存中…(img-77NNeV6U-1713696362425)]
Java面试核心知识点
一共有30个专题,足够读者朋友们应付面试啦,也节省朋友们去到处搜刮资料自己整理的时间!
[外链图片转存中…(img-w3pQNx1U-1713696362426)]
Java面试核心知识点
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-Zv1og05s-1713696362426)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!