首先我们先介绍最常用的锁。锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(不过有些锁可以允许多个线程并发访问共享资源,比如读写锁、信号量等)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,在Java SE 5后,并发包新增了Lock接口以及相关实现类用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是使用时需要显式的获取和释放锁。虽然它缺少了synchronized隐式获取释放锁的便捷性,但是拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字不具备的同步特性。
使用synchronized关键字将会隐式获取锁,但是它将锁的获取释放固化了。虽然这种方式简化了同步的管理,但是扩展性变差了。例如,针对一个场景,先获取锁A,再获取锁B,获取锁B后释放锁A然后获取锁C,获取锁C后释放锁B再获取锁D,此时使用synchronized关键字就很难实现了,而使用Lock却容易很多。
Lock的使用如下:
Lock lock = new ReentrantLock();
lock.lock();
try {
//do something
} finally {
lock.unlock();
}
在finally块释放锁可以保证如果try块的逻辑代码出现异常后,锁能够保证被释放,避免出现死锁等其他并发问题。同时,不要将获取锁的过程写在try块中,如果锁获取过程出现异常,最后会导致锁无故释放。
Lock接口提供的synchronized关键字不具备的特性如下:
特性
描述
尝试非阻塞的获取锁
当前线程尝试获取锁,不管是否成功立即返回
获取锁能被中断
与synchronized关键字不同,获取锁的线程能够响应中断,当获取锁的线程被中断时,中断异常会被立刻抛出
超时获取锁
在指定的截止时间前获取锁,如果截止时间到了仍然未获取到锁,返回
下面是锁的三个特性的示例:
public class LockTest {
private Lock lock = new ReentrantLock();
@Test
public void unblockTest() {
Thread thread = new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquire lock");
SleepUtils.sleep(2);
} finally {
lock.unlock();
}
});
thread.start();
SleepUtils.sleep(1); //等待thread获取到锁
if(lock.tryLock()) { //尝试非阻塞获取锁
//do something
System.out.println(Thread.currentThread().getName() + " acquire lock");
} else {
//do something
System.out.println(Thread.currentThread().getName() + " acquire lock fail");
}
}
@Test
public void interruptedTest() {
Thread thread = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread-1 acquire lock");
SleepUtils.sleep(3);
} finally {
lock.unlock();
System.out.println("Thread-1 release lock");
}
});
thread.start();
Thread thread2 = new Thread(() -> {
boolean interrupted = false;
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
//当获取锁时被中断,可以做其他的事情
System.out.println("Thread2 is interrupted");
interrupted = true;
}
if(!interrupted) {
try {
System.out.println("Thread-2 acquire lock");
SleepUtils.sleep(1);
} finally {
lock.unlock();
System.out.println("Thread-2 release lock");
}
}
});
SleepUtils.sleep(1); //等待线程1获取锁
thread2.start();
SleepUtils.sleep(1); //等待线程2处于获取锁的过程中
thread2.interrupt(); //中断线程2
SleepUtils.sleep(2);
}
@Test
public void timeoutTest() throws InterruptedException {
Thread thread = new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquire lock");
SleepUtils.sleep(2);
} finally {
lock.unlock();
}
});
thread.start();
SleepUtils.sleep(1); //等待thread获取到锁
if(lock.tryLock(1, TimeUnit.SECONDS)) { //超时获取锁
//do something
System.out.println(Thread.currentThread().getName() + " acquire lock");
} else {
//do something
System.out.println(Thread.currentThread().getName() + " acquire lock time out");
}
}
}
输出如下:
Thread-0 acquire lock
main acquire lock fail
Thread-1 acquire lock
Thread2 is interrupted
Thread-1 release lock
Thread-0 acquire lock
main acquire lock time out
以上三种方式能够向开发者提供一种更灵活的方式处理锁获取,而不是只在无法获取到锁时一直等待。
API
Lock接口的基本API如下:
方法名称
描述
void lock()
获取锁,调用该方法后当前线程会获取锁,只有锁获得后才会返回
void lockInterruptibly() throws InterruptedException
可中断的获取锁,和lock()的不同之处在于该方法会响应中断,即在锁的获取过程中可以中断该线程
boolean tryLock()
尝试非阻塞的获取锁,调用该方法立刻返回,如果获取成功返回true,否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
超时获取锁,在以下情况下会返回:
- 当前线程成功获取锁
- 其他线程中断了当前线程
- 指定的等待时间结束
void unlock()
释放锁
Condition newCondition()
获取等待通知组件,该组件和当前锁绑定,当前线程只有获取了锁,才能调用该组件的wait()方法