Zer0e's Blog

谈谈锁的种类

字数统计: 2.2k阅读时长: 8 min
2020/08/14 Share

前言

今天不谈设计模式,本来想写一篇从源码看hashmap,但是不传图片光靠文字要讲解清楚确实有点困难。
那我们来聊聊常见的锁的种类,锁的思想在多线程开发中基本上都是互通的,而且在数据库中我们也会用到。那就开始这篇文章吧。

正文

乐观锁与悲观锁

乐观锁的乐观在于使用者认为每次读取数据别人都不会修改,所以不进行上锁操作,但是更新的时候会判断是否其他人已经更新过了这个数据,如果别人已经修改了,则重复读,再进行更新。一般情况下这种是通过版本号来实现,即来数据库中添加一个列为更新版本号,如果版本号与上一次一致,则数据没有变动,如果不一致,则重复读然后比较再写。
java中的乐观锁通过CAS操作实现,比较当前值与传入值是否一样,如果一样更新,不一样则失败。而SQL的乐观锁则是使用版本号或者更新时间的列来实现。
乐观锁适合读多写少的情况。

悲观锁与乐观锁相反,认为每次读取数据时都是旧的,所以会在每次读取数据时都进行上锁操作。
java中常见的悲观锁就是Synchronized。

自旋锁

自旋锁就是在等待其他线程释放资源的同时,不阻塞,即没有让出cpu,不切换,而是通过一个自旋的操作来不断使用cpu进行使用,减少了切换线程所需要的开销。
自旋操作就是不断消耗cpu,直到超时都没有获取到锁时,才进行阻塞。
自旋的优点在于减少了线程的阻塞,如果锁的竞争不激烈,那自旋锁对性能的提升是巨大的,因为线程之间的切换是有消耗的。缺点也很明显,就是如果锁竞争激烈,那自旋锁就会在获取锁之前占用cpu,而等待时间长则自旋的消耗可能会小于线程阻塞再唤醒的消耗,导致cpu的浪费。

公平锁与非公平锁

这个概念比较好理解,就是根据锁提出获取请求的先后顺序来分配锁,如果按顺序,那就是公平锁,如果可以插队,那就是非公平锁。
synchronized是非公平锁,而ReentrantLock可以通过传入参数指定是否公平。
我们可以来看看ReentrantLock的构造函数。

1
2
3
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

来看看公平锁与非公平锁的差别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

// 非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

可以发现公平锁多了一个!hasQueuedPredecessors()条件,跟进

1
2
3
4
5
6
7
8
9
10
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

就是判断当前线程是否是同步队列中对的第一个。

可重入锁与非可重入锁

这个概念就是指一个线程在外层方法获得锁时,内层方法是否自动获得锁(前提是锁是同一个),在内层自动获得锁的就是可重入锁。
synchronized与ReentrantLock都是可重入锁。
可重入锁一定程度上可以避免死锁。即在循环调用时不会出现循环等待。
就拿synchronized举例,假设实例中有两个方法,方法都是synchronized修饰的,所以锁住的是实例对象,而方法A中调用了方法B,如果不是可重入锁,那么在A调用B时,B会出现等待A释放锁的情况,导致死锁。

可重入锁的原理是维护了一个status变量,来计数重入次数。当线程尝试获取锁时,先获取status值,如果status==0,表示没有其他线程在执行同步代码,就将status置为1,如果status!=0,就判断当前线程是否已经获取了这个锁,是的话status+1,并且可以继续获得锁。
而释放时,先获取status值,如果当前线程持有锁,则status-1,如果status这时等于0,则表示该线程重复获取锁的次数已经为0,这时就会释放锁。

不可重入锁则是status值要嘛为1要嘛为0,如果为1则其他获取锁的操作会失败,0的时候锁释放。

独享锁(排它锁)与共享锁

独享锁又称排它锁,指的是该锁只能被一个线程持有。
而共享锁指的是锁可以被多个线程持有。
Java中常见的共享锁有ReadWriteLock,即读写锁,允许多个读操作或者一个写操作。
而Mysql中则可以使用lock in share mode来添加共享锁,其他session可以查询数据,也可以再次添加共享锁。
使用for update来实现排它锁。

无锁 偏向锁 轻量级锁 重量级锁

这几个概念放在一起说,他们的区别在于在线程竞争同步资源时,获取锁的流程细节的差别。
无锁就是字面意思,不锁住资源,所有线程都能访问和修改同一个资源,但只有一个线程能成功。
偏向锁是指同一段同步代码一直被同一个线程访问,那么之后该线程在访问这段代码时,就会自动获取锁,减少获取锁的代价。
轻量级锁是指在偏向锁的情况下,有其他线程访问了同步代码,那偏向锁就会变成轻量级锁,当线程尝试获取锁时,不会阻塞,而是自旋。
重量级锁是指在轻量级锁时,如果等待的线程自旋次数过多,或者有第三个线程访问时,升级为重量级锁,所有等待的线程都会阻塞。
锁的状态只能升级不能降级。

这几种锁的实现在于Java对象头中的标记字段。


锁状态 前30位 后2位(锁标志位)
无锁 对象的hashCode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10

首先是偏向锁,当一个线程获取锁是,会在标记字段中存储当前线程id,在进入及退出同步代码时不通过cas操作来获取锁和解锁,而是检测标记字段中是否存储着与当前线程id一致的值,与是否是偏向锁的值。
当其他线程竞争时,持有偏向锁的线程才会释放偏向锁。

然后是轻量级锁,在线程进入同步块时,如果是无锁状态,就拷贝标记字段到一个名为锁记录的空间。拷贝完成后,将标记字段更新为指向锁记录的指针,然后把锁记录中的owner指针指向标记字段。
操作成功后,那么线程就获得该对象的锁,如果标记字段中的锁标志位为”00”,表示正处于轻量级锁状态。
如果操作失败,则检查标记字段是否指向当前线程,如果是,则该线程已经获得了锁,否则进行竞争。

重量级锁,在前30位中指向重量级锁monitor的指针,标记字段中锁标志位为”10”。

总结

以上讲解了常用的锁的分类,对其进行了简单介绍,有些地方从源码方式讲解实现原理,并给出了java用法,有些地方给出了mysql的用法。
其实从源码去分析是十分好的方法,从源码看问题能学到更多东西。

参考

不可不说的Java“锁”事

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 乐观锁与悲观锁
    2. 2.2. 自旋锁
    3. 2.3. 公平锁与非公平锁
    4. 2.4. 可重入锁与非可重入锁
    5. 2.5. 独享锁(排它锁)与共享锁
    6. 2.6. 无锁 偏向锁 轻量级锁 重量级锁
  3. 3. 总结
  4. 4. 参考