本篇主要介绍Java中显式锁的内容
- 在Java中,显式锁(
Explicit Lock
)提供了一种比synchronized
关键字更灵活的线程同步机制。Java的显式锁由java.util.concurrent.locks
包提供,最常用的显式锁是ReentrantLock
它是一个可重入锁,类似于synchronized
关键字,但提供了更多的功能和灵活性。;
1. 基本用法
- 用于对变量进行访问的保护,以确保在多线程环境下,线程运行是安全的;
- 具体做法:
//引入两个必要的库和里边的类 import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; …… //在设置变量之后,声明建立一个ReentrantLock类锁,并由Lock创建的对象接收 Lock lock = new ReentrantLock(); …… //在需要的时候获取锁 locks.lock(); try{ 代码块; }finally{ lock.unlock();//用finally的原因是确保在任何情况下都能释放锁,不至于出现死锁的情况。 } //此时,如果有多个方法都在使用相应的变量(体现在代码块中),将获取锁操作视作p操作,释放锁操作视为v操作,则下一个线程/方法在进行操作之前只能等前一个线程释放锁,即使数目上去了,结果也是如此。
2. 高级功能
2.1 可中断的锁获取
- 可以使用
ReentrantLock
尝试获取锁,并在等待锁的过程中响应中断(中断服务程序
); - 这个过程中使用
lock.lockInterruptibly()
方法,它也是ReentrantLock
提供的,但与.lock()
不同,它在获取锁的过程中会响应中断。如果当前线程在等待获取锁时被中断,那么该方法会抛出InterruptedException
异常。(等于是线程a和线程b,两者都使用了lockInterruptibly。当其中一个获取到锁后,引发中断,在等待锁的另一个线程被中断后会自动抛出InterruptedException异常直至锁被重新释放后解除中断
)。本质上不是用等待而是用中断的方式完成使用锁的排他,但得注意要有try--finally
块保证任何情况在都能正确释放锁,不然很快会死锁; - 实例:
//对中断锁的设置 public void safeIncrement() throws InterruptedException { lock.lockInterruptibly(); // 可中断地获取锁 try { count++; } finally { lock.unlock(); } } //一种情况举例 Counter counter = new Counter(); Runnable task = () -> { try { counter.safeIncrement(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.out.println("Thread was interrupted"); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start();
2.2 尝试获取锁
- 使用
Locks
类的.trylock()
方法可以尝试获取锁,并在获取不到时立即返回;
try后边可以同时跟catch和finally,两者发挥的功能不同,一个是抛出异常的说明,另一个是兜底保证代码中是能被执行。
- 实例
public boolean tryIncrement() { if (lock.tryLock()) { // 尝试获取锁 try { count++; return true; } finally { lock.unlock(); } } return false; // 获取锁失败 }
2.3 带超时的尝试获取锁
- 是一种在多线程编程中用于提高灵活性和响应能力的方法;
- 通过这种方法,线程在尝试获取锁时可以指定一个超时时间,如果在指定时间内未能获取锁,线程可以执行其他操作或进行适当的处理,而不是无限期地等待锁;
- 在Java中,可以使用
ReentrantLock
类的tryLock(long timeout, TimeUnit unit)
方法来实现带超时的尝试获取锁;
实现步骤:
- 用
ReentrantLock()
定义锁;- 用
tryLock(时间数目,时间单位)
尝试在指定时间内获取锁并传值给一个布尔型变量存储,如果成功了返回true
,如果失败了返回false
;- 如果获取锁成功了,则执行临界区的代码;反之如果失败了,执行其他操作(
如记录日志或者返回特定结果
);- 如果在等待锁的过程中被中断了,则抛出
InterruptionException
异常,并在catch
块中处理该异常,调用Thread.currentThread().interrupt()
方法恢复中断状态;- 无论成功与否,最后确保释放锁,用到
finally
块,.unlock()
操作只有当成功取到锁后才会被调用。
一个实例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();//获取锁
public boolean tryIncrementWithTimeout() {
boolean acquired = false;//默认无锁
try {
acquired = lock.tryLock(1, TimeUnit.SECONDS); // 尝试在1秒内获取锁
if (acquired) {
count++;
return true;
} else {
System.out.println("Could not acquire lock within the specified time.");
return false;
}//成功或者失败的情况的处理方式
//遇到中断,抛出异常并使用类方法恢复
} catch (InterruptedException e) {
System.out.println("Thread was interrupted while waiting for the lock.");
Thread.currentThread().interrupt();
return false;
} finally {
if (acquired) {
lock.unlock(); // 确保锁被释放
}
}
}
public int getCount() {
return count;
}
//标准测试的主函数
public static void main(String[] args) {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.tryIncrementWithTimeout();
}
};//Lambda表达式,接口实体化
//将接口转为线程后建立对象
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
//线程启动
t1.start();
t2.start();
try {
//join用于使一个线程可以等待另一个线程执行完毕后再继续执行,这里使用它是为了产生可能的异常来表明线程在等待期间被中断。
t1.join();
t2.join();
} catch (InterruptedException e) {
//printsStackTrace的问题又是一个巨坑,之后开专篇解答
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
2.4 ReadWriteLock
接口
- 提供了一种更细粒度的锁机制,适用读多写少的场景;
ReadWriteLock
接口有两个锁:一个读锁和一个写锁。读锁是共享的,写锁是排他的;- 这也是操作系统的知识,具体原理需要去翻书,但是简而言之是读写锁的使用,有效解释了同步的合理情况只有“读读、先读后写、先写后读”三种情况;
- ReadWriteLock 接口包含以下方法:
Lock readLock()
: 返回一个读锁。Lock writeLock()
: 返回一个写锁。
- Java 提供了一个
ReadWriteLock
接口的标准实现类ReentrantReadWriteLock
。这是一个可重入的读写锁实现,允许线程持有多个读锁,或一个写锁(这里是指默认的情况,正常是有读锁后不能有写锁。但在某些实现中,如果线程已经持有读锁,则可以获得写锁,这种情况我依旧会开新的篇章叙述
)。
如何使用读写锁
ReadWriteLock
:
- 定义锁,使用
ReentrantReadWriteLock
类;writeLock.lock()
获取写锁,执行写操作,在finally
中writeLock.unlock()
释放写锁;readLock.lock()
获取读锁,执行读操作,在finally
中readLock.unlock()
释放读锁。允许多个读线程同时执行,但在有写线程执行的时候,读操作将被阻塞;- 在线程内异常之前使用带有读写锁的读写相关操作;
- 使用
join()
方法平均各线程情况,等待所有线程完成。
一个实例:
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.Lock;
public class ReadWriteLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private int value;
// 写操作
public void setValue(int value) {
writeLock.lock();//包含写锁的写操作
try {
this.value = value;
System.out.println(Thread.currentThread().getName() + " wrote " + value);
} finally {
writeLock.unlock();
}
}
// 读操作
public int getValue() {
readLock.lock();//包含读锁的读操作
try {
System.out.println(Thread.currentThread().getName() + " read " + value);
return value;
} finally {
readLock.unlock();
}
}
//Java的方法非常鸡贼的避开了C++中关于读写操作的复杂实现的内容。
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 创建写线程
Thread writer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.setValue(i);//写操作,继承写锁性质
try {
Thread.sleep(100); // 模拟写操作的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Writer");
// 创建读线程
Thread reader1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.getValue();//读操作,继承读锁性质
try {
Thread.sleep(50); // 模拟读操作的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Reader1");
Thread reader2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.getValue();//同上
try {
Thread.sleep(50); // 模拟读操作的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Reader2");
//启动进程
writer.start();
reader1.start();
reader2.start();
//等待所有线程完成
try {
writer.join();
reader1.join();
reader2.join();
} catch (InterruptedException e) {
e.printStackTrace();//出现中断,抛出异常,这个方法又是个坑,另外叙述。
}
}
}
总结
- 显式锁(如
ReentrantLock
)提供了比synchronized
关键字更灵活的同步机制,包括可中断的锁获取、尝试获取锁和带超时的锁获取。ReadWriteLock
则提供了更细粒度的锁机制,适用于读多写少的场景。通过这些工具,可以编写出更高效和灵活的并发代码。