Lock接口是另外一种更加优秀的并发锁设计,调用其lock、unlock方法就可以实现加锁、解锁,需要注意的是,lock、unlock需要成对出现(lock2次,则也需要unlock2次,如果只unlock一次,后续的其他线程也无法再次获取锁),即加锁次数与解锁次数相同,才算是完全释放锁。
Lock接口有如下2个常用到实现类:ReentrantLock、ReadWriteLock中的readerLock、writerLock
一、 ReentrantLock
ReentrantLock是独享锁,支持公平锁、非公平锁,也是可重入锁。
1.1 重入锁示例
/*
执行结果:
外部方法开始执行
内部方法开始执行
内部方法结束执行
外部方法结束执行
*/
public class Demo312 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.err.println("外部方法开始执行");
Thread.sleep(100);
demoMethod();
System.err.println("外部方法结束执行");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
private static void demoMethod(){
lock.lock();
try {
System.err.println("内部方法开始执行");
Thread.sleep(100);
System.err.println("内部方法结束执行");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
1.2 lock2次,只unlock1次的示例:
/*
执行结果(出现死锁,程序未能正常结束):
子线程执行2次加锁
主线程尝试获取锁……
子线程执行1次解锁
*/
public class Demo313 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
try {
Thread.sleep(100);
new Thread(new Runnable() {
@Override
public void run() {
try {
System.err.println("子线程执行2次加锁");
lock.lock();
lock.lock();
} finally {
System.err.println("子线程执行1次解锁");
lock.unlock();
//lock.unlock();
}
}
}).start();
System.err.println("主线程尝试获取锁……");
lock.lock();
System.err.println("主线程获取到锁");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.err.println("主线程释放锁");
lock.unlock();
}
}
}
二、读写锁 ReadWriteLock
ReadWriteLock其实是维护了一对组合锁,改进了独占锁,适合读操作多于写操作的场景。
- readerLock:读锁,只用于读取操作,可多个线程同时持有(我读的时候,你可以读不可以写);
- writerLock:写锁,只用于写入操作,只能单个线程持有(我写的时候,你不可读不可写);
下面分别使用独占锁、读写锁进行读写操作,可以明显看到,读写锁的性能更高。
2.1 使用常规独占锁进行读写操作
public class Demo314 {
public static void main(String[] args) {
new Thread(Demo314::read).start();
new Thread(Demo314::read).start();
new Thread(Demo314::write).start();
}
private synchronized static void read(){
long l = System.currentTimeMillis();
System.err.println(Thread.currentThread().getName()+"正在执行读操作……");
while(System.currentTimeMillis()-l<1000L){
}
System.err.println(Thread.currentThread().getName()+"读操作执行完毕");
}
private synchronized static void write(){
long l = System.currentTimeMillis();
System.err.println(Thread.currentThread().getName()+"正在执行写操作……");
while(System.currentTimeMillis()-l<1000L){
}
System.err.println(Thread.currentThread().getName()+"写操作执行完毕");
}
}
可以看到,读操作是不能并行执行的,执行结果:
Thread-0正在执行读操作……
Thread-0读操作执行完毕
Thread-1正在执行读操作……
Thread-1读操作执行完毕
Thread-2正在执行写操作……
Thread-2写操作执行完毕
2.2 使用读写锁进行读写操作
public class Demo315 {
private final static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final static Lock readLock = lock.readLock();
private final static Lock writeLock = lock.writeLock();
public static void main(String[] args) {
new Thread(Demo315::read).start();
new Thread(Demo315::read).start();
new Thread(Demo315::write).start();
}
private static void read(){
try {
readLock.lock();
long l = System.currentTimeMillis();
System.err.println(Thread.currentThread().getName()+"正在执行读操作……");
while(System.currentTimeMillis()-l<1000L){
}
System.err.println(Thread.currentThread().getName()+"读操作执行完毕");
} finally {
readLock.unlock();
}
}
private static void write(){
try {
writeLock.lock();
long l = System.currentTimeMillis();
System.err.println(Thread.currentThread().getName()+"正在执行写操作……");
while(System.currentTimeMillis()-l<1000L){
}
System.err.println(Thread.currentThread().getName()+"写操作执行完毕");
} finally {
writeLock.unlock();
}
}
}
可以看到,读操作是在并行(交替)执行的,执行结果:
Thread-0正在执行读操作……
Thread-1正在执行读操作……
Thread-0读操作执行完毕
Thread-1读操作执行完毕
Thread-2正在执行写操作……
Thread-2写操作执行完毕
2.3 锁降级
锁降级指的是线程在写操作执行完毕之后,再获取到读锁,将持有的写锁释放的过程。只有锁降级,不存在锁升级。官方给出如下锁降级示例:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
三、Condition中的await、signal与signalAll
synchronized锁对应有wait、notify、notifyAll方法,用于休眠单个、唤醒单个或全部线程,而Condition则是配合Lock使用的,但是一个Lock对象可以对应有多个Condition,这就提供了更多的集合(用于存放不同类型的等待线程),这就拥有了更细粒度的、更加精确的线程控制(notify是随机唤醒,而使用condition就可以有选择地唤醒某一类线程)。
一个典型的场景是阻塞队列:阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列,下图展示了如何通过阻塞队列是如何工作的:
手动模拟一个简单的队列
public class Demo316 {
private static final ReentrantLock lock = new ReentrantLock();
// 读condition
private static final Condition readCondition = lock.newCondition();
// 写condition
private static final Condition writeCondition = lock.newCondition();
private static int count = 2;
private static List<Integer> dataArray = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
new Thread(Demo316::read).start();
new Thread(Demo316::read).start();
new Thread(Demo316::read).start();
Thread.sleep(100L);
new Thread(Demo316::write).start();
new Thread(Demo316::write).start();
new Thread(Demo316::write).start();
}
private static void read(){
try {
lock.lock();
while(dataArray.size() == 0){
readCondition.await();
}
Integer num = dataArray.remove(0);
System.err.println(" -> ️"+Thread.currentThread().getName()+"执行读操作:"+num);
writeCondition.signalAll();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
private static void write(){
try {
lock.lock();
while(count <= dataArray.size()){
writeCondition.await();
}
int num = (int) (Math.random() * 20);
System.err.println(" <- "+Thread.currentThread().getName()+"️执行写操作:"+num);
dataArray.add(num);
readCondition.signalAll();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
执行结果:
<- Thread-3️执行写操作:4
<- Thread-5️执行写操作:17
-> ️Thread-0执行读操作:4
-> ️Thread-1执行读操作:17
<- Thread-4️执行写操作:17
-> ️Thread-2执行读操作:17