并发编程<一>线程5大状态切换时机分析及sleep,join,wait,notify,notifyAll,yield剖析

线程5大状态分析

并发编程<一>线程5大状态切换时机分析及sleep,join,wait,notify,notifyAll,yield剖析

上图是线程从创建到消亡的一个切换过程。下面我们简单类分析每一个状态。

  1. 新建状态:新建状态具体是指调用new Thread()创建出线程对象,但是还没有调用start方法的这段时间。前面的一篇文章《Java虚拟机剖析之内存区域,内存的溢出,泄漏》一文中有说到,每一个线程都会有自己的私有内存区域。处于新建状态下的线程,此时还未分配系统资源,也即是还没有分配到私有内存。
  2. 就绪状态:start方法刚被调用的一段时机。处于当前状态的线程已经分配到所需资源,但是还没有获得CPU使用权,在此状态的线程会相互竞争CPU使用权。
  3. 运行状态:被os选中,获得CPU使用权,开始执行任务,也即是开始运行run/main方法。
  4. 阻塞状态:在执行任务的过程中由于一些原因导致线程阻塞(后面会重点讲阻塞状态,这也是本文重点)。
  5. 终止状态:任务执行完毕(run/main方法执行完毕)或者线程异常终止。

阻塞状态,阻塞原因

阻塞状态对我们开发人员来说是最关键的一个状态,因为我们能通过各种造成阻塞的手段合理的调度指定的线程执行特定的任务,能准确的控制每一个任务执行的时机。造成阻塞的原因大致为下面4种:

  1. 调用sleep/join方法
  2. 调用wait方法
  3. 访问临界资源时(如synchronized字段修饰的方法或者代码块),等待竞争锁对象所有权
  4. I/O导致阻塞(比如:等待用户输入)
其中这四种方式中1,2两种阻塞可以中断,3,4两种不会对唤醒线程的操作有反应。

关键方法分析sleep,join,wait,notify,notifyAll,yield

  1. sleep方法是Thread的静态方法,当该方法调用时,会让调用的线程进入阻塞状态,直到历时sleep的参数时间后唤醒该线程。当线程在持有临界资源对象锁持有权时调用sleep方法,线程进入阻塞,不会释放所持有的对象锁持有权,容易造成死锁。
  2. join方法是Thread的公有方法,归对象所有。在A线程中调用B线程的join方,A线程进入阻塞状态,知道B线程的任务执行完毕才会唤醒A线程继续执行未完成的任务。
  3. wait方法是超类Object的方法,该方跟notify/notifyAll配套使用,这一对方法用于线程间通讯控制并发。这一对方法需要线程在持有临界资源对象锁所有权的情况下才能调用,否则抛出IllegalMonitorStateException。调用wait方法的线程交出CPU使用权进入阻塞状态,需等到竞争同一锁对相的线程调用notify/notifyAll方法才会被唤醒(notify是在所有相关的处于等待状态的线程中随机选择一个线程唤醒,notifyAll是将所有相关的处于等待状态的线程全部唤醒),然后进入锁池,重新竞争对象锁的持有权。wait方法调用的线程会释放临界资源对象锁的持有权。
  4. yield方法是Thread的静态方法,调用该方法的线程相当于线程的时间片用完,回到就绪状态,重新竞争跟同等优先级线程CPU使用权(直观的说就是A,B线程为同等优先级线程,同时开始竞争CPU使用权。A线程获取到CPU使用权进入运行状态,正在执行任务,run方法运行到一半的时候调用了yield方法,此时A线程将会跟B线程再一次平等继续竞争CPU使用权,如果A得到使用权,会继续刚才完成到一半的任务继续未完成的任务执行完)
  5. 另外suspend和resume方法配对使用,跟wait和notify/notifyAll这一对差不多,调用suspend的线程进入阻塞,需要等到对应的resume方法被调用才会唤醒。但是有一个区别是suspend方法调用的线程会释放对象锁的持有权。这对方法不经常用,所以略过。。。

sleep和wait方法的区别及特性验证

共同点:

  1. 都能是线程进入阻塞状态
  2. 都可以设置阻塞时间
不同点:

  1. 唤醒方式不同,sleep方法是在指定的时间之后自动唤醒。wait必须等到别的相关线程调用notify/notifyAll才会唤醒(当然了,wait(long time)方法也能指定等待时间,等待时间到了之后还未调用notify/notifyAll将会自动唤醒,特殊情况)
  2. sleep调用时不一定需要线程持有临界资源的对象锁,但是wait方法的调用线程必须持有临界资源的对象锁,否则会抛出异常。
  3. 调用sleep方法进入阻塞状态后不会释放持有的对象锁,但是wait方法会释放所持有的对象锁(主要区别)
这些区别导致了控制线程的方式完全不同,使用的场景也不相同,我们用实例能直观的验证sleep和wait的特性以及区别。下面用生产者/消费者模式进行说明验证,其实真正的生产者/消费者模式需要用的是wait方法,用sleep方法有可能造成死锁,这儿只是为了证明两种方法的区别!

