线程共享和协作(三):如何实现线程间协作

线程的状态

万事万物都有其自己的生命周期和状态,一个线程从创建到结束被销毁也有其自己的六种状态,而wait、notify、sleep等等这些方法就是协助切换线程间的状态

Oracle官方文档提供的六种线程状态

状态名称 说明
NEW 初始状态,线程被创建,但是还没有调用start()方法,线程还未被启动
RUNNABLE 运行状态,一个线程开始在java虚拟机中被执行
BLOCKED 阻塞状态,线程被锁住等待获得对象的monitor lock,换言之就是被锁(Synchronize)阻塞了
WAITING 等待状态,无限期等待另一个线程执行特定操作的线程处于此状态。
TIMED_WAITING 超时等待状态,在指定的等待时间内等待另一个线程执行操作的线程处于此状态。
TERMINATED 终止状态,线程执行完毕已经退出

用一张图可以清晰的表示上述状态在线程中的运行状态切换

image

线程的状态切换的操作

建立线程后我们会根据需求对线程进行一些操作,这些操作会改变线程的基本状态,同事也成为了线程间的一种通信方式,下面就主要聊聊这些方法。

  • wait()、notify()和notifyAll()

    wait方法主要是将当前运行的线程挂起,让其进入阻塞状态,然后释放它持有的同步锁(也就是前面文章提到的monitor),通知其他线程来获取执行,直到notifynotifyAll方法来唤醒。

    wait也是一个多参数方法,可以通过wait(long timeout)来设定线程在指定时间内如果没有notifynotifyAll方法的唤醒,也会自动唤醒,wait方法调用的也是这个方法,不过传入的参数为0L。

    在使用wait方法时,一定要在同步范围内,否则就会抛出IllegalMonitorStateException异常。

