除了关键字synchronized以外,Java还在标准库java.util.concurrent.locks包中包含了锁(Lock)和信号量(Semaphore),为开发人员提供了更加灵活的线程同步方法。
与关键字synchronized的用法相比,java.util.concurrent.locks.Lock有着很多不同之处。
总之,Lock类为开发人员提供了更多的灵活性。但是,总的来说,使用synchronized关键字程序的运行效率比使用Lock类的程序略高。
java.concurrent.locks.Lock是一个接口。Lock接口有如下几个成员方法。
方法名称 | 使用方法 |
---|---|
void lock(); | 线程尝试获取锁对象。如果锁对象已被占用,该线程会等待锁对象被释放。在等待过程中,该线程不会被调度。 |
void lockInterruptibly(); | 该方法与lock()方法类似;唯一的区别是当线程在等待时,可以接收中断(Interrupt)。当接收到中断后,会抛出InterruptedException异常。 |
boolean tryLock(); | 该方法与lock()方法类似;唯一的区别是当成功获取锁对象时,该方法返回true。当锁对象被占用时,该方法立即返回false。当前线程不会处于等待状态。 |
boolean tryLock(long time, TimeUnit unit); | 该方法与tryLock()方法类似,唯一的区别是当锁对象被占用时,当前线程会处于等待状态,直到锁对象被释放;或者超时。 |
void unlock() | 释放所获得的锁对象。 |
如下代码是Java标准库文档提供的代码样例。在finally子句中调用unlock()方法是为了保证无论try语句执行成功或者失败,锁对象都能在最后成功释放。
import java.concurrent.locks.Lock;
import java.concurrent.locks.ReentrantLock;
public class LockExample {
public static void main(String[] args) {
Lock l = new ReentrantLock();
l.lock();
try {
// 访问被锁对象保护的资源
} finally {
l.unlock();
}
}
}
2.1章介绍的是锁Lock接口,在2.2、2.3章节中,我们将介绍两个常用的锁的实现。本小节先介绍java.util.concurrent.locks.ReentrantLock。
ReentrantLock类是一种可重入的锁,它实现了java.concurrent.locks.Lock接口。可重入锁的意义在于,一个线程可以多次获得同一个锁对象的拥有权,而不被阻塞。这个特性与关键字synchronized是一致的。在某些场景下,可重入锁非常有用。例如,在使用递归算法解决问题时,如果在递归函数中使用了锁,则在递归调用的过程中自己可能被自己锁住,而无法完成递归调用。但是,如果在此时使用可重入锁,则不需要担心这个问题,因为,一个线程可以多次获得同一个锁的拥有权,所以,自己不会把自己锁住。
如下面的代码所示,在方法resetAndIncrement()方法中调用了increment()方法。因为在此调用前,当前线程已获取了锁对象this.lock,如果该锁不是可重入的锁的话,那么,在increment()方法中,当前线程不能再次获得锁对象,因为该锁对象已被自身占用了。但是,在本例中,该锁是可重入的,所以,当前线程能够多次获取同一个锁对象。试想,如果this.lock不是可重入的锁,而且如果需要在resetAndIncrement()方法中调用increment()方法的话,那么,这段代码将会变得非常复杂。
import java.concurrent.locks.Lock;
import java.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private Lock lock = new ReentrantLock();
private int counter = 0;
public void increment() {
this.lock.lock();
try {
this.counter++;
} finally {
this.lock.unlock();
}
}
public void resetAndIncrement() {
this.lock.lock();
try {
this.counter = 0;
// 因为这里使用的是可重入的锁,所以在increment()方法中可以成功获取锁对象
this.increment();
} finally {
this.lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 使用Lambda表达式创建Runnable对象
Thread thread1 = new Thread(()-> example.increment());
Thread thread2 = new Thread(()-> example.resetAndIncrement());
thread1.start();
thread2.start();
}
}
java.util.concurrent.locks.ReadWriteLock是一个接口;其内部包含着一个读操作锁(Lock for reads)和一个写操作锁(Lock for writes)。读操作锁允许多个读操作同时执行,而写操作锁只允许在同一时刻,只能有一个写操作执行。读操作和写操作是互斥的,即:读时不能写;写时不能读。ReadWriteLock的一个实现类是ReentrantReadWriteLock类(可重入的读写锁)。
ReadWriteLock接口定义了两个方法。readLock()方法返回一个用于读操作的锁;writeLock()方法返回一个用于写操作的锁。在下面的代码中,ReadWriteLockExample类定义了一个共享的链表sharedList和一个读写锁lock。add()方法和remove()方法在获得写锁后,向共享链表sharedList中添加或删除元素;而contains()方法则在获得读锁后,查询共享链表中是否包含某个元素。当在main()函数中启动四个线程同时运行时,ReentrantReadWriteLock对象会保护共享链表sharedList。
import java.util.List;
import java.util.LinkedList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private List<String> sharedList = new LinkedList<String>();
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void add (String key) {
Lock writeLock = this.lock.writeLock();
try {
writeLock.lock();
this.sharedList.add(key);
} finally {
writeLock.unlock();
}
}
public void remove() {
Lock writeLock = this.lock.writeLock();
try {
writeLock.lock();
if (!this.sharedList.isEmpty())
this.sharedList.remove(0);
} finally {
writeLock.unlock();
}
}
public boolean contains(String key) {
Lock readLock = this.lock.readLock();
try {
readLock.lock();
this.sharedList.contains(0);
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 使用Lambda表达式创建Runnable对象
Thread thread1 = new Thread(()-> example.add(10));
Thread thread2 = new Thread(()-> example.add(10));
Thread thread3 = new Thread(()-> example.remove());
Thread thread4 = new Thread(()-> example.contains(10));
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
与锁不同的是,信号量不是互斥锁(Mutual Exclusion)。信号量允许一定数量的线程同时访问共享资源;而在任意时刻,互斥锁只允许一个线程访问共享资源。在java.util.concurrent.Semaphore类内部保存着一组“许可证(permits)”。当线程调用acquire()方法时,会占用一个信号量中的一个许可证。如果许可证都已被占用,则该线程会进入等待状态。当线程调用release()方法时,会释放一个许可证。实际上,在具体的实现中,信号量对象并不需要在内部创建许可证对象,信号量对象只需维护一个计数器即可。
如下面的代码所示,SemaphoreExample模拟了一个用户管理系统。该系统控制同时登陆用户的数量。当登陆用户数多余上限时,随后的登陆请求将会被阻塞。
所以,在SemaphoreExample中定义了一个私有成员变量semaphore,它将在构造函数中初始化,并设置最多可登陆的用户数。在preLogin()登陆预处理方法中,当前线程会尝试获取一个信号量this.semaphore的“许可证”。如果获取成果,当前线程可以继续执行。如果许可证已用完,当前线程则原地等待。在postLogout()退出后处理方法中,每个登陆的用户会退回一个“许可证”,以允许其他等待的用户完成登陆。
public class SemaphoreExample {
private Semaphore semaphore = null;
public SemaphoreExample(int limit) {
this.semaphore = new Semaphore(limit);
}
// 登陆操作
public void login() {
preLogin();
// 继续处理登陆过程中的其他逻辑
}
// 登陆预处理
private void preLogin() {
this.semaphore.acquire();
}
// 退出操作
public void logout() {
// 处理退出过程中的其他逻辑
postLogout();
}
// 退出后处理
private void postLogout() {
this.semaphore.release();
}
}
本章节介绍了Java的锁和信号量类。与关键字synchronized相比,锁提供了更加灵活的线程同步机制。锁机制不仅可以在不同方法中调用,还可以通过继承以扩展锁的功能(例如:添加公平性等)。Java信号量则允许一定数量的线程同时访问共享资源。
注册用户登陆后可留言