Java 锁的特性及其常用类的使用
本文总结了 Java 锁的特性分类,及其常用类的使用,包括 synchronized
、ReentrantLock
、ReentrantReadWriteLock
。
作者:王克锋
出处:https://kefeng.wang/2016/11/26/java-locks/
版权:自由转载-非商用-非衍生-保持署名,转载请标明作者和出处。
1 概述
Java 锁用于多线程环境下的同步操作。有两大类:
- 关键字形式:
synchronized
- 类的形式:在
package java.util.concurrent.locks
下,
包括ReentrantLock
、ReentrantReadWriteLock
。
2 相关特性
2.1 公平锁/非公平锁
- 公平锁:各线程严格按照申请锁的顺序获取锁,如
ReentrantLock(true)
,通过AQS的来实现线程调度; - 非公平锁:各线程获取锁的顺序不一定先来先得,可能是来的早不如来的巧(申请时,锁正好可用,但其他线程正好在阻塞中),如
synchronized
和ReentrantLock(false)
(false可缺省)。
差别:非公平锁吞吐量更大,但可能造成饥饿现象。
2.2 可重入锁(递归锁)
同一个线程中,调用堆栈的上层获取了锁,下层再次申请锁时会直接获得。比如 synchronized
和 ReentrantLock
。
优势:同一个线程中递归申请锁时,可避免死锁。
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 | /** |
3.2 AtomicInteger 示例
AtomicInteger
实现了免加锁的线程安全操作。
优势是,避免线程上下文切换、阻塞、甚至死锁,性能远高于锁的机制;
其原理是每个接口内部是个原子操作。
1 | /** |
3.3 AtomicInteger 的实现
1 | public class AtomicInteger extends Number implements Serializable { |
4 ReentrantLock(可重入锁)
Java 中获取锁的操作的粒度是“线程”,而不是“调用”,即不是每一次调用都是建立一个锁。
同一线程,已经持有某个锁的情况下,可以重复再持有该锁(内部用引用计数记录),相应的需要多次解锁;
这一特性可用于,被同一个锁保护的代码中,可以调用另一个使用相同锁的方法。
1 | public class MyClass { |
5 ReentrantReadWriteLock(可重入读写锁)
读锁:读操作相互不排斥,但会排斥写操作;
写锁:其他读操作、写操作,都要排斥;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public 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 | public class MyClass { |
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
13public 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并发编程实践》)。