文章目录
  1. 1 概述
  2. 2 相关特性
    1. 2.1 公平锁/非公平锁
    2. 2.2 可重入锁(递归锁)
    3. 2.3 独享锁(如互斥锁)/共享锁(如读写锁)
    4. 2.4 乐观锁/悲观锁
    5. 2.5 分段锁
    6. 2.6 自旋锁
    7. 2.7 偏向锁/轻量级锁/重量级锁
    8. 2.8 无锁
  3. 3 原子操作类(Atomic)
    1. 3.1 synchronized 方式
    2. 3.2 AtomicInteger 示例
    3. 3.3 AtomicInteger 的实现
  4. 4 ReentrantLock(可重入锁)
  5. 5 ReentrantReadWriteLock(可重入读写锁)
  6. 6 Condition(条件对象)
  7. 7 synchronized 关键字(可重入锁)
  8. 8 各种锁的选用原则

本文总结了 Java 锁的特性分类,及其常用类的使用,包括 synchronizedReentrantLockReentrantReadWriteLock

作者:王克锋
出处:https://kefeng.wang/2016/11/26/java-locks/
版权:自由转载-非商用-非衍生-保持署名,转载请标明作者和出处。

1 概述

Java 锁用于多线程环境下的同步操作。有两大类:

  • 关键字形式:synchronized
  • 类的形式:在 package java.util.concurrent.locks 下,
    包括 ReentrantLockReentrantReadWriteLock

2 相关特性

2.1 公平锁/非公平锁

  • 公平锁:各线程严格按照申请锁的顺序获取锁,如 ReentrantLock(true),通过AQS的来实现线程调度;
  • 非公平锁:各线程获取锁的顺序不一定先来先得,可能是来的早不如来的巧(申请时,锁正好可用,但其他线程正好在阻塞中),如 synchronizedReentrantLock(false)(false可缺省)。

差别:非公平锁吞吐量更大,但可能造成饥饿现象。

2.2 可重入锁(递归锁)

同一个线程中,调用堆栈的上层获取了锁,下层再次申请锁时会直接获得。比如 synchronizedReentrantLock
优势:同一个线程中递归申请锁时,可避免死锁。

2.3 独享锁(如互斥锁)/共享锁(如读写锁)

  • 独享锁:同一时刻只能被一个线程持有,如synchronized / ReentrantLock / ReentrantReadWriteLock.WriteLock
  • 共享锁:同一时刻可以被多个线程持有,如ReentrantReadWriteLock.ReadLock

独享锁与共享锁也是通过AQS来实现。

2.4 乐观锁/悲观锁

  • 乐观锁:乐观地认为并发操作全部是读操作,不加锁没有问题,它适合于读操作很多的场合,比如 JDK 中的原子类,通过无锁的CAS自旋实现原子操作的更新;
  • 悲观锁:悲观地认为并发操作全部是写操作,不加锁必出问题,它适合于写操作很多的场合,比如 JDK 中的锁类。

2.5 分段锁

比如 ConcurrentHashMap,类似于 HashMap,内部是个 Entry 数组;
数组的每个元素是一个链表,通过分段锁 Segment(继承自 ReentrantLock) 实现加锁功能:
进行 put() 操作时,key.hashCode() 不相同时可以并发进行,相同时才加锁。
但进行 size() 这种全局操作时,所有分段都要加锁。
优势:拆分成多段加锁,提高并发性。

2.6 自旋锁

当锁被占用的时间都很短时,线程请求获取锁也会很快,
此时不必采用阻塞方式,因为这需要操作系统进行用户态至核心态的转换,消耗更多的CPU资源;
此时可采用自旋(自我循环)的方式,请求的线程循环重试,以减少线程上下文切换,但会消耗CPU。
自旋锁使用 JVM 选项 -XX:+UseSpinning 开启(JDK6已经默认开启)。

2.7 偏向锁/轻量级锁/重量级锁