public class SynchronizedDemo {
    public static void main(String[] args) {
        final SynchronizedDemo test = new SynchronizedDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.waitDemo();
            }
        }).start();
    }

     private void waitDemo() {
        System.out.println("Start Thread"+System.currentTimeMillis());
        try {
            wait(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End Thread"+System.currentTimeMillis());
    }
}
运行结果:
Start Thread1557818387416
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at com.example.javalib.SynchronizedDemo.waitDemo(SynchronizedDemo.java:24)
    at com.example.javalib.SynchronizedDemo.access$000(SynchronizedDemo.java:10)
    at com.example.javalib.SynchronizedDemo$1.run(SynchronizedDemo.java:16)
    at java.lang.Thread.run(Thread.java:745)

查看API文档对于IllegalMonitorStateException的定义

Thrown to indicate that a thread has attempted to wait on an object's monitor or to notify other threads waiting on an object's monitor without owning the specified monitor.

该错误的大意为:线程试图等待一个对象的监视器或者去通知其他在等待对象监视器的线程,但是该线程本身没有持有指定的监视器.主要是因为调用wait方法时没有获取到对象的monitor,获得的途径可以通过Synchronized关键字来完成,在上述代码的方法中添加Synchronized关键字

private synchronized void waitDemo() {
        System.out.println("Start Thread"+System.currentTimeMillis());
        try {
            wait(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End Thread"+System.currentTimeMillis());
    }

通过这个例子得知,wait方法的使用必须在同步的范围内,否则就会抛出IllegalMonitorStateException异常,wait方法的作用就是阻塞当前线程等待notify/notifyAll方法的唤醒,或等待超时后自动唤醒。

wait方法通过释放对象的monitor来挂起线程,进入WaitSet队列, 然后后续等待锁线程继续来执行,直到同一对象上调用notifynotifyAll后才可以唤醒等待线程。

notify 和 notifyAll的区别是notify方法只唤醒一个等待(对象的)线程并使该线程开始执行,如果有多个线程等待一个对象,那么只会随机唤醒其中一个线程,后者则会唤醒所有等待(对象的)线程,哪个线程第一个被唤醒也是取决于操作系统。

负责调用方法去唤醒线程的线程也被称为唤醒线程,唤醒线程后不能被立刻执行,因为唤醒线程还持有该对象的同步锁,必须等待唤醒线程执行完毕后释放了对象的同步锁后,等待线程才能获取到对象的同步锁进而继续执行。

从上述中可以看到wait,notify,notifyAll方法的调用去挂起唤醒线程主要是操作对象的monitor,而monitor是所有对象的对象头里都拥有的,所以这三个方法定义在Object类中,而不是Thread类中

下面一个用经典面试题:双线程打印奇偶数来展示wait和notify的用法(代码随便写的,理会意思就行)

public class Main {
    Object odd = new Object(); // 奇数条件锁
    Object even = new Object(); // 偶数条件锁
    private int max=200;
    private AtomicInteger status = new AtomicInteger(0); // AtomicInteger保证可见性,也可以用volatile

    public Main() {
    }

    public static void main(String[] args) {
        Main main = new Main();
        Thread printer1 = new Thread(main.new MyPrinter("线程1", 0));
        Thread printer2 = new Thread(main.new MyPrinter("线程2", 1));
        printer1.start();
        printer2.start();
    }
    public class MyPrinter2 implements Runnable {
        private String name;
        private int type; // 打印的类型,0:代表打印奇数,1:代表打印偶数

        public MyPrinter2(String name, int type) {
            this.name = name;
            this.type = type;
        }
        @Override
        public void run() {
            ThreadBean bean = new ThreadBean();
            bean.start(name);
        }
    }
    public class MyPrinter implements Runnable {
        private String name;
        private int type; // 打印的类型,0:代表打印奇数,1:代表打印偶数

        public MyPrinter(String name, int type) {
            this.name = name;
            this.type = type;
        }

        @Override
        public void run() {
            if (type == 0){
                while(status.get()<20){
                    if(status.get()%2==0){
                        synchronized (even){
                            try {
                                even.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }else{
                        synchronized (odd){
                            System.out.println("当前是"+name+"输出"+status.get());
                            status.set(status.get()+1);
                            odd.notify();
                        }
                    }
                }
            }else{
                while(status.get()<20){
                    if(status.get()%2==1){
                        synchronized (odd){
                            try {
                                odd.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }else{
                        synchronized (even){
                            System.out.println("当前是"+name+"输出"+status.get());
                            status.set(status.get()+1);
                            even.notify();
                        }
                    }
                }
            }
        }
    }
}

  • yield

yield是一个静态的原生native方法,他的作用是让出当前线程的CPU分配的时间片,将其分配给和当前线程同优先级的线程,然后当前线程状态由运行中(RUNNING)转换为可运行(RUNNABLE)状态,但这个并不是等待或者阻塞状态,也不会释放对象锁,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。

现在的操作系统中包含多个进程,一个进程又包含多个线程,那么这些多线程是一起执行的吗?就像电脑上,我们可以一边看电视一边浏览网页,其实并不然,看视两边同步进行的,但其实是cpu让两个线程交替执行,只不过交替执行的速度很快,肉眼分辨不出来,所以才会有同步执行的错觉。同理,这里也是一样,系统会分出一个个时间片,线程会被分配到属于自己执行的时间片,当前线程的时间片用完后会等待下次分配,线程分配的时间多少也觉得了线程使用多少处理器的资源,线程优先级也就是觉得线程是分配多一些还是少一些处理器的资源

Java中,通过一个整型变量Priority来控制线程的优先级,范围为1~10,通过调用setPriority(int Priority)可以设置,默认值为5。

yield一样,sleep也调用时也会交出当前线程的处理器资源,但是不同的是sleep交出的资源所有线程都可以去竞争,yield交出的时间片资源只有和当前线程同优先级的线程才可以获取到。

  • join

join方法的作用是父线程(一般是main主线程)等待子线程执行完成后再执行,换言之就是讲异步执行的线程合并为同步的主线程,。

wait一样,join方法也有多个参数的方法,也可以设定超时时间,join()方法调用的也是join(0L)

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开始"+"时间:"+System.currentTimeMillis());
        JoinDemo main = new JoinDemo();
        Thread printer1 = new Thread(main.new MyPrinter("线程1"));
        Thread printer2 = new Thread(main.new MyPrinter("线程2"));
        Thread printer3 = new Thread(main.new MyPrinter("线程3"));
        printer1.start();
        printer1.join();
        printer2.start();
        printer2.join();
        printer3.start();
        System.out.println("主线程结束"+"时间:"+System.currentTimeMillis());
    }

    public class MyPrinter implements Runnable {
        String content;

        public MyPrinter(String content) {
            this.content = content;
        }

        @Override
        public void run() {
            System.out.println("当前线程"+content+"时间:"+System.currentTimeMillis());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
输出结果:
主线程开始时间:1557824674063
当前线程线程1时间:1557824674063
当前线程线程2时间:1557824675065
主线程结束时间:1557824676065
当前线程线程3时间:1557824676065

从上面例子可以看到线程1和2调用了join方法后,主线程是等待两个线程执行完成之后才会继续执行

  • interrupt

interrupt的目的是为了中断线程,原来Thread.stop, Thread.suspend, Thread.resume 都有这个功能,但由于都太暴力了而被废弃了,暴力中断线程是一种不安全的操作,相对而言interrupt通过设置标志位的方式就比较温柔

interrupt基于一个线程不应该由其他线程来强制中断或停止,而是应该由线程内部来自行停止的思想来实现的,自己的事自己处理,是一种比较温柔和安全的做法,而且中断不活动的线程不会产生任何影响。

从API文档的中的介绍来看interrupt()的作用是中断本线程。除非当前线程正在中断自身(始终允许),否则将调用此线程的checkAccess方法,但这可能导致抛出SecurityException

如果在调用Object类的wait()join()sleep(long)阻塞了这个线程,那么它的中断状态将被清除并收到InterruptedException

如果在InterruptibleChannel上的I / O操作中阻塞了该线程,则该通道将被关闭,线程的中断状态将被设置,并且线程将收到ClosedByInterruptException

  • 终止阻塞线程

例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。此时将InterruptedException放在适当的位置进行捕获就能终止阻塞中的线程,如下代码,将中断的捕获放在while(true)之外,就可以退出while循环

@Override
public void run() {
    try {
        while (true) {
            // 执行任务...
        }
    } catch (InterruptedException ie) {  
        // 由于产生InterruptedException异常,退出while(true)循环,线程终止!
    }
}

但是如果需要将··InterruptedException··在··while(true)``循环体之内的话,就需要额外的添加退出处理,通过捕获异常后的break退出当前循环。

@Override
public void run() {
    while (true) {
        try {
            // 执行任务...
        } catch (InterruptedException ie) {  
            // InterruptedException在while(true)循环体内。
            // 当线程产生了InterruptedException异常时,while(true)仍能继续运行!需要手动退出
            break;
        }
    }
}

  • 终止运行线程

通常,我们通过“标记”方式终止处于“运行状态”的线程。其中,包括“中断标记”和“额外添加标记”。通过设立一个标志来在线程运行的时候判断是否执行下去。

@Override
public void run() {
    while (!isInterrupted()) {
    }
}

isInterruptedThread的内部方法,可以获取当前线程是否中断的标志,当线程处于运行状态时,我们通过interrupt()修改线程的中断标志,来达到退出while循环的作用。

上述是系统内部的标志符号,我们也可以自己设置一个标志符来达到退出线程的作用

private volatile boolean isExit= false;
protected void exitThread() {
    isExit= true;
}

@Override
public void run() {
    while (isExit) {
    }
}

通过自己设置标志符,在需要的时候直接调用exitThread就可以修改while的判断条件,从而达到退出线程的目的。

综合阻塞和运行状态下线程的终止方式,结合两者可以使用一个通用较为安全的方法

@Override
public void run() {
    try {
        // 1\. isInterrupted()保证,只要中断标记为true就终止线程。
        while (!isInterrupted()) {
        }
    } catch (InterruptedException ie) {  
        // 2\. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
    }
}

最后谈谈 interrupted()isInterrupted()
interrupted()isInterrupted()都能够用于检测对象的“中断标记”。
区别是,interrupted()除了返回中断标记之外,它还会清除中断标记(即将中断标记设为false);而isInterrupted()仅仅返回中断标记。

  • Sleep

    最后简单说一下sleep,这算是多线程我们最常用的方法了

    sleep是Thread的静态native方法,它的作用是让当前线程按照指定的时间休眠,休眠时期线程不会释放锁,但是会让出执行当前线程的cpu资源给其他线程使用,和wait较为类似,但是也有一些不同点。

    • sleep()是Thread的静态内部方法,wait()是object类的方法
    • wait()方法必须在同步代码块中使用,必须获得对象锁(monitor),sleep()方法则可以再仍和地方中使用,wait()方法会释放当前占有的对象锁,本身进入waitset队列,等待被唤醒,sleep()方法只会让出cpu资源,并不会释放锁
    • sleep()方法在休眠时间结束后获得CPU分配的资源后就可以继续执行,wait()方法需要被notify()唤醒后还需要等待唤醒线程执行完毕释放锁后,才会获得CPU资源继续执行

你可能感兴趣的:(线程共享和协作(三):如何实现线程间协作)