java并发线程原理深入理解(三)-线程中断机制和线程间通信

101 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

一、java线程的生命周期

Java 语言中线程共有六种状态,分别是:

  1. NEW(初始化状态)
  2. RUNNABLE(可运行状态+运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。

image.png

从JavaThread的角度,JVM定义了一些针对Java Thread对象的状态(jvm.h)

image.png

从OSThread的角度,JVM还定义了一些线程状态给外部使用,比如用jstack输出的线程堆栈信息中线程的状态(osThread.hpp)

image.png

二、Thread常用方法

2-1、sleep方法

  • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入参数为0时,和yield相同

2-2、yieId方法

  • yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
  • 具体的实现依赖于操作系统的任务调度器

2-3、join方法

等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。

2-3-1、不使用join方法

下面来演示一下,首先不适用join的时候

public class ThreadJoin {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t begin");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t finished");
            }
        });
        long start=System.currentTimeMillis();
        t.start();

        System.out.println("执行时间:" + (System.currentTimeMillis() - start));
        System.out.println("Main finished");
    }
}

执行结果,可以看到主线程先执行完毕,之后才是创建的子线程执行,并且记录的执行时间也为0.

image.png

2-3-2、使用join方法

可以看到,当调用子线程的join方法的时候,主线程就会等待,当子线程执行完毕之后,主线程才继续执行。 image.png

2-4、stop方法

stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。

2-4-1、不使用stop的情况下

public class ThreadStop {
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    try {
                        Thread.sleep(6000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }
        });
        thread.start();
        Thread.sleep(2000);
        // 停止thread,并释放锁
//        thread.stop();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "等待获取锁");
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                }
            }
        }).start();

    }
}

执行结果如下,可以看到thread0先启动,并获得锁,接着让thread0休眠,然后thread1去等待获取锁,当thread0休眠结束后释放锁,thread1马上获得锁,thread1释放锁之后,thread0中的run方法最终打印才输出。

image.png

2-4-2、使用stop

public class ThreadStop {
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    try {
                        Thread.sleep(6000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }
        });
        thread.start();
        Thread.sleep(2000);
        // 停止thread,并释放锁
        thread.stop();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "等待获取锁");
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                }
            }
        }).start();

    }
}

执行结果如下:可以看到当thread0.start之后,获取了锁,然后进入等待,由于thread0.stop导致thread0被终止,这样thread0等待的6s就无效了,随机thread1获得锁。

由此可以看出使用thread.stop中断线程是不安全的,强行把执行一半的线程终止。并且会是否对象锁,可能会造成数据不一致

image.png

某些场景虽然我们想让线程停止,但是还是让其未完成的事情在完成之后再停止,下面来看一下,如何处理

三、java线程的中断机制

ava没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。

3-1、API的使用

  • interrupt(): 将线程的中断标志位设置为true,不会停止线程
  • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle

3-1-1、使用API中断线程

3-1-1-1、设置标志位及获取标志位

如下代码首先启动线程t1.start(),接着设置线程中断标志位为truet1.interrupt(),这样线程执行中,就可以通过Thread.currentThread().isInterrupted()来获取是否设置了标志位

通过以上结论,我们试想,如果让线程中断停止执行,就可以通过设置和获取线程的标志位来进行处理了。

public class ThreadInterrupt {
    static int i = 0;

    public static void main(String[] args) {
        System.out.println("begin");
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    i++;
                    System.out.println("in while");
                    if (Thread.currentThread().isInterrupted() ) {
                        System.out.println("设置中断标志位=========");
                    }
                    System.out.println("已运行:"+i);
                    if(i>10){
                        break;
                    }
                }
            }
        });
        t1.start();
        t1.interrupt();
    }
}

以上代码打印结果如下:

begin
in while
设置中断标志位=========
已运行:1
in while
设置中断标志位=========
已运行:2
in while
设置中断标志位=========
已运行:3
in while
设置中断标志位=========
已运行:4
in while
设置中断标志位=========
已运行:5
in while
设置中断标志位=========
已运行:6
in while
设置中断标志位=========
已运行:7
in while
设置中断标志位=========
已运行:8
in while
设置中断标志位=========
已运行:9
in while
设置中断标志位=========
已运行:10
in while
设置中断标志位=========
已运行:11

3-1-1-2、清除线程标志位

i==5时通过Thread.interrupted()来进行清除标志位,这样6以后因为没有线程标志位Thread.currentThread().isInterrupted()就为false

