文章目录
- 前言
- 一、常见的锁策略
- 1.乐观锁vs悲观锁
- 2.重量级锁vs轻量级锁
- 3.自旋锁vs挂起等待锁
- 4.读写锁vs互斥锁
- 5.公平锁vs非公平锁
- 6.可重入锁vs不可重入锁
- 可重入锁在哪释放锁
- 7.synchronized具体是采用了哪些锁策略呢?
- synchronized内部实现策略(内部原理)
- 锁消除
- 锁粗化
- 二、死锁
- 1.什么是死锁
- 2.死锁的几个典型的场景
- 3.死锁产生的必要条件
- 4.如何解决死锁的问题
- 三、ReentrantLock可重入锁
前言
一、常见的锁策略
指的不是某个具体的锁。抽象的概念,描述的是锁的特性,描述的是“一类锁"
锁冲突:两个线程尝试获取一把锁,一个线程能获取成功,另一个线程阻塞等待。
1.乐观锁vs悲观锁
1.乐观锁:预测该场景中,不太会出现锁冲突的情况.(后续做的工作会更少)
2.悲观锁:预测该场景,非常容易出现锁冲突.(后续做的工作会更多)
2.重量级锁vs轻量级锁
1.重量级锁:加锁的开销是比较大的(花的时间多,占用系统资源多)
一个悲观锁,很可能是重量级锁(不绝对)
2.轻量级锁:加锁开销比较小的.(花的时间少,占用系统资源少)
一个乐观锁,也很可能是轻量级锁(不绝对)
3.自旋锁vs挂起等待锁
1.自旋锁,是轻量级锁的一种典型实现.
在用户态下,通过自旋的方式(while循环).实现类似于加锁的效果的.
这种锁,会消耗一定的cpu资源,但是可以做到最快速度拿到锁~~
2.挂起等待锁,是重量级锁的一种典型实现.
通过内核态,借助系统提供的锁机制,当出现锁冲突的时候,会牵扯到内核对于线程的调度. 使冲突的线程出现挂起(阻塞等待)
这种方式,消耗的cpu资源是更少的~~也就无法保证第一时间拿到锁.
4.读写锁vs互斥锁
1.读写锁,把读操作加锁和写操作加锁分开了.
一个事实:多线程同时去读同一个变量,不涉及到线程安全问题(此时,多线程并发执行的效率就更高)
如果两个线程,一个线程读加锁,另一个线程也是读加锁,不会产生锁竞争.
如果两个线程,一个线程写加锁,另一个线程也是写加锁,会产生锁竞争.
如果两个线程,一个线程写加锁,另一个线程读加锁,也会产生锁竞争.
5.公平锁vs非公平锁
1.公平锁,是遵守先来后到的锁
2.非公平锁,看起来是概率均等,但是实际上是不公平.(每个线程阻塞时间是不一样的)
操作系统自带的锁(pthread mutex)属于是非公平锁
要想实现公平锁,就需要有一些额外的数据结构来支持.(比如需要有办法记录每个线程的阻塞等待时间)
6.可重入锁vs不可重入锁
如果一个线程,针对一把锁,连续加锁两次,会出现死锁,就是不可重入锁;不会出现死锁,就是可重入锁
如果是不可重用锁就会出现:死锁!!
这里的关键在于,两次加锁,都是“同一个线程".
第二次尝试加锁的时候,该线程已经有了这个锁的权限了。这个时候,不应该加锁失败的,不应该阻塞等待的。
1.不可重入锁:这把锁不会保存,是哪个线程对它加的锁.只要它当前处于加锁状态之后,收到了"加锁”"这样的请求.就会拒绝当前加锁.而不管当下的线程是哪个.就会产生死锁.
2.可重入锁:则是会让这个锁保存,是哪个线程加上的锁.后续收到加锁请求之后,就会先对比一下,看看加锁的线程是不是当前持有自己这把锁的线程,这个时候就可以灵活判定了
synchronized 本身是一个可重入锁
上述代码并不会出现死锁。
可重入锁在哪释放锁
如何判断应该释放锁了呢?
让锁这里持有一个“计数器"就行了。
让锁对象不光要记录是哪个线程持有的锁,同时再通过一个整型变量记录当前这个线程加了几次锁!!
每遇到一个加锁操作,就计数器+1,每遇到一个解锁操作,就–1
当计数器被减为0的时候,才真正执行释放锁操作.其他时候不释放。“引用计数”
7.synchronized具体是采用了哪些锁策略呢?
- synchronized 既是悲观锁,也是乐观锁. (自适应)
- synchronized既是重量级锁,也是轻量级锁. (自适应)
- synchronized重量级锁部分是基于系统的互斥锁实现的;轻量级锁部分是基于自旋锁实现的
- synchronized是非公平锁(不会遵守先来后到.锁释放之后,哪个线程拿到锁,各凭本事)
- synchronized是可重入锁. (内部会记录哪个线程拿到了锁,记录引用计数)
- synchronized不是读写锁.
synchronized内部实现策略(内部原理)
代码中写了一个synchronized之后,这里可能会产生一系列的"自适应的过程",锁升级(锁膨胀)
无锁->偏向锁->轻量级锁->重量级锁
1.偏向锁,不是真的加锁,而只是做了一个"标记".如果有别的线程来竞争锁了,才会真的加锁.如果没有别的线程竞争,就自始至终都不会真的加锁了.
(加锁本身,有一定开销.能不加,就不加。非得是有人来竞争了,才会真的加锁)
2.轻量级锁:sychronized通过自旋锁的方式来实现轻量级锁。
我这边把锁占据了,另一个线程就会按照自旋的方式,来反复查询当前的锁的状态是不是被释放了.
但是,后续,如果竞争这把锁的线程越来越多了(锁冲突更激烈了),从轻量级锁(这个锁操作是比较消耗cpu的如果能够比较快速的拿到锁,多消耗点CPU也不亏.)升级成重量级锁(但是,随着竞争更加激烈即使前一个线程释放锁,也不一定能拿到锁.啥时候能拿到,时间可能会比较久了)。
锁消除
编译器,会智能的判定,当前这个代码,是否有必要加锁.
如果你写了加锁,但是实际上没有必要加锁,就会把加锁操作自动删除掉.
锁粗化
关于"锁的粒度"
如果加锁操作里包含的实际要执行的代码越多,就认为锁的粒度越大
二、死锁
1.什么是死锁
如果是一个服务器程序,出现死锁,
死锁的线程就僵住了,就无法继续工作了,会对程序造成严重的影响
2.死锁的几个典型的场景
死锁的三种典型情况:
1.一个线程,一把锁,但是是不可重入锁.该线程针对这个锁连续加锁两次,就会出现死锁
⒉两个线程,两把锁.这两个线程先分别获取到一把锁,然后再同时尝试获取对方的锁
2的案例:
结果:
3.N个线程M把锁(哲学家就餐问题)
5个哲学家,就是5个线程。5个筷子,就是5把锁
但是,如果出现了极端情况,就会出现死锁。
比如,同一时刻,五个哲学家都想吃面,并且同时伸出左手拿起左边的筷子,再尝试伸右手拿右边的筷子。
3.死锁产生的必要条件
死锁的必要条件:(四个必要条件:缺一不可)
只要能够破坏其中的任意一个条件,都可以避免出现死锁!
1.互斥使用:一个线程获取到一把锁之后,别的线程不能获取到这个锁。
实际使用的锁,一般都是互斥的(锁的基本特性)
2.不可抢占锁,只能是被持有者主动释放,而不能是被其他线程直接抢走。
也是锁的基本的特性.
3.请求和保持.这个一个线程去尝试获取多把锁,在获取第二把锁的过程中,会保持对第一把锁的获取状态。
取决于代码结构(很可能会影响到需求)
4.循环等待. t1尝试获取locker2,需要t2执行完,释放locker2; t2尝试获取locker1,需要t1执行完,释放locker1
取决于代码结构(解决死锁问题的最关键要点~~)
4.如何解决死锁的问题
如果具体解决死锁问题,实际的方法有很多种。(如银行家算法,可以解决死锁问题,但是不太接地气)
介绍一个,更简单有效的解决死锁的方法:针对锁进行编号.并且规定加锁的顺序。
比如,约定,每个线程如果要获取多把锁,必须先获取编号小的锁,后获取编号大的锁.
只要所有线程加锁的顺序,都严格遵守上述顺序,就一定不会出现循环等待!!
三、ReentrantLock可重入锁
这个锁,没有synchronized那么常用,但是也是一个可选的加锁的组件.
lock()加锁
unlock()解锁
ReentrantLock 具有一些特点,是synchronized 不具备的功能:
实际开发中,进行多线程开发,用到锁还是首选 synchronized