BooleanLock是对Synchronized锁的扩展性封装
Synchronized锁的作用
- 被Synchronized锁包裹,同一时刻,只能有一个线程在访问,通过线程互斥来达到同步
Synchronized锁的缺陷
- 因争抢Synchronized锁造成线程阻塞,无法中断
- 因争抢Synchronized锁造成线程阻塞,没有时限,会一直阻塞,直到获取到锁。
举个例子:
class MyThread extends Thread{
@Override
public void run() {
synchronized (this){
try {
Log.e("syncMethod","我抢到了锁");
TimeUnit.DAYS.sleep(1);
} catch (InterruptedException e) {
Log.e("syncMethod","我被打断");
e.printStackTrace();
}
}
}
}
public static void main(String[] args){
Thread thread1 = new MyThread ();
Thread thread2 = new MyThread ();
thread1.start();
// 为了保证thread1先启动
TimeUnit.MILLISECONDS.sleep(5);
thread2.start();
thread2.interrupt();
}
如上所示,thread1先启动拿到了锁,睡眠1天,在这个时间内,thread1不会释放锁,于是thread2一直在等待thread1释放锁,并且无法打断,主动调用interrupt方法也无法打断,thread2会一直阻塞。
Synchronized的问题就在于拿到锁的线程如果不释放锁,其他线程就会一直阻塞,而BooleanLock的核心就在于线程在拿到锁后,会访问或者改变一个Boolean值,然后立刻释放锁,这样就不会有线程在等待其他线程释放锁,而这个Boolean值就是记录是否有人拿到了锁。
现在我们来实现一个Boolean锁,一个Boolean锁应该可以打断,并且可以设置阻塞时长,如果在指定时长内无法获取到锁,就抛出异常。
public interface Lock {
/**
* lock方法跟synchronized同步非常类似,除非获取到了锁,否则就会一直阻塞,区别在于此种方法可以中断,抛出异常
* @throws InterruptedException
*/
void lock() throws InterruptedException;
/**
* 相对于lock()方法多了一个超时功能
* @param mills 超过这个时间线程还没有释放锁,会抛出超时
* @throws InterruptedException
*/
void lock(long mills) throws InterruptedException,TimeoutException;
/**
* 释放锁
*/
void unlock();
/**
* 当前哪些线程陷入阻塞
* @return 阻塞的线程
*/
List<Thread> getLockedThreads();
}
实现Lock锁,需要有三个变量,一个Boolean值记录是否有线程获取到了锁,第二个记录当前获取到锁的线程,第三个则是记录当前正在阻塞的线程集合。
首先来实现一个可以打断的锁
private boolean locked;
private Thread currentThread;
private List<Thread> blockedList = new ArrayList<>();
public void lock throws InterruptedException {
//lock方法体使用synchronized包裹,lock完就退出了锁 @1
synchronized(this){
while(locked){
//如果locked标志为true,表示锁被别人获取了
if(!blockedList.contains(Thread.currentThread())){
blockedList.addd(Thread.currentThrad());
}
try {
//locked标志为true,当前线程就等待locked为false,自身等待,wait方法释放真实的锁 @2
this.wait();
} catch (InterruptedException e){
//这里线程是上面的wait方法被打断会跳出,因为线程是调用wait方法主动阻塞
// 而不是在等待synchronized锁释放而陷入阻塞,所以是可以打断的
//所以这里是整个BooleanLock机制的核心!!!
blockedThreadList.remove(Thread.currentThread());
throw new InterruptedException(Thread.currentThread().getName() + "被打断");
}
//如果走到这里,表示之前没有线程获取到锁,我获取到了
blockedThreadList.remove(Thread.currentThread());
//上锁
locked = true;
currentThread = Thread.currentThread();
}
}
}
public void unlock() {
synchronized (this){
//如果当前线程是获取到锁的线程,就可以去执行释放锁的操作
if (currentThread == Thread.currentThread()){
//释放锁
this.locked = false;
//唤醒所有正在wait而陷入阻塞的线程,因为locked已经被设置成false,所以可以进入下一轮争抢
this.notifyAll();
}
}
}
上面的Lock是可以主动unlock,来唤醒正在陷入BooleanLock阻塞的线程,并且打断它,之后是否需要再次抢夺锁,就看自己的需要了。核心在于上面注释的@1和@2两个地方,无论是线程是获取到锁成功,还是已经有线程抢先,而我只能wait,我都释放了真实的锁,没有线程陷入被动的阻塞。
但是,还有一种需要,就是超时,我抢夺一个锁,如果在指定时间内我没有抢到,我就主动跳出这个抢夺,去做其他事情。
实现一个有超时功能的锁
public void lock(long mills) throws InterruptedException,TimeoutException {
synchronized (this){
//如果传入的时间不合法,就直接调用没有超时功能的Lock
if(mills <= 0){
this.lock();
}else{
long lastMills = mills;
//这里记录一个时间戳,totalMills表示到达这个时刻,如果还没有获取到锁,就报超时
long totalMills = lastMills + System.currentTimeMillis();
while (locked){
if (lastMills <= 0){
throw new TimeoutException(Thread.currentThread().getName() + "超时" + mills + ".mills" + "没有获取到锁");
}
if(!blockedThreadList.contains(Thread.currentThread())){
blockedThreadList.add(Thread.currentThread());
}
//这里释放了真实的锁
this.wait(lastMills);
// lastMills表示还需要等待多久时间,而lastMills减少表示两种情况
// 第一种: 线程已经wait到时间了,自动唤醒了,这个时候已经过去了lastMills的时间,下面这个操作就把lastMills置0
// 第二种: 线程wait没有到时间,被unlock唤醒,但是被唤醒后还是没有抢到锁,于是lastMills被减少,
// 继续等待,时间减去已经等待了多长时间
lastMills = totalMills - System.currentTimeMillis();
}
blockedThreadList.remove(Thread.currentThread());
locked = true;
currentThread = Thread.currentThread();
}
}
Boolean的核心就是防止线程被动阻塞,上面注释里面的锁表示一个Boolean值,true表示已经上锁,false表示没有上锁。而注释里面的真实的锁表示synchronized锁。获取到真实的锁后,线程总是很快的释放真实的锁,没有线程需要等待synchronized锁被释放,转而去等待locked值被释放(locked被置为false),这种等待的阻塞,完全是线程主动的阻塞,所有可以被打断。
Tips: 希望没有人问出,为什么locked不标示为volatile...
完整代码
public class BooleanLock implements Lock{
private Thread currentThread;
private boolean locked = false;
private final List<Thread> blockedThreadList = new ArrayList<>();
@Override
public void lock() throws InterruptedException {
synchronized (this){
// lock方法体使用synchronized包裹,lock完就释放真实的锁 @1
while (locked){
if(!blockedThreadList.contains(Thread.currentThread())){
blockedThreadList.add(Thread.currentThread());
}
try {
//locked标志为true,当前线程就等待locked为false,自身等待,wait方法释放真实的锁 @2
this.wait();
}catch (InterruptedException e){
//这里线程是上面的wait方法被打断会跳出,因为线程是调用wait方法主动阻塞
// 而不是在等待synchronized锁释放而陷入阻塞,所以是可以打断的,
//所以这里是整个BooleanLock机制的核心!
blockedThreadList.remove(Thread.currentThread());
throw new InterruptedException(Thread.currentThread().getName() + "被打断");
}
}
//如果走到这里,表示之前没有线程获取到锁,我获取到了
blockedThreadList.remove(Thread.currentThread());
//上锁
locked = true;
currentThread = Thread.currentThread();
}
}
@Override
public void lock(long mills) throws InterruptedException,TimeoutException {
synchronized (this){
//如果传入的时间不合法,就直接调用没有超时功能的Lock
if(mills <= 0){
this.lock();
}else{
long lastMills = mills;
//这里记录一个时间戳,totalMills表示到达这个时刻,如果还没有获取到锁,就报超时
long totalMills = lastMills + System.currentTimeMillis();
while (locked){
if (lastMills <= 0){
throw new TimeoutException(Thread.currentThread().getName() + "超时" + mills + ".mills" + "没有获取到锁");
}
if(!blockedThreadList.contains(Thread.currentThread())){
blockedThreadList.add(Thread.currentThread());
}
//这里释放了真实的锁
this.wait(lastMills);
// lastMills表示还需要等待多久时间,而lastMills减少表示两种情况
// 第一种: 线程已经wait到时间了,自动唤醒了,这个时候已经过去了lastMills的时间,下面这个操作就把lastMills置0
// 第二种: 线程wait没有到时间,被unlock唤醒,但是被唤醒后还是没有抢到锁,于是lastMills被减少,
// 继续等待,时间减去已经等待了多长时间
lastMills = totalMills - System.currentTimeMillis();
}
blockedThreadList.remove(Thread.currentThread());
locked = true;
currentThread = Thread.currentThread();
}
}
}
@Override
public void unlock() {
synchronized (this){
//如果当前线程是获取到锁的线程,就可以去执行释放锁的操作
if (currentThread == Thread.currentThread()){
//释放锁
this.locked = false;
//唤醒所有正在wait而陷入阻塞的线程,因为locked已经被设置成false,所以可以进入下一轮争抢
this.notifyAll();
}
}
}
@Override
public List<Thread> getLockedThreads() {
//unmodifiableList表示不可修改,防止阻塞队列被强行主动修改,出现错误
return Collections.unmodifiableList(blockedThreadList);
}
}