先回顾一下synchronized缺陷:1、无法控制阻塞时长 2、阻塞不可中断,验证代码如下
public class SynchronizedDefect {
public void method(){
synchronized(this){
try {
System.out.println(Thread.currentThread().getName() + "进来了");
TimeUnit.SECONDS.sleep(10);
System.out.println(Thread.currentThread().getName() + "结束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException{
SynchronizedDefect defect = new SynchronizedDefect();
Thread t1 = new Thread(defect::method,"线程一");
t1.start();
TimeUnit.SECONDS.sleep(1);
Thread t2 = new Thread(defect::method,"线程二");
t2.start();
Thread t3 = new Thread(defect::method,"线程三");
t3.start();
System.out.println(t1.getState());//TIMED_WAITING
System.out.println(t2.getState());//BLOCKED
System.out.println(t3.getState());//BLOCKED
t2.interrupt();// interupt相当于提前中断了t2的阻塞,会修改interupt标识,所以一进入该休眠方法立即进入异常
}
//简单分析:启动三个线程分别调用同步方法,同步方法,首先线程一启动,进入睡眠,另外两个线程进入阻塞状态,之后可能线程二可能线程三抢到锁,假设线程二抢到则直接报错(因为你修改了interrupt标识),
//总结:假设有一个线程进入了同步方法,另一个线程在外等候,你试图利用t2.interropt修改打断标识,让其捕获以此打断阻塞的线程二,发现你并不能打断阻塞线程,只能干巴巴的等到他抢到锁。
}
接下来来实现具备可中断并且可控制阻塞时长的lock接口锁的实现
public interface Lock {
void lock() throws InterruptedException;//可被中断的方法
void lock(long timeout) throws InterruptedException, TimeoutException;//可被中断,并且增加了超时的功能
void unlock();//解锁
List getBlockThreads();//用来获取当前阻塞线程
}
public class BoolLock implements Lock{
private Thread currentThread;//当前拥有锁的线程
private Boolean locked = false;
private final List lockList = new ArrayList<>();//存储阻塞线程
@Override
public void lock() throws InterruptedException {
synchronized (this){
//其它线程能进入此处,因为这不是关联对象调用此方法,而是另一个测试类的方法引用此方法
while (locked){
lockList.add(Thread.currentThread());//孤儿线程 阻塞队列线程
this.wait();//该线程放弃关联引用对象的monitor的所有权,并释放monitor的所有权给其它线程争抢的机会
}
System.out.println("当前线程获得锁" + Thread.currentThread().getName());
lockList.remove(Thread.currentThread());//如果抢到锁了,则从阻塞队列中删除自己(如果没有返回false)
locked = true;
this.currentThread = Thread.currentThread();//拿到锁的线程
}
}
@Override
public void lock(long timeout) throws InterruptedException, TimeoutException {
synchronized (this){
if(timeout <= 0){
try {
throw new TimeException();
} catch (TimeException e) {
e.printStackTrace();
}
}else {
long remainMills = timeout;
long endMills = System.currentTimeMillis() + remainMills;
while (locked){
if (remainMills <= 0){
throw new TimeoutException("当前线程被唤醒或者指定时间wait到了还没获得锁");
}
if(!lockList.contains(Thread.currentThread())){
lockList.add(Thread.currentThread());
}
this.wait(remainMills);
remainMills = endMills - System.currentTimeMillis();//剩余时间
}
System.out.println("当前获得锁的线程:"+Thread.currentThread().getName());
lockList.remove(Thread.currentThread());
this.locked = true;
this.currentThread = Thread.currentThread();
}
}
}
@Override
public void unlock() {
synchronized (this){
//获得锁的线程解锁
if (currentThread == Thread.currentThread()){
this.locked = false;
System.out.println("当前线程解锁" + Thread.currentThread().getName());
this.notify();//唤醒睡眠线程强锁 这里唤醒一个也可以
}
}
}
@Override
public List getBlockThreads() {
return Collections.unmodifiableList(lockList);
}
}
涉及到的自定义异常TimeException类:
public class TimeException extends Exception{
@Override
public void printStackTrace() {
System.out.println("您输入的时间小于0");
}
}
public class Test {
private final Lock lock = new BoolLock();
public void test(){
try {
lock.lock();//这里是Test类调用同步方法
TimeUnit.SECONDS.sleep(5);//谁调用休眠的方法,谁将睡眠5s,这里指获得锁的线程,因为孤儿线程还是waitting状态,不会往下走
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(lock.getBlockThreads());//在解锁前,输出阻塞队列
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException{
Test test = new Test();
//启动五个线程,分别去调用另一个类BolckLock的同步方法,观察线程输出打印信息,这一点与syncronized同步非常类似,接下来验证可中断的特性
IntStream.range(0,5).mapToObj(value -> new Thread(test::test)).forEach(Thread::start);
}
}
这里为止就已经可以发现已经具备了syncronized关键字类似的功能,线程同步并且一次获取锁和解锁,接下来修改一下main方法,继续观察,实现可中中断,继而避免了syncronized不能打断阻塞线程
public static void main(String[] args) throws InterruptedException{
Test test = new Test();
Thread t1 = new Thread(test::test,"T1线程");
t1.start();
TimeUnit.MILLISECONDS.sleep(100);//睡眠10毫秒确保T2后启动
Thread t2 = new Thread(test::test,"T2线程");
t2.start();
System.out.println("T1状态" + t1.getState() + "T2状态" + t2.getState() );
t2.interrupt();//wait和sleep都能接受到中断异常
}
-----------------------输出信息-----------------------
当前线程获得锁T1线程
T1状态TIMED_WAITING,T2状态RUNNABLE
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at Lock.BoolLock.lock(BoolLock.java:20)
at Lock.Test.test(Test.java:13)
at java.lang.Thread.run(Thread.java:748)
[Thread[T2线程,5,main]]//T2线程直接进入finally因为不是解锁线程,所以只输出了一句,T2生命周期结束
[Thread[T2线程,5,]]//T1是解锁线程既输出了阻塞队列,又输出了解锁方法的打印信息
当前线程解锁T1线程
-----------------------修改后输出信息-----------------------
前线程获得锁T1线程
T1状态TIMED_WAITING,T2状态WAITING
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at Lock.BoolLock.lock(BoolLock.java:21)
at Lock.Test.test(Test.java:13)
at java.lang.Thread.run(Thread.java:748)
[Thread[T2线程,5,main]]
当前线程解锁T1线程
当前线程获得锁T2线程 //因为catch获取了异常,即这里可以抓住异常,依然会执行内部的循环
[]
当前线程解锁T2线程
但是此时还存在一个问题,就是阻断的线程还在阻塞的队列中,没有清除,需要将lock()覆写的代码稍加修改,在阻塞线程中加入一个异常捕获即可
@Override
public void lock() throws InterruptedException {
synchronized (this){
//其它线程能进入此处,因为这不是关联对象调用此方法,而是另一个测试类的方法引用此方法
while (locked){
try {
lockList.add(Thread.currentThread());//孤儿线程 阻塞队列线程
this.wait();//该线程放弃关联引用对象的monitor的所有权,并释放monitor的所有权给其它线程争抢的机会
} catch (InterruptedException e) {
lockList.remove(Thread.currentThread());//如果当前阻塞线程被打断,则移除避免内存泄漏
e.printStackTrace();
}
}
System.out.println("当前线程获得锁" + Thread.currentThread().getName());
lockList.remove(Thread.currentThread());//如果抢到锁了,则从阻塞队列中删除自己(如果没有返回false)
locked = true;
this.currentThread = Thread.currentThread();//拿到锁的线程
}
}
上面已经讲解过了提前中断阻塞线程的方法,现在说一下可超时的情况:假设某一个线程抢到锁需要5s睡眠,而其它线程在外面等待,假设我设定其其它线程的等待时间,每个人给它1s,一旦超过1s,则集体罢工。
正常情况:等待时间 >= 5s,则我这里不会进入超时,因为你等待的时间一直被重置,但是重置期间另外一个线程已经走完了,你还在等待中,允许你有时间去拿锁。
等待时间<5s,则会进入超时异常,直接结束掉程序,目的是为了减少过长时间的滞留。修改了一下lock(long timeout)方法
BlockLock类可超时的方法,不必等拿到锁的线程过长时间
@Override
public void lock(long timeout) throws InterruptedException, TimeoutException {
synchronized (this){
if(timeout <= 0){
try {
throw new TimeException();
} catch (TimeException e) {
e.printStackTrace();
}
}else {
long remainMills = timeout;
long endMills = System.currentTimeMillis() + remainMills;
while (locked){
if (remainMills <= 0){
lockList.remove(Thread.currentThread());
throw new TimeoutException("当前线程被唤醒或者指定时间wait到了还没获得锁");//抛出异常:下面代码不执行
}
if(!lockList.contains(Thread.currentThread())){//时间复杂度o(n)
lockList.add(Thread.currentThread());
}
this.wait(remainMills);
remainMills = endMills - System.currentTimeMillis();//剩余时间
}
System.out.println("当前获得锁的线程:" +Thread.currentThread().getName());
lockList.remove(Thread.currentThread());
this.locked = true;
this.currentThread = Thread.currentThread();
}
}
}
//Test类的方法
public void test(){
try {
lock.lock(1000);//这里是Test类调用同步方法
TimeUnit.SECONDS.sleep(5);//谁调用休眠的方法,谁将睡眠5s,这里指获得锁的线程,因为孤儿线程还是waitting状态,不会往下走
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(lock.getBlockThreads());//在解锁前,输出阻塞队列
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException,TimeoutException{
Test test = new Test();
//启动五个线程,分别去调用另一个类BolckLock的同步方法,观察线程输出打印信息,这一点与syncronized同步非常类似,接下来验证可中断的特性
//IntStream.range(0,5).mapToObj(value -> new Thread(test::test)).forEach(Thread::start);
Thread t1 = new Thread(test::test,"T1线程");
t1.start();
TimeUnit.MILLISECONDS.sleep(100);//睡眠10毫秒确保T2后启动
Thread t2 = new Thread(test::test,"T2线程");
t2.start();
System.out.println("T1状态" + t1.getState() + ",T2状态" + t2.getState());
//t2.interrupt();//wait和sleep能接受中断异常
}
--------------控制台信息-------
当前获得锁的线程:T1线程
T1状态TIMED_WAITING,T2状态RUNNABLE
java.util.concurrent.TimeoutException: 当前线程被唤醒或者指定时间wait到了还没获得锁
at Lock.BoolLock.lock(BoolLock.java:49)
at Lock.Test.test(Test.java:15)
at java.lang.Thread.run(Thread.java:748)
[] //T2线finally打印的,因为不是锁线程就没打印解锁
[]//T1线程打印
当前线程解锁T1线程