JUC高级(三)

134 阅读8分钟

JUC高级(三)

想看前两章的可以

JUC高级(一)

JUC高级(二)

元气满满的更新ing!!

线程中断机制

首先,一个线程不应该由其他线程强制中断或者停止,而是应该由线程自己停止,自己来决定自己的命运。

Tread.stop Thread.suspend,Thread.resume均被废弃了

因为这些方法很不安全,导致死锁的情况也是常有发生的。

其次,虽然在java中无法立即停止一条线程,然而停止线程却尤其重要,比如取消一个耗时的操作。

因此,java提供了一种停止线程的协商机制——中断,即中断表示协商机制。

注意:中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。

只是别的线程通过某种标识位过来告诉你,你该中断了。至于你接受到这个通知做出什么样的行为,完全取决于你自己。

中断相关API——三大方法

  • public void interrupt()

    实例方法,仅仅只是设置线程中断状态为true,发起一个协商而不会立即停止线程。

  • public static boolean interrupted()

    静态方法,判断线程是否被中断并且清除当前的中断状态,变成false。这个方法返回的是中断状态。

  • public boolean isInterrupted()

    实例方法,判断当前的线程是否被中断。通常和第一个方法搭配使用。

大厂面试题中断机制考点

如何停止中断运行中的线程?

通过volatile变量实现

volatile是常用的高并发场景下的关键字,先了解

public class InterruptDemo {
​
    static volatile boolean isStop = false;
​
    public static void main(String[] args) throws InterruptedException {
​
        new Thread(()->{
            while (true){
                if(isStop){
                    System.out.println(Thread.currentThread().getName() + "\t isStop 变成true");
                    break;
                }
​
                System.out.println("-----hello volatile");
            }
        },"t1").start();
​
        Thread.sleep(2000);
        new Thread(()->{
            isStop = true;
        },"t2").start();
    }
​
}

通过AtomicBoolean实现

主要代码还是不变,只是这个用来提醒线程是否停止的标识符从volatile变成了AtomicBoolean。

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (true){
                if(atomicBoolean.get()){
                    System.out.println(Thread.currentThread().getName() + "\t atomicBoolean 变成true");
                    break;
                }
​
                System.out.println("-----hello atomicBoolean");
            }
        },"t1").start();
​
        Thread.sleep(2000);
        new Thread(()->{
            atomicBoolean.set(true);
        },"t2").start();
    }

通过线程类自带的中断api实例方法实现

就是我们上面讲的三种

我们刚刚的两种方法volatile和atomicBoolean,其实就是在需要中断的线程中,不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑去stop线程。

public static void main(String[] args) throws InterruptedException {
​
    Thread t1 = new Thread(() -> {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName() + "\t is interrupted");
                break;
            }
            System.out.println("-----hello interrupt Api");
        }
    }, "t1");
​
    t1.start();
​
    Thread.sleep(2000);
    new Thread(()->{
        t1.interrupt();
    },"t2").start();
    
}

这相当于是t2对t1发起了中断请求,t1接受了中断请求,并且打印了t1 is interrupted。

也可以自己把自己中断。

例如:t1.interrupt();

线程中断API源码解读

interrupt方法

对于interrupt的方法,它的jdk底层源码是这样的。

我们发现,它调用了interrupt0()方法,而interrupt0方法是一个native方法,我们知道native方法是C++写的。

它的作用仅仅是设置一个标志位。

  1. 如果线程处于正常活动状态,那么interrupt方法会将该线程的中断标志设置为true,仅此而已。所以该方法不能真正的中断线程,需要和被调用的线程自己进行配合。
  2. 如果线程处于被阻塞的状态(比如sleep,wait,join),在别的线程调用当前现成的interrupt方法,那么线程将立即退出被阻塞的状态,并且抛出InterruptException异常

对于第二种情况,我们来进行一次代码的演示

对于while(true)循环里,每循环一次,让它睡眠一会儿,这样在睡眠的时候,被t2线程唤醒的概率就很高了!

​
    public static void main(String[] args) {
​
        Thread t1 = new Thread(() -> {
            while (true){
                System.out.println(Thread.currentThread().getName() + "正在运行中");
                if(Thread.currentThread().isInterrupted()){
                    System.out.println(Thread.currentThread().getName() +"\t 已经被中断了");
                    break;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printS
                }
            }
        }, "t1");
        t1.start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(()->{
            t1.interrupt();
        },"t2").start();
    }
​

其实呢,这个interruptException就是sleep这个方法抛出去的。并且程序没有停下来,说明在睡眠中,对线程使用interrupt方法会抛异常。

而这个程序不会停下来的原因就是,虽然t2线程将t1线程的中断标志位变成true了,但是由于此时抛出了异常,程序会将中断标志位清除,所以程序会一直这样打印。

