目录
Java常见的锁策略
悲观锁和乐观锁
这是两种不同的锁的实现方式
- 乐观锁,在加锁之前,预估当前出现锁冲突的概率不大,因此在进行加锁的时候就不会做太多的工作。
加锁过程做的事情比较少,加锁的速度可能就更快,但是是更容易引入一些其他的问题(但是可能会消耗更多的CPU资源)
- 悲观锁,在加锁之前,预估当前锁冲突出现的概率比较大,因此加锁的时候,就会做更多的工作,
做的事情更多,加锁的速度可能更慢,但是整个过程中不容易出现其他问题。
轻量级锁和重量级锁
- 轻量级锁,加锁的开销小,加锁的速度更快--->轻量级锁,一般就是乐观锁
- 重量级锁,加锁的开销更大,加锁速度更慢---->重量级锁,一般也就是悲观锁
轻量重量,加锁之后,对结果的评价。
悲观乐观,是加锁之前,对未发生的事情进行的预估
整体来说,这两种角度,描述的是同一个事情。
自旋锁和挂起等待锁
- 自旋锁就是轻量级锁的一种典型实现
进行加锁的时候,搭配一个while循环,如果加锁成功,自然循环结束。如果加锁不成功,不是阻塞放弃CPU,而是进行下一次循环,再次尝试获取到锁。
这个反复快速执行的过程,就称为"自旋",一旦其他线程释放了锁,就能第一时间拿到锁。
同时,这样的自旋锁,也是乐观锁。使用自旋的前提,就是预期锁冲突概率不大,其他线程释放了锁,就能第一时间拿到。
万一当前加锁的线程特别多,自旋意义就不大了,白白浪费CPU了。
- 挂起等待锁,就是重量级锁的一种典型实现,同时也是一种悲观锁。
进行挂起等待的时候,就需要内核调度器介入了,这一块要完成的操作就多了,真正获取到锁要花的时间就更多一些了。
这个锁可以适用于锁冲突激烈的情况
普通互斥锁和读写锁
- 普通互斥锁:类似于synchronized(操作涉及到 加锁 和 解锁)
- 读写锁:这里的读写锁,把加锁分成两种情况:
1)加读锁
2)加写锁
读锁和读锁之间,不会出现锁冲突(不会阻塞)
写锁和写锁之间,会出现锁冲突(会阻塞)
读锁和写锁之间,会出现锁冲突(会阻塞)
一个线程加读锁的时候,另一个线程,只能读,不能写
一个线程加写锁的时候,另一个线程,不能写,也不能读
为啥要引入读写锁???
如果两个线程读,本身就是线程安全的!不需要进行互斥!
如果使用synchronized这种方式加锁,两个线程读,也会产生互斥,产生阻塞(对于性能有一定的损失)
完全给读操作不加锁,也不行,就怕一个线程读一个线程写,可能会读到写了一半的数据
读写锁,就可以很好的解决上述问题。
实际开发中,读操作本身就是非常频繁的,非常常见的
读写锁就能把这些并发读之间的锁冲突的开销给省下了,就对于性能提升很明显了
公平锁和非公平锁
注意,此处定义的 "公平",遵循先来后到,才叫公平!
Java中的synchronized就是非公平的(也就是没有按先后顺序)
要想实现公平锁,就需要引入额外的数据结构(引入队列,记录每个线程先后顺序)才能实现公平锁。(能记录先后顺序的)
使用公平锁,天然就可以避免线程饿死的问题
非公平锁:就是每个线程等概率竞争,不遵循先来后到
可重入锁和不可重入锁
一个线程针对这一把锁,连续加锁两次,不会死锁,就是 可重入锁;会死锁,就是不可重入锁
synchronized是可重入锁。
系统自带的锁,是不可重入的锁。
可重入锁需要记录持有锁的线程是谁,加锁的次数的计数
Java中的synchronized算哪种情况?
synchronized具有自适应能力!!
synchronized在某些情况下是 乐观锁/轻量级锁/自旋锁 ,某些情况下是 悲观锁/重量级锁/挂起等待锁
内部会自动的评估当前锁冲突的激烈程度。
如果当前锁冲突的激烈程度不大,就处于 乐观锁/轻量级锁/自旋锁
如果当前锁冲突的激烈程度很大,就处于 悲观锁/重量级锁/挂起等待锁
不是读写锁
非公平锁
可重入锁
synchronized内部优化地非常好,大部分情况下使用synchronized都是不会有啥问题的(无脑用)
系统原生的锁算哪种情况?
对于系统原生的锁(Linux提供的mutex这个锁)
1.悲观锁
2.重量级锁
3.挂起等待锁
4.不是读写锁
5.非公平锁
6.不可重入锁
synchronized的加锁过程,尤其是"自适应"是咋回事?
当线程执行到synchronized的时候,如果这个对象当前处于未加锁的状态,就会经历以下过程:
1.偏向锁阶段
核心思想,"懒汉模式",能不加锁,就不加锁,能晚加锁,就晚加锁
所谓的偏向锁,并非真的加锁了,而只是做了一个非常轻量的标记
搞暧昧,就是偏向锁,只是做了一个标记。没有真加锁(也不会有互斥)
一旦有其他线程,来和我竞争这个锁,就在另一个线程之前,先把锁获取到
从偏向锁就会升级到轻量级锁(真加锁了,就有互斥了)
如果我搞暧昧的过程中,要是没人来竞争,整个过程就把加锁这样的操作就完全省略了
非必要不加锁。
在遇到竞争的情况下,偏向锁没有提高效率
但是如果在没有竞争的情况下,偏向锁就大幅度的提高了效率。
总的来说,偏向锁意义还是很大的。
2.轻量级锁阶段
(假设有竞争,但是不多)
此处就是通过自旋锁的方式来实现的
优势:另外的线程把锁释放了,就会第一时间拿到锁
劣势:比较消耗CPU
于此同时,synchronized内部也会统计,当前这个锁对象上,有多少个线程在参与竞争
这里当发现参与竞争的线程比较多了
就会进一步升级到重量级锁
对于自旋锁来说,如果同一个锁竞争很多
大量的线程都在自旋,整体CPU的消耗就很大了
3.重量级锁阶段
此时拿不出锁的线程就不会继续自旋了,而是进入"阻塞等待"
就会让出CPU了(不会使CPU占用率太高)
当当前线程释放锁的时候,就由系统随机唤醒一个线程来获取锁了
此处锁 只能 升级,不能降级,自适应这个词,严格的说不算很严谨(但是,保不齐未来某个版本就能降级了)
synchronized中内置的优化策略
锁消除
编译器编译代码的时候,如果发现这个代码,不需要加锁,就会自动把锁给干掉
这里的优化是比较保守的,针对一眼看上去就完全并不涉及线程安全问题的代码,能够把锁消除掉
锁粗化
会把多个细粒度的锁,合并一个粗粒度的锁
synchronized{}大括号里面包含的代码越少,就认为锁的粒度越细
包含的代码越多,就认为锁的粒度越粗
小结
synchronized背后涉及到了很多的"优化手段"
1.锁升级。偏向锁-->轻量级锁-->重量级锁
2.锁消除。自动干掉不必要的锁
3.锁粗化。把多个细粒度的锁合并成一个粗粒度的锁,减少锁竞争的开销
这些机制都是在内部,在看不到的地方默默发挥作用的