24Java多线程之锁的分类与对比

Java中锁的分类有:

  • 可中断锁
  • 可重入锁(递归锁)
  • 公平锁/非公平锁
  • 独占锁(互斥锁)/共享锁
  • 乐观锁/悲观锁
  • 自旋锁
  • 偏向锁/轻量级锁/重量级锁(锁的状态)
  • 分段锁(锁的设计)

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

synchronized与Lock对比

synchronized Lock
别名 隐式锁 显式锁
实现方式 Java中的关键字,语言内置实现 是一个接口
锁的释放方式 同步代码执行结束或抛出异常时自动释放 finally 块中手动释放
多个锁嵌套的释放顺序 依次获取(释放)锁,
最先获取的最后释放
自由获取(释放)锁
发生异常时 自动释放线程占有的锁,不会导致死锁 需在 finally 块中手动释放锁,否则会导致死锁
定时阻塞获取锁 不支持 支持
判断是否已成功获取锁 无法办到 tryLock() 返回值是 boolean
性能 竞争激烈时(大量线程同时获取锁),性能更优
可中断锁
(获取锁时能否响应线程中断指令)

tryLock(long time, TimeUnit unit)
lockInterruptibly()
可重入锁
公平锁 非公平锁,无法判断锁是否公平 默认非公平锁。
实现类的构造方法可指定锁的公平性。isFair() 判断锁是否公平。
独占锁(互斥锁)/共享锁 独占锁(互斥锁) 独占锁:synchronized、ReentrantLock、ReentrantReadWriteLock.WriteLock
共享锁:ReentrantReadWriteLock.ReadLock
通过 AQS(AbstractQueuedSynchronizer)实现独占和共享。
读写锁 ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock

可中断锁

中断锁指的是可中断获取锁的等待状态,也就是在获取锁时可以响应线程中断指令,从而中断获取锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

在 Java 中,synchronized 是不可中断锁,Lock 是可中断锁

可重入锁(递归锁)(特性)

可重入互斥锁 - 维基百科

reentrant :可重入; 可重入的; 重入; 可再入的; 重进入;

可重入锁又名递归锁。特点:

  • 可重复进入持有相同锁的同步作用域(同步代码块、同步方法、Lock锁同步代码)。
  • 在一个线程中可以多次获取同一把锁,但也需要释放同样加锁次数的锁,即重入了多少次,就要释放多少次,不然也会导致锁不被释放。

如果不设计成可重入锁,反复给自己加锁就会产生死锁。

锁类型 锁对象
可重入锁 synchronized
可重入锁 ReentrantLock(Re entrant Lock 重新进入锁)
可重入锁 ReentrantReadWriteLock.ReadLock
可重入锁 ReentrantReadWriteLock.WriteLock
不可重入锁 没有内置对象,需自定义

可重入锁演示

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
51
52
53
54
public class Test {
public static void main(String[] args) {
new Test().同步代码块();
new Test().同步方法();
Test.静态同步方法();
Test.Lock锁();
}

// 可选择锁对象
public void 同步代码块() {
synchronized (this) {
System.out.println("同步代码块,第一次获取锁");
synchronized (this) {
System.out.println("同步代码块,第二次获取锁");
}
}
}

// 对象锁,锁对象是this
public synchronized void 同步方法() {
System.out.println("同步方法,第一次获取锁");
同步方法2();
}

public synchronized void 同步方法2() {
System.out.println("同步方法,第二次获取锁");
}


// 类锁,锁对象是当前类的Class对象
public static synchronized void 静态同步方法() {
System.out.println("静态同步方法,第一次获取锁");
静态同步方法2();
}

public static synchronized void 静态同步方法2() {
System.out.println("静态同步方法,第二次获取锁");
}

// Lock锁
public static void Lock锁() {
Lock lock = new ReentrantLock();
lock.lock();
System.out.println("ReentrantLock,第一次获取锁");
lock.lock();
System.out.println("ReentrantLock,第二次获取锁");
try {
// ... 同步代码
} finally {
lock.unlock();
lock.unlock();
}
}
}

不可重入锁演示

实际开发中几乎不会用到。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class Test {
public static void main(String[] args) {
Lock lock = new UnReentrantLock();
lock.lock();
System.out.println("UnReentrantLock,第一次获取锁");
lock.lock();
System.out.println("UnReentrantLock,第二次获取锁");
try {
// ... 同步代码
} finally {
lock.unlock();
lock.unlock();
}
}
}

