多线程笔记---线程间的协做方法(wait、notify、sleep、yield、join、interrupt、notifyAll)

线程的状态

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

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

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

用一张图能够清晰的表示上述状态在线程中的运行状态切换 面试

《JAVA并发编程的艺术》一书中的线程状态转换图

线程的状态切换的操做

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

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

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

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

    在使用wait方法时,必定要在同步范围内,不然就会抛出IllegalMonitorStateException异常。bash

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方法的唤醒,或等待超时后自动唤醒。oracle

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资源继续执行

线程的状态转换以及基本操做
Java 并发编程:线程间的协做
Java多线程系列--“基础篇”09之 interrupt()和线程终止方式

相关文章
相关标签/搜索