这里是面试高频考点

对于这个现象,我们可以在catch模块中,再次调用这个interrupt方法来解决。

 try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }

Thread.interrupted()方法

这是Thread类的静态方法。它的作用我们之前提过,是将中断标志位清空,并且返回当前的中断状态。

我们进入代码实例看看。

public class InterruptDemo3 {
    public static void main(String[] args) {
        System.out.println("当前的线程状态\t" + Thread.interrupted());
        System.out.println("当前的线程状态\t" + Thread.interrupted());
        System.out.println("-------1");
        Thread.currentThread().interrupt();
        System.out.println("当前的线程状态\t" + Thread.interrupted());
        System.out.println("当前的线程状态\t" + Thread.interrupted());
    }
}

那有同学可能会好奇了,Thread.interrupt()和 实例方法的interrupt有啥区别呢?

实际上,静态的interrupted方法也只不过是多做了一步清空状态的操作。也就是clearInterruptEvent(),这也是个native方法

这些知识都是后续AQS的学习的基础!基础不牢地动山摇!

LockSupport

在学习LockSupport之前,我们一定是要进行Object的类的wait()和notify()的学习,以及可重入锁中的await()和signal()的学习

其实上述说的这两对方法,其实就是用的操作系统里面生产者消费者的这样的一个思想。

我们下面会通过代码实战,来演示这两对的代码实现,以及为什么我们需要用到LockSupport来替代他们

wait()和notify

@SuppressWarnings("ALL")
public class LockSupportDemo {
    public static void main(String[] args) {
        Object objectLock = new Object();
​
        new Thread(()->{
            synchronized (objectLock){
                System.out.println(Thread.currentThread().getName() + "开始运作,---即将进入等待状态");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName()+"我被唤醒啦");
            }
        },"t1").start();
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(()->{
            synchronized (objectLock){
                System.out.println(Thread.currentThread().getName() + "我的任务是唤醒线程");
                objectLock.notify();
            }
        },"t2").start();
    }
}

这是对于Object类的wait()和notify()方法。在t1线程里,我们对它做了等待操作,此时t1会放开对象锁,然后t2线程拿到对象锁,通知t1线程别等了,让他继续执行。

然而,这个对于操作系统核心思想的生产者消费者的实现是有优化空间的。因为这些操作必须要在同步代码块中进行,并且notify()方法一定得在wait()方法的后面,否则就会出现现成的阻塞。

await()和signal()

    public static void main(String[] args) {
​
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
​
        new Thread(()->{
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "开始运作,---即将进入等待状态");
                    condition.await();
                    System.out.println(Thread.currentThread().getName()+"我被唤醒啦");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
        },"t1").start();
​
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
​
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "我的任务是唤醒线程");
                condition.signal();
            }finally {
                lock.unlock();
            }
        },"t2").start();
​
    }

同样,它和上面的方法的问题一样。需要手动加锁,解锁。且signal方法必须要放在await()方法后面。

LockSupport 之 park 和 unpark

LockSupprt是通过park和unpark来实现线程的阻塞和唤醒

 public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t开始进入,即将被拦截");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "醒来了");
        }, "t1");
​
        t1.start();
​
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
​
        new Thread(()->{
            System.out.println("开始放行t1");
            LockSupport.unpark(t1);
        },"t2").start();
​
    }
​

这是正常逻辑下的代码,我们发现,无需将代码放入同步块中执行。

那如果,unpark()先执行,park后执行呢?

我们给t1线程先睡俩秒

 Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "\t开始进入,即将被拦截");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "醒来了");
        }, "t1");

我们发现,LockSupport相比于以上两种方法有巨大的优势:

  1. 无锁块要求
  2. 可以先唤醒后等待

有一点需要注意的是,在官方源码解读里说到,There is at most one。

这个意思是,许可证的数量最多只有一个,也就是说,我们在t2线程里面写无数个unpark也好,t2最多只能给t1发一个许可证。

也就是说,我们在进行代码实战的时候,两个线程内,不要出现unpark,park多对多的情况!!!

形象的理解: 线程的阻塞需要消耗凭证,但是这个凭证最多只有一个。

当调用park方法的时候,如果有凭证,那么就直接消耗这个凭证然后正常退出,如果无凭证,就必须阻塞等待凭证可用。

当调用unpark方法的时候,它只会增加一个凭证,但凭证最多一个,累加无效。

LockSupport面试题

为什么可以突破wait/notify的原有调用顺序?

在unpark先调用,获得一个凭证以后,park方法执行会消耗凭证,所以不会被阻塞。

为什么唤醒两次后阻塞两次,但最终结果还是会阻塞线程?

因为凭证数目最多为1,调用2此unpark方法和调用一次效果一样,只会增加一个凭证;而调用两次park需要两次凭证,证不够,不能放行。