在Java1.5版本以前,我们开发多线程程序只能通过关键字synchronized进行共享资源的同步、临界值的控制,虽然随着版本的不断升级,JDK对synchronized关键字的性能优化工作一直都没有停止过,但是synchronized在使用的过程中还是存在着比较多的缺陷和不足,因此在1.5版本以后JDK增加了对显式锁的支持,显式锁Lock除了能够完成关键字synchronized的语义和功能之外,它还提供了很多灵活方便的方法,比如,我们可以通过显式锁对象提供的方法查看有哪些线程被阻塞,可以创建Condition对象进行线程间的通信,可以中断由于获取锁而被阻塞的线程,设置获取锁的超时时间等一系列synchronized关键字不具备的能力
文章目录
1 Lock接口
Lock接口是对锁操作方法的一个基本定义,它提供了synchronized关键字所具备的全部功能方法,另外我们可以借助于Lock创建不同的Condition对象进行多线程间的通信操作,与关键字synchronized进行方法同步代码块同步的方式不同,Lock提供了编程式的锁获取(lock())以及释放操作(unlock())等其他操作
public interface Lock {
/**
尝试获取锁,如果此刻该锁未被其他线程持有,则会立即返回,并且设置锁的hold计数为1;
如果当前线程已经持有该锁则会再次尝试申请,hold计数将会增加一个,并且立即返回;
如果该锁当前被另外一个线程持有,那么当前线程会进入阻塞,直到获取该锁,
由于调用lock方法而进入阻塞状态的线程同样不会被中断,这一点与进入synchronized同步方法或者代码块被阻塞类似。
*/
void lock();
/**
该方法的作用与前者类似,
但是使用该方法试图获取锁而进入阻塞操作的线程则是可被中断的,也就说线程可以获得中断信号。
*/
void lockInterruptibly() throws InterruptedException;
/**
调用该方法获取锁,无论成功与否都会立即返回,线程不会进入阻塞状态,
若成功获取锁则返回true,若获取锁失败则返回false。
使用该方法时请务必注意进行结果的判断,否则会出现获取锁失败却仍旧操作共享资源而导致数据不一致等问题的出现。
*/
boolean tryLock();
/**
该方法与tryLock()方法类似,只不过多了单位时间设置,
如果在单位时间内未获取到锁,则返回结果为false,
如果在单位时间内获取到了锁,则返回结果为true,同样hold计数也会被设置为1。
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
当某个线程对锁的使用结束之后,应该确保对锁资源的释放,以便其他线程能够继续争抢,unlock()方法的作用正在于此。
**/
void unlock();
/**
创建一个与该lock相关联的Condition对象
**/
Condition newCondition();
}
2 ReentrantLock
在显式锁Lock接口的诸多实现中,我们用得最多的就是ReentrantLock,该类不仅完全实现了显示锁Lock接口所定义的接口,也扩展了对使用显式锁Lock的一些监控方法。
/**
查询当前线程在某个Lock上的数量,
如果当前线程成功获取了Lock,那么该值大于等于1;
如果没有获取到Lock的线程调用该方法,则返回值为0。
**/
public int getHoldCount();
/**
判断当前线程是否持有某个Lock,
由于Lock的排他性,因此在某个时刻只有一个线程调用该方法返回true。
**/
public boolean isHeldByCurrentThread();
/**
判断Lock是否已经被线程持有。
**/
public boolean isLocked();
/**
创建的ReentrantLock是否为公平锁。
**/
public final boolean isFair();
/**
在多个线程试图获取Lock的时候,
只有一个线程能够正常获得,其他线程可能(如果使用tryLock()方法失败则不会进入阻塞)会进入阻塞,
该方法的作用就是查询是否有线程正在等待获取锁。
**/
public final boolean hasQueuedThreads();
/**
在等待获取锁的线程中是否包含某个指定的线程。
**/
public final boolean hasQueuedThread(Thread thread);
/**
返回当前有多少个线程正在等待获取锁。
**/
public final int getQueueLength();
3 正确使用显式锁Lock
锁的存在,无论是Lock接口还是synchronized关键字,主要是帮我们解决多线程资源的竞争问题,也就是说在同一时刻只能有一个线程对共享资源进行访问,即排他性,另外就是确保若干代码指令执行的原子性。
3.1 确保已获取锁的释放
使用synchronized关键字进行共享资源的同步时,JVM提供了两个指令monitor enter和monitor exit来分别确保锁的获取和释放操作,这与显式锁Lock的lock和unlock方法的作用是一致的,只是这个不需要我们显式调用,但是显式锁的释放就需要我们主动调用
很容易就想到使用try…finally语句块可以确保获取到的lock将被正确释放
private final Lock lock = new ReentrantLock();
public void foo(){
// 获取锁
lock.lock();
try{
}finally{
// finally语句块可以确保lock被正确释放
lock.unlock();
}
}
// ReentrantLock释放锁的源码
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程未获得该锁,那么调用unlock方法将会抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可以看到lock不允许未获得锁的线程调用unlock()方法
3.2 可重入性的控制
lock和synchronized关键字一样都具备可重入性,lock的内部维护了hold计数器,而synchronized的内部则维护了monitor计数器,它们的作用都是一样的,若成功获取锁的初始值为1,那么持有该锁时再次获取锁除了会立即成功之外,对应的计数器也会随之自增,在使用synchronized关键字的时候,JVM会为我们担保这一切,但是显式锁的使用则需要程序员自行控制
public static void main(String[] args) throws InterruptedException {
final ReentrantLock lock = new ReentrantLock();
new Thread(() ->{
try {
// 加锁
lock.lock();
System.out.println(Thread.currentThread().getName() + " 获取到锁");
// 首次获取lock,hold的计数器为1
System.out.println("T1首次获得锁,计数器为 " + lock.getHoldCount());
// lock重入,hold的计数器随之增加1个
lock.lock();
System.out.println(Thread.currentThread().getName() + " 再次获取到锁");
// lock重入,hold的计数器随之增加1个
System.out.println("锁重入,计数器为 " + lock.getHoldCount());
}finally {
// 释放lock,但是对应的hold计数器只能减一
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
// 因此当前线程还持有该锁
System.out.println("T1释放锁,计数器为 " + lock.getHoldCount());
}
},"T1").start();
// 休眠一下,使T1运行,并成功获得锁释放锁
TimeUnit.SECONDS.sleep(2);
// 阻塞,永远不会获取锁
lock.lock();
System.out.println("主线程获得锁");
lock.unlock();
System.out.println("主线程释放锁");
}
T1 获取到锁
T1首次获得锁,计数器为 1
T1 再次获取到锁
锁重入,计数器为 2
T1 释放锁
T1释放锁,计数器为 1
从上面的代码中,很明显可以看到lock被重入(多次获取),每一次的重入都会在hold计数器原有的数量基础之上加一,显式锁lock需要程序员手动控制对锁的释放操作。lock被第二次获取之后只进行了一次unlock操作,这就导致当前线程对该锁的hold数量仍旧是非0,因此并未完成对该锁的释放行为,进而导致其他线程无法获取该锁处于阻塞状态
若程序出现这样的情况则是非常危险的,因为匿名线程在生命周期结束之后,线程本身的对象引用还被AQS的exclusiveOwnerThread所持有,但是线程本身已经死亡,这样一来就没有任何线程能够对当前锁进行释放
3.3 避免锁的交叉使用引起死锁
这个和交叉使用关键字synchronized一样,可能会导致死锁
3.4 多个原子性方法的组合不能确保原子性
无论是synchronized关键字还是lock锁,其主要作用之一都是保证若干代码指令的原子操作,要么都成功要么都失败,也就是说在代码指令的运行过程中不允许被中断。
public class ReentrantLockDemo2 {
// 定义一个累加器,内部方法都是线程安全的
private static class Accumulator {
// 定义一把锁
private static final Lock lock = new ReentrantLock();
private int x = 0;
private int y = 0;
void addX() {
lock.lock();
try {
x++;
} finally {
lock.unlock();
}
}
void addY() {
lock.lock();
try {
y++;
} finally {
lock.unlock();
}
}
int getX() {
lock.lock();
try {
return x;
} finally {
lock.unlock();
}
}
int getY() {
lock.lock();
try {
return y;
} finally {
lock.unlock();
}
}
}
// 定义一个线程类,对Accumulator进行操作
private static class AccumulatorThread extends Thread {
private final Accumulator accumulator;
private AccumulatorThread(Accumulator accumulator) {
this.accumulator = accumulator;
}
/**
* 不断地调用addX和addY,
* 根据我们的期望,x和y应该一样,但是事实并非如此
*/
@Override
public void run() {
while (true) {
accumulator.addX();
accumulator.addY();
if (accumulator.getX() != accumulator.getY()) {
System.err.printf("The x:%d not equals y:%d\n", accumulator.getX(), accumulator.getY());
}
}
}
}
public static void main(String[] args) {
// 启动10个线程
final Accumulator accumulator = new Accumulator();
for (int i = 0; i < 10; i++){
new AccumulatorThread(accumulator).start();
}
}
}
Accumulator类,每一个方法都是线程安全的方法,因此也可以说每一个方法的执行都是原子性的,但是在AccumulatorThread中使用了多个原子性方法的组合,其结果未必就是原子性的了,执行程序会出现很多x和y不相等的情况,甚至出现x和y相等还被输出的情况:
The x:142146728 not equals y:142146729
The x:142146951 not equals y:142146951
The x:142147171 not equals y:142147172
The x:142147414 not equals y:142147414
The x:142147688 not equals y:142147688
The x:142147912 not equals y:142147912