/**
* 自定义一个不可重入锁
*/
class UnReentrantLock implements Lock {
// 当前绑定的线程,记录以获取锁的线程
private Thread bindThread;

@Override
public void lock() {
synchronized (this) {
// 当已有线程拿到锁时
while (bindThread != null) {
try {
// 使当前线程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 绑定当前线程
this.bindThread = Thread.currentThread();
}
}

@Override
public void unlock() {
synchronized (this) {
// 当绑定的线程不是当前线程时
if (bindThread != Thread.currentThread()) {
return;
}
// 解绑线程
bindThread = null;
// 唤醒所有线程,唤醒的线程可以继续绑定了
notifyAll();
}
}

@Override
public void lockInterruptibly() throws InterruptedException {

}

@Override
public boolean tryLock() {
return false;
}

@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}

@Override
public Condition newCondition() {
return null;
}
}

公平锁/非公平锁

是否公平:指的是每个线程获取锁的机会是否平等。

公平锁:多个线程按照申请锁的顺序来获取锁。比如同时有多个线程在等待同一个锁,当这个锁被释放时,等待时间最久的线程(最先申请的线程)会获得该锁。

非公平锁:多个线程争抢同一个锁时,有可能后申请的线程比先申请的线程优先获取锁。可能造成优先级反转或者饥饿现象(导致某些线程永远获取不到锁)。优点是比公平锁吞吐量大。

对于 synchronized 而言,也是一种非公平锁。由于其并不像 ReentrantLock 是通过 AQS(AbstractQueuedSynchronizer) 的来实现线程调度,所以并没有任何办法使其变成公平锁。

对于 ReentrantLockReentrantReadWriteLock 实现而言,默认是非公平锁,构造方法可指定锁的公平性

在 ReentrantLock 和 ReentrantReadWriteLock 中都定义了两个静态内部类,分别用来实现非公平锁和公平锁。可以在创建对象时,通过构造方法指定锁的公平性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
// 同步锁的结构和 ReentrantLock 完全相同
}

public class ReentrantLock implements Lock, java.io.Serializable {
// 此锁的同步控制基础。下面分为公平和非公平版本。使用 AQS 状态来表示锁上的持有数量。
abstract static class Sync extends AbstractQueuedSynchronizer {
}
// 公平锁的同步实现
static final class FairSync extends Sync {
}
// 非公平锁的同步实现
static final class NonfairSync extends Sync {
}
}

非公平锁演示

下面示例中,每次都是 Thread-0 拿到锁,验证了 synchronized 是非公平锁。

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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread0.start();
thread1.start();
thread2.start();
}
}

class Task implements Runnable {
@Override
public void run() {
while (true) {
// 同步锁为this的同步代码块
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

修改上面示例 Task 类如下,验证 Lock 的非公平锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Task implements Runnable {
/**
* 默认非公平锁
*/
private Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}

公平锁演示

修改上面示例 private Lock lock = new ReentrantLock(true); 即可。

判断锁是否公平

1
2
3
public final boolean isFair() {
return sync instanceof FairSync;
}

该方法只能判断 ReentrantLock、ReentrantReadWriteLock:

1
2
3
4
5
6
public class Test {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
System.out.println(lock.isFair()); // false
}
}

独占锁(互斥锁)/共享锁

排他锁 - 维基百科互斥锁 - 维基百科

共享锁 - 百度百科

读写锁 - 维基百科

独占锁(互斥):该锁一次只能被一个线程所持有。例如:synchronized、ReentrantLock、读写锁中的写锁 ReentrantReadWriteLock.WriteLock

共享锁(非互斥):该锁可被多个线程所持有。例如:读写锁中的读锁 ReentrantReadWriteLock.ReadLock,但读写、写读 、写写的过程是互斥的。

Lock 锁的 独占共享 是通过 AQS(AbstractQueuedSynchronizer)来实现的。

共享锁演示

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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread0.start();
thread1.start();
thread2.start();
}
}

class Task implements Runnable {
/**
* 读写锁
*/
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

/**
* 读锁
*/
private Lock readLock = this.readWriteLock.readLock();

@Override
public void run() {
this.readLock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
}

独占锁演示

修改上述代码:

1
2
3
4
5
6
class Task implements Runnable {
/**
* 写锁
*/
private Lock writeLock = this.readWriteLock.writeLock();
}

乐观锁/悲观锁

乐观锁与悲观锁指的不是锁类型,而是指 并发策略 是乐观的还是悲观的。

乐观并发控制 - 维基百科

悲观并发控制 - 维基百科

悲观锁:只要编码中 利用到锁,并发策略就是悲观的。

乐观锁:无锁编程,通常采用 CAS 等原子操作来实现。典型的例子就是原子类,通过 CAS 自旋实现原子操作。

自旋锁

自旋锁 - 维基百科

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,避免线程上下文切换带来的开销。但由于获取锁的线程一直处在运行状态,所以不适合单核单线程的CPU。

通常采用 CAS 等原子操作来实现,可以参考 自旋锁的实现

偏向锁/轻量级锁/重量级锁(锁的状态)

这三种锁是指锁的状态,并且是针对 synchronized。在 Java 5 通过引入锁升级的机制来实现高效 synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

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

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

分段锁(锁的设计)

分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

我们以 ConcurrentHashMap 来说一下分段锁的含义以及设计思想,ConcurrentHashMap 中的分段锁称为 Segment,它即类似于 HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素又是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。

当需要 put 元素的时候,并不是对整个 HashMap 进行加锁,而是先通过 hashcode 来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计 size 的时候,就是获取 HashMap 全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。