synchronized 的偏向锁、轻量级锁、重量级锁是通过Java对象头实现的。

  • 偏向锁:当一段同步代码一直被同一个线程访问时,该线程就会自动获取该锁;
  • 轻量级锁:偏向锁被另一个线程访问时,就会升级为轻量级锁,另一个线程采用自旋方式尝试获取锁;
  • 重量级锁:轻量级锁情况下,如果另一个线程尝试一定次数(默认 -XX:PreBlockSpin=10)之后仍未获得锁,则升级为重量级锁,另一个线程就会阻塞,也阻塞其他更多线程的获取。

2.8 无锁

比如 ThreadLocal / volatile / CAS / 协程(单线程里实现多任务的调度)。

3 原子操作类(Atomic)

Atomic 相关的类有 AtomicBoolean/AtomicInteger/AtomicLong/AtomicReference
下面以 AtomicInteger 为例来说明。

3.1 synchronized 方式

传统方式使用加锁机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author: https://kefeng.wang
* @date: 2016-11-26 10:26
**/
public class AtomicTest {
public static void main(String[] args) {
class AtomicIntegerDemo {
private volatile int i = 0;

public AtomicIntegerDemo(int n) {
i = n;
}

public synchronized int getAndIncrement() { // i++
return i++;
}
}

AtomicIntegerDemo aid = new AtomicIntegerDemo(1);
int n = aid.getAndIncrement(); // i++
}
}

3.2 AtomicInteger 示例

AtomicInteger 实现了免加锁的线程安全操作。
优势是,避免线程上下文切换、阻塞、甚至死锁,性能远高于锁的机制;
其原理是每个接口内部是个原子操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author: https://kefeng.wang
* @date: 2016-11-26 10:26
**/
public class AtomicTest {
public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger(1);
int n = ai.getAndIncrement(); // i++

// 更多接口
n = ai.incrementAndGet(); // ++i
n = ai.getAndDecrement(); // i--
n = ai.decrementAndGet(); // --i
n = ai.getAndAdd(3); // return v; v += 3;
n = ai.addAndGet(3); // v += 3; return v;
n = ai.getAndSet(5); // return v; v = 5;
ai.compareAndSet(5, 6); // if (v == 5) v = 6;
}
}

3.3 AtomicInteger 的实现

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
public class AtomicInteger extends Number implements Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe(); // 支持 CAS 操作的硬件接口类
private static final long valueOffset; // 对象头部 value 域的偏移位置(CAS 操作时要用)

// 计算 valueOffset
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}

private volatile int value; // 当前值(注意 volatile)

public AtomicInteger(int initialValue) {
value = initialValue;
}

public final int get() {
return value;
}

// 【CAS(Compare and Swap) 操作】
// CAS 编译策略:硬件支持则编译为CAS指令,否则编译为加锁操作。
// CAS 内部原理:只有原值与 expect 相等时,才更新原值为 update
// CAS 的优点:在真实环境中,竞争都不是非常大,都是 CAS 性能优于锁的情况;除非极端竞争情况,CAS 才会比锁的性能差。
// CAS 的缺点:需要调用者处理竞争的问题,而悲观锁自身能处理竞争。
public final int incrementAndGet() {
// 内部是循环调用 unsafe.compareAndSwapInt() 直至成功。
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}

4 ReentrantLock(可重入锁)

Java 中获取锁的操作的粒度是“线程”,而不是“调用”,即不是每一次调用都是建立一个锁。
同一线程,已经持有某个锁的情况下,可以重复再持有该锁(内部用引用计数记录),相应的需要多次解锁;
这一特性可用于,被同一个锁保护的代码中,可以调用另一个使用相同锁的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyClass {
private Lock lock = new ReentrantLock();

private void method() {
lock.lock();

try {
// 临界区
} finally {
lock.unlock();
}
}
}

5 ReentrantReadWriteLock(可重入读写锁)

