1.常见的锁策略
(1)乐观锁 和 悲观锁
乐观锁:预测锁竞争的情况不激烈(工作量较少)
悲观锁:预测锁竞争的情况很激烈(工作量较多)
(2)轻量级锁 和 重量级锁
轻量级锁:加锁和解锁的开销较小,效率更高。(乐观锁通常是一个轻量级锁)
重量级锁:加锁和解锁的开销较大,效率较低。(悲观锁通常是一个重量级锁)
(3)自旋锁 和 挂起等待锁
自旋锁:是一种典型的轻量级锁,会不断去尝试获取锁。
挂起等待锁:是一种典型的重量级锁,会进入阻塞队列,暂时不参与cpu调度。
(4)互斥锁 和 读写锁
互斥锁:synchronized锁就是一个互斥锁,如果一个线程加锁了,另一个线程要获取锁,会进入阻塞等待。
读写锁:如果多个线程都只进行读操作,那么此时是没有线程问题的,无需加锁。而如果在锁中的代码有读也有写,或者全是写(简单来说就是代码中带有写操作)才会进行加锁。
(5)公平锁 和 非公平锁
公平锁:假如有两个线程一个阻塞等待了1分钟,一个等待10秒钟,此时锁被释放会先给等待时间长的加锁。
非公平锁:仍然是上面的情况,但是此时它们的获取概率相等。
(6)可重入锁 和 不可重入锁
可重入锁:一个线程重复加多次锁,不会死锁。
不可重入锁:一个线程重复加两次锁,会出现死锁。
2.synchronized实现了哪些锁策略
(1)synchronized既是一个乐观锁又是一个悲观锁
默认是乐观锁,如果竞争激烈会转变为悲观锁。
(2)synchronized既是一个轻量级锁又是一个重量级锁
默认是轻量级锁(乐观锁),如果竞争激烈会转变为重量级锁(悲观锁)。
(3)synchronized的轻量级锁是基于自旋锁的方式实现的;synchronized的重量级锁是基于挂起等待锁的方式实现的。
(4)synchronized是互斥锁,不是读写锁
(5)synchronized是非公平锁
(6)synchronized是可重入锁
3.synchronized原理
synchronized的工作原理是当两个线程针对同一个对象加锁时,会产生阻塞等待。
除此之外,synchronized的内部还有一些优化机制,目的是为了让这个锁更加高效好用。
3.1 锁升级/锁膨胀
synchronized在加锁后会进入这样的几种状态。
(1)无锁
此时synchronized没有被加锁
(2)偏向锁
此时有线程对其加锁,但是此时的加锁并非真正的加锁操作,只是在锁对象上打了一个标记,并没有进行实际的加锁操作,此时的偏向锁比起真正的加锁的开销要少了很多,但是这种状态只存在与没有锁竞争的情况下,如果有其他线程对其尝试加锁,那么就会进行真正的加锁操作,反之,如果执行到了末尾也没有其他线程来加锁,此时就不会加锁,直接把标记消除掉就好了。
(3)轻量级锁
当在偏向锁的阶段有其他线程对其进行加锁,也就是发生锁竞争的时候,那么偏向锁就会升级成为轻量级锁,此时会通过自旋锁的方式进行加锁的,但是不断的去获取锁也会占用cpu资源。此时,如果加锁的线程很快就将锁释放掉,那么自然相安无事,此时的自旋也是划算的;但是,如果迟迟拿不到锁,也就是自旋到一定程度时,就会升级成为重量级锁(挂起等待锁)。
(4)挂起等待锁
如果线程进行了重量级锁的加锁,并且发生了锁竞争,此时竞争的线程就会放入到阻塞队列中,暂时不参与cpu的调度,直到锁被释放,这个线程才有机会被调度到,并且有机会获取锁。
注意:锁的升级是不可以倒退的。
3.2 锁消除
锁消除是编译器的一种智能判定,判断当前代码是否真的需要加锁。
如果加锁代码在某个场景中没有线程安全问题,不需要加锁,那么编译器就会把锁给干掉。
比如StringBuffer,它的关键方法都带有synchronized,但是如果我在单线程的环境下去使用它,那么它的加锁操作就会变得毫无意义,此时编译器就会将锁给干掉了。
3.3 锁粗化
通常情况下,我们希望锁的粒度越细越好,也就是加锁的代码越少越好,因为锁的粒度越细,可以被并发的代码就越多,反之就越少。
但是在有些情况下,锁的粒度反而粗一点会更好,如下:
图中被红色圈住的部分是要加锁的代码,没被圈住的部分是可以不加锁的代码,此时可以看到,每次的解锁与加锁之间的间隔非常的小,因为加锁和解锁的操作也是有一定开销的,在一段代码中频繁大量的进行加锁解锁操作,那么并发带来的效益可能还不如频繁加锁的带来消耗多。就好比:
我完成了三个工作,要汇报给自己的领导
打电话--汇报工作1--挂电话 间隔一分钟
打电话--汇报工作2--挂电话 间隔一分钟
打电话--汇报工作3--挂电话
此时领导一定被你烦的不行,为什么不可以一次性汇报完呢?
在这里也是同样的道理,打电话相当于加锁,挂电话相当于解锁,如果将那些小的间隔也一并加入锁中,如下:
此时,将多个加锁操作合并为一个整体,就可以将上述的情况解决了。