java 线程并发中的等待与唤醒

2016-08-03 18:09:36   最后更新: 2016-08-03 18:09:36   访问数量:1543




java 线程状态通常包括以下几种:

  1. 创建(new)
  2. 就绪(runnable)
  3. 运行(running)
  4. 阻塞(blocked)
  5. time waiting
  6. waiting
  7. 消亡(dead)

当需要新起一个线程来执行某个子任务时,就创建了一个线程

但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态

当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待

当得到CPU执行时间之后,线程便真正进入运行状态

线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)

当由于突然中断或者子任务执行完毕,线程就会被消亡

下图展示了线程的状态及状态的变化流转:

 

 

通常线程会一直处于 running 状态直到运行结束、销毁,本篇日志中,我们就来看看如何让线程进入 waiting 状态和 time waiting 状态

 

我们曾经介绍过 Thread 类在创建多线程应用中的用法:

java 创建多线程的三种方式 -- Runnable、Thread、Callable

Thread 类提供了一个静态的 sleep 方法用来让线程进入 time waiting 状态,睡眠一段时间以后自动唤醒

需要注意的是,调用 sleep 方法并不会使线程释放锁

 

package com.techlog.test.service; import org.springframework.stereotype.Service; /** * just for test * Created by techlog on 16/5/20. */ @Service public class ChannelThreadLocal { public static void main(String[] argv) { new Thread(new Thread1()).start(); try { Thread.sleep(3000); } catch (InterruptedException e) { System.out.println("Sleep interrupted"); } new Thread(new Thread2()).start(); } private static class Thread1 implements Runnable { @Override public void run() { System.out.println("Thread1 run"); } } private static class Thread2 implements Runnable { @Override public void run() { System.out.println("Thread2 run"); } } }

 

 

可以看到,在输出 Thread1 run 后,等待了 3 秒后,才输出了 Thread2 run

 

在实际开发中,Object 类的三个用来实现线程同步的方法常常被用到,尤其在解决生产者-消费者问题的时候:wait、notify、notifyAll

package com.techlog.test.service; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * just for test * Created by techlog on 16/5/20. */ @Service public class PrintSortedThreadName { private static List<Long> threadIds; private static int index; private static boolean start; public static void main(String[] argv) { threadIds = new ArrayList<Long>(); final Object lock = new Object(); int concurrentcount = 10; start = false; index = 0; for (int i = 0; i < concurrentcount; i++) { new Thread(new Process(lock)).start(); } synchronized (lock) { Collections.sort(threadIds); start = true; lock.notifyAll(); } } private static class Process implements Runnable { private final Object lock; public Process(Object lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { threadIds.add(Thread.currentThread().getId()); while (threadIds.get(index) != Thread.currentThread().getId() || !start) { try { lock.wait(); } catch (InterruptedException e) { System.out.println("wait interrupted"); } } System.out.println(Thread.currentThread().getId() + " : " + Thread.currentThread().getName()); index++; lock.notifyAll(); } } } }

 

 

上面的代码实现了一个简单的线程同步功能:按照线程 ID 从小到大的顺序输出线程名称

我们通过 main 方法创建了 concurrentcount 指定个数的线程

每一个线程都向 threadIds 写入了自己的 ThreadId,然后,调用 Object 的 wait 方法陷入等待(因为类共享的 start 变量没有被开启)

main 方法在此后将 threadIds 进行了排序,并打开了 start 开关,此后,waiting 状态的所有线程相继被唤醒,打印出了:

11 : Thread-0

12 : Thread-1

13 : Thread-2

14 : Thread-3

15 : Thread-4

16 : Thread-5

17 : Thread-6

18 : Thread-7

19 : Thread-8

20 : Thread-9

 

锁 lock 实现了两个目的:对共享数据(index、threadIds)的保护和线程同步

上面的代码很直观地展示出了 Object 对象的 wait 和 notifyAll 两个方法的用法

wait 方法让线程释放锁并陷入 waiting 状态,直到被唤醒,如果在 wait 方法中传递了可选的 timeout 参数,则线程会在相应时间后自动被唤醒,而 notifyAll 则唤醒在相应锁上所有处于 waiting 状态的线程

Object 还实现了 notify 方法,每次调用只唤醒一个在相应锁上所有处于 waiting 状态的线程,通常我们使用 notifyAll,如果将上面代码中的 notifyAll 换成 notify,则所有并发线程会陷入死锁

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 






技术帖      技术分享      线程      sleep      thread      lock      wait      notify      notifyall     


京ICP备15018585号