读锁:读操作相互不排斥,但会排斥写操作;
写锁:其他读操作、写操作,都要排斥;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyClass {
private ReadWriteLock rwLock = new ReentrantReadWriteLock(); // 读写锁
private Lock readLock = rwLock.readLock(); // 读锁
private Lock writeLock = rwLock.writeLock(); // 写锁

public void read() { // 读操作(使用 readLock)
readLock.lock();
try {
// read
} finally {
readLock.unlock();
}
}

public void write() { // 写操作(使用 writeLock)
writeLock.lock();
try {
// write
} finally {
writeLock.unlock();
}
}
}

6 Condition(条件对象)

Object.wait() / notify() 是 final 方法,所以 Condition(Object的子类) 只能改名用 await() / signal();
线程已经进入临界区之后,又要满足某个条件才能继续;
一个锁对象可以有一个或多个关联的条件对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyClass {
private boolean condOk = false;
private Lock lock = new ReentrantLock();
private Condition cond = lock.newCondition();

private void method() throws InterruptedException {
lock.lock();

try {
while (!condOk) { // 使用具体条件
// cond.await() 不返回,阻塞了当前线程,并放弃 lock 给其他线程
// 直至其他线程调用 cond.notifyAll() 且 lock 解锁,
// 当前线程的 cond.await() 才返回并持有 lock 继续
cond.await();
}

// 临界区

cond.signalAll(); // 通知条件对象(应该在 condOk 会改变时调用)
} finally {
lock.unlock();
}
}
}

7 synchronized 关键字(可重入锁)

synchronized 在 JDK1.5 中是重量级的、低性能的锁,但 JDK1.6 经过偏向锁、轻量级锁等优化,性能与 ReentrantLock 相当。

每个 Object 对象都有一个内部对象锁(intrinsicLock),并且该锁有一个内部条件(intrinsicCondition):

  • 内部对象锁:synchronized 限定的方法或代码段,相当于首尾包装了对象锁的 lock() / unlock();
  • 内部条件:调用对象的 wait() / notifyAll() 相当于调用了内部条件的 await() / signalAll();

前面 ReentrantLock / Condition 相关代码可以简化为“内部对象锁 / 内部条件”机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyClass {
private boolean condOk = false;

private synchronized void method() throws InterruptedException {
while (!condOk) { // 使用具体条件
wait();
}

// 临界区

notifyAll();
}
}

synchronized 关键字的使用场景:

  • synchronized(obj) {}: 修饰于代码段(且指定obj),使用 obj 的内部对象锁;
  • synchronized method() {}: 修饰于类的实例方法,使用的是当前实例的锁,相当于 synchronized(this) {};
  • synchronized static method() {}: 修饰于类的静态方法,使用的是该类 class 对象的锁,相当于 synchronized(MyClass.class) {}

8 各种锁的选用原则

  • ReentrantReadWriteLock: 共享锁,多读少写的情形;
  • ReentrantLock: 独占锁,仅当需要使用其高级特性(可定时、可轮询、可中断、公平队列、非块结构);
  • synchronized: 独占锁,无需高级特性时,尽量使用本关键字(根据《Java并发编程实践》)。
文章目录
  1. 1 概述
  2. 2 相关特性
    1. 2.1 公平锁/非公平锁
    2. 2.2 可重入锁(递归锁)
    3. 2.3 独享锁(如互斥锁)/共享锁(如读写锁)
    4. 2.4 乐观锁/悲观锁
    5. 2.5 分段锁
    6. 2.6 自旋锁
    7. 2.7 偏向锁/轻量级锁/重量级锁
    8. 2.8 无锁
  3. 3 原子操作类(Atomic)
    1. 3.1 synchronized 方式
    2. 3.2 AtomicInteger 示例
    3. 3.3 AtomicInteger 的实现
  4. 4 ReentrantLock(可重入锁)
  5. 5 ReentrantReadWriteLock(可重入读写锁)
  6. 6 Condition(条件对象)
  7. 7 synchronized 关键字(可重入锁)
  8. 8 各种锁的选用原则