public class ThreadInterrupt {
    static int i = 0;

    public static void main(String[] args) {
        System.out.println("begin  ");
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    i++;
                    System.out.println("in while  ");
                    //判断线程是否设置了线程标志位
                    if (Thread.currentThread().isInterrupted() ) {
                        System.out.println("设置中断标志位=========  ");
                    }
                    if(i==5){
                        //清除线程标志位
                        Thread.interrupted();
                    }
                    System.out.println("已运行:"+i+"  ");
                    if(i>10){
                        break;
                    }
                }
            }
        });
        t1.start();
        //设置线程标志位
        t1.interrupt();
    }
}

以上代码运行结果如下:

begin
in while
设置中断标志位=========
已运行:1
in while
设置中断标志位=========
已运行:2
in while
设置中断标志位=========
已运行:3
in while
设置中断标志位=========
已运行:4
in while
设置中断标志位=========
已运行:5
in while
已运行:6
in while
已运行:7
in while
已运行:8
in while
已运行:9
in while
已运行:10
in while
已运行:11

3-1-2、使用线程标志位中断线程

如下、在main方法中,先让线程启动,这时候线程中run()方法就会运行,并执行while,因为此时未设置线程标志位,因此!Thread.currentThread().isInterrupted()即为truewhile就会一直执行,当线程sleep(5)之后,设置了线程标志位Thread.interrput(),这样!Thread.currentThread().isInterrupted()即为false,跳出while,线程终止。

public class UseInterruptStopThread implements Runnable{
    @Override
    public void run() {
        int count=0;
        //当设置线程标志位后、停止while
        while (!Thread.currentThread().isInterrupted()){
            System.out.println("count=="+count++);
        }
        System.out.println("线程停止");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new UseInterruptStopThread());
        t1.start();
        t1.sleep(5);
        t1.interrupt();
    }
}

注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况,如果被清除了,那么线程就无法安装既定的规则运行了。

3-1-3、sleep期间能否感受到中断

修改以上代码,启动线程之后,让线程进入休眠10秒,这样while就会一直运行,但是由于添加了Thread.sleep(1000),这样就会1秒运行一次。

public class UseInterruptStopThread implements Runnable{
    @Override
    public void run() {
        int count=0;
        //当设置线程标志位后、停止while
        while (!Thread.currentThread().isInterrupted()){
            System.out.println("count=="+count++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("线程停止");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new UseInterruptStopThread());
        t1.start();
        t1.sleep(10000);
        t1.interrupt();
    }
}

执行结果如下,可以看出,当睡眠10秒结束,并设置线程标志位,让线程中的while停止运行,但是由于在run中使用了sleep。导致标志位被清除,while将一直以每秒运行一次的进行下去。

image.png

3-1-4、小结

通过以上我们可以知道可以通过设置线程标志位来实现中断线程任务的运行,同时如果使用Thread.sleep,那么处于休眠中的线程就会被中断,线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样就会导致while条件Thread.currentThread().isInterrupted()为false。如果不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。

如下在catch中重新设置标志位,线程任务就可以停止了

image.png

sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位

wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位

3-1-5、sleep和wait区别

3-1-5-1、使用sleep

首先在test中使用sleep进行等待

public class WaitAndSleep {
    private Object lock=new Object();

    public void test(){
        System.out.println(Thread.currentThread().getName()+" start");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName()+" excute");
            try {
//                lock.wait(2000);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" end");
        }
    }

    public static void main(String[] args) {
        WaitAndSleep ws=new WaitAndSleep();
        for (int i=0;i<2;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    ws.test();
                }
            },"thread"+i).start();
        }
    }
}

执行结果如下,thread0获得了锁,执行excute和end,释放锁之后thread1再次获得锁,执行excute和end.

image.png

3-1-5-2、使用wait

依旧使用以上代码,只是将test方法中的sleep改为wait,执行结果如下:

image.png

通过以上执行结果可以看到,thread0和thread1同时获得了执行操作,这样就意味着synchronized加锁被wait取消了。

因此在使用synchronized加锁的时候,可以使用sleep让线程等待,不能使用wait,会破坏锁

3-1-5-3、一个共同点,三个不同点

共同点

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

  • 方法归属不同
    • sleep(long) 是 Thread 的静态方法
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  • 醒来时机不同
    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
    • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
    • 它们都可以被打断唤醒(调用interrupt()方法)
  • 锁特性不同(重点)
    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