工厂:

package com.example;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class FactoryClass {
    //用于存放产品的容器
    public List<String> products = new ArrayList<>();

    /**
     * 生产量是否已经达到饱满
     *
     * @return true 表示已经饱满
     */
    private boolean isFull() {
        return products.size() >= 40 ? true : false;
    }

    /**
     * 库存是否已经为0
     *
     * @return true 表示已售完
     */
    private boolean isEmpty() {
        return products.size() <= 0 ? true : false;
    }

    /**
     * 商店卖东西,调用wait进入阻塞
     *
     * @param consumer 消费者的名字
     */
    public void sell1(String consumer) {
        synchronized (products) {
            while (isEmpty()) {
                System.out.print("The goods is sold out ," + consumer + " need to wait a while !\n");
                try {
                    products.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(consumer + " has been wait for 2 seconds,try to get goods !\n");
            }
            System.out.print(consumer + " bought the goods !\n");
            products.remove(products.size() - 1);
            products.notifyAll();
        }
    }

    /**
     * 商店卖东西,调用sleep进入阻塞
     *
     * @param consumer 消费者的名字
     */
    public void sell2(String consumer) {
        synchronized (products) {
            while (isEmpty()) {
                System.out.print("The goods is sold out ," + consumer + " need to wait for a while !\n");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(consumer + " has been waiting for 2 seconds , try to get goods ! \n");
            }
            products.remove(products.size() - 1);
            System.out.print(consumer + " bought the goods \n");
        }
    }

    /**
     * 工人生产,调用wait进入阻塞
     *
     * @param employee 工人的名字
     */
    public void produce1(String employee) {
        synchronized (products) {
            while (isFull()) {
                System.out.print("the warehouse is full ," + employee + " can have a rest for 2 seconds !\n");
                try {
                    products.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(employee + " has been had a rest for 2 seconds, request to start working !\n");
            }

            products.add("product");
            System.out.print(employee + " has been produced a goods !\n");
            products.notifyAll();
        }
    }

    /**
     * 工人生产,调用sleep 进入阻塞
     *
     * @param employee 工人的名字
     */
    public void produce2(String employee) {
        synchronized (products) {
            while (isFull()) {
                System.out.print("the warehouse is full ," + employee + " can have a rest for 2 seconds !\n");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(employee + " has been had a rest for 2 seconds, request to start working !\n");
            }
            products.add("product");
            System.out.print(employee + " has been produced a goods !\n");
        }
    }
}
员工任务类(线程):

package com.example;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class ProduceRun implements Runnable {
    private FactoryClass factory;

    public ProduceRun(FactoryClass factory) {
        this.factory = factory;
    }

    @Override
    public void run() {
        Employee employee = null;
        for (int i = 0; i < 60; i++) {
            employee = new Employee(factory);
            employee.setName("employee-" + i);
            employee.work();
        }
    }
}
消费者任务类(线程):

package com.example;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class ConsumeRun implements Runnable {

    private FactoryClass factory;

    public ConsumeRun(FactoryClass factory) {
        this.factory = factory;
    }

    @Override
    public void run() {
        Consumer consumer = null;
        for (int i = 0; i < 60; i++) {
            consumer = new Consumer(factory);
            consumer.setName("consumer->" + i);
            consumer.shopping();
        }
    }
}

主程序入口:

package com.example;

public class MyClass {

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

        FactoryClass factory = new FactoryClass();
        ProduceRun produceRun = new ProduceRun(factory);
        ConsumeRun consumeRun = new ConsumeRun(factory);
        new Thread(consumeRun).start();
        new Thread(produceRun).start();
    }
}
员工:

package com.example;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class Employee {

    //工人工作的工厂
    private FactoryClass factory;
    //工人姓名
    private String name;

    public Employee(FactoryClass factory) {
        this.factory = factory;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 干活,调试wait结果调用produce1方法,调试sleep,调用produce2
     */
    public void work() {
        factory.produce1(name);
        //  factory.produce2(name);
    }
}

消费者:

package com.example;

/**
 * Created by PICO-USER on 2017/11/7.
 */

public class Consumer {
    //消费的商店
    FactoryClass factory;
    //消费者的姓名
    String name;

    public Consumer(FactoryClass factory) {
        this.factory = factory;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 购物,运行wait结果调用shell1方法,sleep调用sell2方法。
     */
    public void shopping() {
        factory.sell1(name);
        // factory.sell2(name);
    }
}

wait方法运行结果:

并发编程<一>线程5大状态切换时机分析及sleep,join,wait,notify,notifyAll,yield剖析

sleep方法运行结果:

并发编程<一>线程5大状态切换时机分析及sleep,join,wait,notify,notifyAll,yield剖析

对于第一,第二不同点,就不具体验证了,有兴趣可以自己写两个小Demo便可以验证。这儿具体验证第三点。分析上面的两个结果,很清楚的看出一个不同之处,对于wait方法的运行结果来说,当商品卖完的时候消费者进入等待状态,员工能及时生产出商品给消费者,然而对于sleep方法的运行结果来说,当商品卖完的时候,消费者一直在处于等待->尝试得到商品->等待->尝试得到商品.....这样的一个死循环,并且员工并不能去生产新的商品给消费者。

为什么会出现这种情况呢?其实很简单,首先wait方法导致消费者线程进入阻塞状态的时候,释放了CPU以及对象锁,此时两个线程共同竞争CPU使用权和对象锁的持有权,当员工线程得到CPU的使用权之后执行任务,再得到对象锁之后,开始生产商品,商品完成之后,唤醒所有阻塞的线程。然后再一次释放CPU和对象锁。然后他们两个再一次平等竞争CPU和对象锁,一直这样循环直到两个线程任务都完成。其次sleep方法导致消费者线程进入阻塞的时候,它只是释放了CPU,并没有释放对象锁,此时两个线程只能平等竞争CPU,而员工线程是不能得到对象锁的。员工线程得到CPU使用权之后开始运行任务,但是因为对象锁被消费者线程占有,并没有释放,员工线程得不到对象锁,所以无法访问临界资源(synchronized代码块),所以会进入上面提到的第三种阻塞(也即是竞争对象锁造成阻塞)。这两个线程一直循环着这个过程,导致员工一直无法干活,消费者一直在处于等待->尝试得到商品->等待->尝试得到商品.....这样的一个死循环。

重点:对上面的区别验证,其实只需要弄清楚下面这两点就不难理解!

  1. 线程竞争到CPU使用仅仅只能开始执行任务(执行run方法),并不一定能完成任务。为什么这么说呢?因为上面对阻塞状态,阻塞原因小节中提到阻塞原因的第3条是竞争临界资源对象锁造成阻塞,所以有可能线程开始执行任务了,但是在等待对象锁导致阻塞,从而无法完成任务。上面的sleep方法死锁了就是这个原因。
  2. 线程竞争到CPU的使用权之后需要得到临界资源的访问权,也即是拿到对象锁的使用权,才能访问临界资源,才能完成任务(把run方法走完)。

join方法特性验证

上面已经说过了join方法的特性:该API能让线程进入阻塞,并且会等到另一个线程执行完之后才会唤醒该线程继续未完成的任务。下面看代码和运行结果验证。

程序入口:

package com.example;

public class MyClass {

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

        JoinTh2 joinTh2 = new JoinTh2();
        joinTh2.setName("Thread_A");
        JoinTh1 joinTh1 = new JoinTh1(joinTh2);
        joinTh1.setName("Thread_B");
        joinTh1.start();
    }
}
线程A:

package com.example;

/**
 * Created by PICO-USER on 2017/11/8.
 */

public class JoinTh1 extends Thread {
    Thread thread;

    public JoinTh1(Thread thread) {
        this.thread = thread;
    }

    @Override
    public void run() {
        super.run();
        String name = getName();
        for (int i = 0; i < 5; i++) {
            System.out.print(name + " is performing tasks ! i = " + i + "\n");
            if (i == 2 && thread != null) {
                try {
                    System.out.print(name + " will start another thread " + thread.getName()
                            + ", the time is :" + MyUtils.getCurrentTime() + " !\n");
                    thread.start();
                    thread.join();
                    System.out.print(name + " continue to perform outstanding tasks , the time is :" + MyUtils.getCurrentTime() + "!\n");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

线程B:

package com.example;

/**
 * Created by PICO-USER on 2017/11/8.
 */

public class JoinTh2 extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.print(getName() + " start to perform the tasks !\n");
        try {
            //休眠3秒,模拟一个耗时操作,以便看出来joinTh1会不会等到本线程完成任务再继续执行未完成任务!
            sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.print(getName() + " to complete all tasks, the time is :" + MyUtils.getCurrentTime() + "!\n");
    }
}

调用join方法时的结果:

并发编程<一>线程5大状态切换时机分析及sleep,join,wait,notify,notifyAll,yield剖析

不调用join方法时结果:

并发编程<一>线程5大状态切换时机分析及sleep,join,wait,notify,notifyAll,yield剖析

结果对比验证分析:第一次调用在A中调用了B的join方法,第二次将线程A中“thread.join();”这行代码注释掉,也即是说不调用join方法。此案例我故意在B中做了一个休眠3秒的操作,目的就是达到一个耗时的操作让“A线程等待B线程执行完成之后再开始恢复任务执行”的效果更明显,更突出。从时间上看,调用join方法后,A线程并没有第一时间开始执行i=3的任务,而是停了3秒钟才开始执行。但是不调用的时候,几乎是同一时间执行了i=2和i=3的操作。验证成功!