​ 在《同步语义-synchronized+final》一文中,我提到了Java中的锁–synchronized。synchronized在Java中是内置锁的实现,通过将方法、变量或者对象设为synchronized来实现自动的加锁和解锁步骤。
但是在Java中,还有多种或显式或隐式的锁来实现并发编程,在这里详细的整理一下。

锁的分类

​ 在Java里,锁能够按照多种方式区分:

依据线程执行的公平性:公平锁和非公平锁
依据能否有多个线程持有同一个锁:共享锁和独占锁、互斥锁和读写锁
按照线程加锁的态度:乐观锁和悲观锁
依据线程持有锁的程度:偏向锁和轻量级锁和重量级锁
除此之外,还有:分段锁、可重入锁、自旋锁、阻塞锁

乐观锁和悲观锁

​ 从宏观上讲,锁分为乐观锁和悲观锁。

乐观锁

​ 乐观锁对上锁保持一种乐观态度,即认为在线程执行过程中,读多写少,遇到并发写的时间较少,所以采用不加锁的方式,也就是无锁编程。在Java中,实现乐观锁的方式为循环CAS方法,只通过在数据提交时的比对,来判断是否应该进行数据更新。这种方式存在ABA问题,循环时间过长的时候开销大,只能保证一个共享变量的原子操作。乐观锁的一致性比悲观锁差,但是对于种地程度的并发,效率大大提高。

1
2
CAS的实现:
compareAndSet(int v, int a);
悲观锁

​ 悲观锁对上锁保持悲观态度,即认为在线程执行过程中,写多读少,遇到并发写的时间较多,所以对于每次读写数据的时候都会进行上锁。在Java中,独占锁或synchronized的实现都是悲观锁的实现。悲观锁的安全性高,更适合高并发的情况。

自旋锁

​ 自旋锁是指线程A想要获取一个锁,但是这个锁被其他线程持有,所以A通过不断地循环来判断锁是否已经被释放,当前可用。
自旋锁在自旋时候并不会释放CPU,所以持有自旋锁的线程需要尽快的释放锁,以让其他需要占有的线程持有锁,防止不断地自旋消耗CPU资源。而如果持有锁的线程不是尽快释放锁,而是将其他线程堵塞,这种实现是阻塞锁。
持有自旋锁的线程在睡眠前或者任务结束后,应当立即释放锁以便让其他线程能够获得锁。

1
2
3
4
5
6
7
自旋锁的实现:
public void lock() {
int myticket = ticketNum.getAndIncrement();
LOCAL.set(myticket);
while (myticket != serviceNum.get()) { //在这里还使用了CAS方法
}
}

阻塞锁

​ 阻塞锁指的是让线程进入阻塞状态进行等待,当获得相应的通知的时候唤醒进程,就绪状态的所有线程通过竞争的方式进入运行状态。
在Java中,重量级锁、ReentrantLock都包含阻塞锁的实现。阻塞锁相对于自旋锁来说,降低了CPU的使用率,但是效率不一定比自旋锁高,因为线程的唤醒进入时间以及回复时间都比自旋锁慢。
在竞争激烈的情况下 阻塞锁的性能要明显高于 自旋锁。理想的情况是; 在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。

可重入锁

​ 可重入锁是指能够重进入的锁,表示一个锁能够支持线程对一个资源的重复加锁。可重入锁有公平锁和非公平锁的实现。
可重入锁中,通过维护一个计数器,来描述持有该锁的线程已经持有了多少次锁,每一次加锁都对应着一次解锁操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可重入锁的实现:
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;
}

公平锁和非公平锁

​ 公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。而对于非公平锁,只要CAS能够设置同步状态成功,那么表示当前线程获取了锁。
在Java中,ReentrantLock分别实现了公平锁和非公平锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
公平锁的实现:hasQueuedPredecessors()方法用于检查等待队列中有没有元素。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
非公平锁的实现:
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}

共享锁和独占锁

​ 独占锁指每次只能有一个线程能够持有锁,ReentrantLock就是以独占方式实现的互斥锁。共享锁,允许读个线程同时持有锁,并发的访问共享资源,ReadWriteLock中的readLock就是一种共享锁的实现。

读写锁

​ 读写锁是Java对于处于读和写两种不同状态下的并发提供的一种锁。读写锁维护了两个锁:读锁和写锁,通过分离读锁和写锁,使得并发性相较于其他的独占锁有了提升。读写锁也是可以选择实现公平锁或者非公平锁。
当有一个线程持有读锁的时候,如果有其他线程想要获得锁,那么便能够获得锁,此时维护的是一个共享锁,并且这个共享锁是支持重进入的。如果线程获取读锁的时候,已经有其他线程获取了写锁,那么这个线程将会进入等待状态。而写锁只有在没有任何线程访问的时候才能够获得,实质上是一个排它锁。

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
41
42
43
44
45
46
47
48
49
50
写锁实现:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
读锁的实现:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}

分段锁

​ 分段锁是在HashMap中的一个实现。HashTable容器在竞争激烈的并发环境中的时候,由于线程会竞争统一把锁,所以会导致效率低下。所以在容器里假设有多把锁,每把锁用于锁容器中的一部分数据,当不同的线程访问不同的数据的时候,就会持有不同的锁,降低了锁竞争的现象,有效的提高并发效率,这种方法就是CurrentHashMap的锁分段技术,将数据分为一段一段的存储,一段数据具有一把锁,一个线程占有一把锁的时候,并不影响其他线程访问其他数据段。

偏向锁、轻量级锁、重量级锁

​ 在synchronized一文中,我简单的提到了这三种锁,我将在一篇文章里详细的介绍这三种锁: 偏向锁、轻量级锁、重量级锁