wait 通常被用于线程间交互,sleep 通常被用于暂停执行,区别如下:

1. 来自不同的类

wait():来自Object类;
sleep():来自Thread类;

2.关于锁的释放:

wait():在等待的过程中会释放锁;
sleep():在等待的过程中不会释放锁

3.使用的范围:

wait():必须在同步代码块中使用;
sleep():可以在任何地方使用;

4.是否需要捕获异常

wait():不需要捕获异常;
sleep():需要捕获异常;

四、Java线程间通信

4-1、volatile

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。

public class VolatileDemo {

    private static boolean flag = true;

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                int count=0;
                while (true){
                    if (flag){
                        System.out.println(Thread.currentThread().getName()+"count:"+count++);
                        System.out.println("trun on");
                        flag = false;
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                int count=0;
                while (true){
                    if (!flag){
                        System.out.println(Thread.currentThread().getName()+"count:"+count++);
                        System.out.println("trun off");
                        flag = true;
                    }
                }
            }
        }).start();
    }
}

以上代码通过两个线程、互相切换变量来进行输出,这样原则上来说就会一直执行下去,下面看下执行结果,可以发现已经没有turn on和turn off的输出,这是因为在两个线程竞争CPU执行时,不是有序的,这样就无法保证线程的有序执行

image.png

将以上代码private static boolean flag = true;改为 private static volatile boolean flag = true;执行结果如下:

image.png

4-2、等待唤醒机制

4-2-1、使用wait、notify实现等待唤醒

等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。

public class WaitNotify {
    private static Object lock = new Object();
    private static  boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    while (flag){
                        try {
                            System.out.println("wait start .......");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    System.out.println("wait end ....... ");
                }
            }
        }).start();
        Long start=System.currentTimeMillis();
        Thread.sleep(3000);
        System.out.println(System.currentTimeMillis()-start);
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (flag){
                    synchronized (lock){
                        if (flag){
                            lock.notify();
                            System.out.println("notify .......");
                            flag = false;
                        }

                    }
                }
            }
        }).start();
    }
}

以上代码执行结果如下,在线程1开始执行并获得锁之后,进行了lock.wait,然后等待3秒之后,线程2开始执行,当线程2获得锁之后,进行lock.notify这样就唤醒了线程1lock.wait的线程,进而线程1可以接着继续执行

image.png

线程唤醒使用wait和notify必须在synchronize代码块中,这样就会导致如果有多个线程等待,不知道唤醒哪个,如果唤醒全部就用notifyAll

使用wait和notify虽然可以实现线程的睡眠和唤醒,但是也是有缺点的。因此一般情况下使用LockSupport来进行线程的睡眠和唤醒。

4-2-2、使用LockSupport实现等待唤醒

LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。

因此LockSupport实现线程等待唤醒用的是parkunpark,这样线程等待唤醒就是两种实现方式:
1、wait->notify(Monitor机制,,需要在synchronized代码块中实现)
2、park->unpark

4-2-2-1、实现代码

首先在ParkThread中的run方法,使用LockSupport.park(),来让线程休眠,然后再main方法中,调用LockSupport.unpark(t1)来唤醒休眠的线程,可以看到在unpark方法中传入了执行的线程,这样就可以指定要唤醒的线程。

public class LockSupportDemo {

    static class ParkThread implements Runnable{
        @Override
        public void run() {
            System.out.println("parkThread 开始执行");
            LockSupport.park();
            System.out.println("parkThread 执行完成");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new ParkThread());
        t1.start();
        Thread.sleep(2000);
        System.out.println("唤醒parkThread");
        LockSupport.unpark(t1);
    }
}

执行结果如下:

image.png

在以上代码main方法在,让线程等待了2秒,那不等待2秒是什么样的结果呢,看下执行结果,首先执行了main方法主线程的unpark,然后才执行需要被唤醒的线程,而需要被唤醒的线程虽然调用了park,但是依然没有休眠,而是直接通过。

这样得出结论:使用park和unpark唤醒机制,可以unpark在前执行,park在后执行;而使用wait和notify,则必须先调用wait再调用notify才能成功唤醒

image.png

4-2-3、管道输入输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。

public class PipedTest {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);

        Thread printThread = new Thread(new Print(in), "PrintThread");

        printThread.start();
        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }

    static class Print implements Runnable {
        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }
}

执行结果如下:

image.png

这样通过输入输出流进行绑定,就实现了一个通知唤醒机制。