Java中的锁


通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现! - 知乎 (zhihu.com)

锁分类

  • 悲观锁,乐观锁
  • 可重入锁
  • 可中断锁
  • 公平锁,非公平锁
  • 读锁(共享锁),写锁(排他锁/互斥锁)
  • 自旋锁

一、悲观锁与乐观锁

  • 悲观锁:认为数据随时都可能更改,操作之前先加锁,别的人都不能获取此数据。

  • 乐观锁:认为数据一般不会被更改,操作的时候会检查数据是否更新,发现数据变了,重新读取重新进行操作。

悲观锁阻塞事务,乐观锁回滚重试

二、乐观锁基础——CAS

CAS,就是比较并替换,Compare And Swap 。

每一个CAS操作过程都包含3个运算参数:一个内存地址V(当前值),一个期望的值A(旧值)和一个新值B。

CAS的基本思路就是,如果内存地址V上的值和期望的值A相等,则给其赋予新值B,否则不做任何事儿。

CAS就是在一个循环里不断的做CAS操作,直到成功为止。

比如一个基于CAS实现更新的例子(伪代码):

1
2
3
4
5
do{
oldValue = V.get(); //读取共享变量V的旧值
newValue = calculate(oldValue); // 计算变量V的新值
} while(!compareAndSwap(V, oldValue, new Value));
// 在while条件中调用更新,更新成功则true失败则重试

CAS是一个CPU原子指令。

因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!

三、自旋锁

有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。

刚刚的乐观锁就有类似的无限循环操作,那么它是自旋锁吗?

不是。尽管自旋与 while(true) 的操作是一样的,但还是应该将这两个术语分开。“自旋”这两个字,特指自旋锁的自旋。

四、synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

锁升级总结

自己整理了一个流程图:

synchronized锁升级/锁膨胀

synchronized锁只会升级,不会降级。

  • 无锁(即CAS操作)
  • 偏向锁(偏向于一个进程,发生竞争则升级)
  • 轻量级锁(自旋锁,自旋次数达到阈值默认10则升级)
  • 重量级锁

前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。

无锁

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。也就是CAS(CAS是基于无锁机制实现的)。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。

轻量级锁

但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

重量级锁

如果发生竞争,其他竞争线程会阻塞挂起,而不是忙等,直到被唤醒。

其他关于synchronized

死磕synchronized底层实现 - 敖丙 - OSCHINA - 中文开源技术交流社区

我git上的脑图我每次写完我都会重新更新,大家可以没事去看看。

五、可重入锁(递归锁)

当前线程持有锁,当前线程再次获取锁,可以得到,那就是可重入锁。

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

StampedLock是不可重入锁。

六、公平锁、非公平锁

如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。

显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。

ReentrantLock可以传入参数指定是否公平锁。

公平锁参数

synchronized是非公平锁。

七、可中断锁

这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。

回到锁的话题上来,如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Lock接口 */
public interface Lock {

void lock(); // 拿不到锁就一直等,拿到马上返回。

void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

void unlock();

Condition newCondition();
}

八、读写锁、共享锁、互斥锁

  • 读锁,共享锁。
  • 写锁,排他锁,互斥锁。

读锁,写锁,都是悲观锁。

JDK提供的唯一一个 ReadWriteLock 接口实现类是 ReentrantReadWriteLock 。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。 除了两个接口方法以外,ReentrantReadWriteLock还提供了一些便于外界监控其内部工作状态的方法,这里就不一一展开。

九、Java悲观锁乐观锁

我们在Java里使用的各种锁,几乎全都是悲观锁

synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。

JDK提供的Lock实现类全是悲观锁。

其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。

那JDK并发包里到底有没有乐观锁呢?

有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。

问题:CAS与自旋的区别?也就是无锁和轻量级锁/自旋锁的区别?

答:貌似懂了,举个例子,修改变量值:CAS是不断读取变量值,并尝试操作;而自旋锁是不断检查修改值的这段代码锁是否可获取,并不去读具体的值。


文章作者: SongX64
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 SongX64 !
  目录