线程的阻塞和唤醒在多线程并发过程中是一个关键点,当线程数量达到很大的数量级时,并发可能带来很多隐蔽的问题。如何正确暂停一个线程,暂停后又如何在一个要求的时间点恢复,这些都需要仔细考虑的细节。在Java发展史上曾经使用suspend()、resume()方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。如下代码,主要的逻辑代码是主线程启动线程mt一段时间后尝试使用suspend()让线程挂起,最后使用resume()恢复线程。但现实并不如愿,执行到suspend()时将一直卡住,你等不来“canyou
get here?”的输出。

public class ThreadSuspend {

public static voidmain(String[] args) {

Thread mt = newMyThread();

mt.start();

try {

Thread.currentThread().sleep(100);

} catch(InterruptedException e) {

e.printStackTrace();

}

mt.suspend();

System.out.println("canyou get here?");

mt.resume();

}

static class MyThreadextends Thread {

public void run() {

while (true) {

System.out.println("running....");

}

}

}

}

产生上面所述现象其实是由死锁导致,看起来一点问题都没有,线程的任务仅仅只是简单地打印字符串,问题的根源隐藏得较深,主线程启动了线程mt后,线程mt开始执行execute()方法,不断打印字符串,问题正是出现在System.out.println,由于println被声明为一个同步方法,执行时将对System类的out(PrintStream类的一个实例)单例属性加同步锁,而suspend()方法挂起线程但并不释放锁,在线程mt被挂起后主线程调用System.out.println同样需要获取System类out对象的同步锁才能打印“can
you get here?”,主线程一直在等待同步锁而mt线程不释放锁,这就导致了死锁的产生。

可见suspend和resume有死锁倾向,一不小心将导致很多问题,甚至导致整个系统崩溃。也许,解决方案可以使用以对象为目标的阻塞,即利用Object类的wait()和notify()方法实现线程阻塞。针对对象的阻塞编程思维需要稍微转化下,它与面向线程阻塞思维有较大差异,如前面的suspend与resume只需在线程内直接调用就能完成挂起恢复操作,这个很好理解,而如果改用wait、notify形式则通过一个object作为信号,可以看成是一堵门,object的wait()方法是锁门的动作,notify()是开门的动作,某一线程一旦关上门后其他线程都将阻塞,直到别的线程打开门。如图2-5-8-4,一个对象object调用wait()方法则像是堵了一扇门,线程一、线程二都将阻塞,线程三调用object的notify()方法打开门(准确说是调用了notifyAll()方法,notify()仅仅能让线程一或线程二其中一条线程通过),线程一、线程二得以通过。

图2-5-8-4

使用wait和notify能规避死锁问题,但并不能完全避免,必须在编程过程中避免死锁。在使用过程中需要注意的几点是:首先,wait、notify方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其次,wait、notify方法必须在synchronized块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait、notify方法的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放。当然假如你不按照上面规定约束编写,程序一样能通过编译,但运行时将抛出IllegalMonitorStateException异常,必须在编写时保证用法正确;最后,notify是随机唤醒一条阻塞中的线程并让之获取对象锁,进而往下执行,而notifyAll则是唤醒阻塞中的所有线程,让他们去竞争该对象锁,获取到锁的那条线程才能往下执行。

通过wait、notify改造上面的例子,代码如下,改造的思想就是在MyThread中添加一个标识变量,一旦变量改变就相应地调用wait和notify阻塞唤醒线程,由于在执行wait后将释放synchronized (this)锁住的对象锁,此时System.out.println("running....");早已执行完毕,System类out对象不存在死锁问题。

publicclass ThreadWait {

public static void main(String[] args) {

MyThread mt = new MyThread();

mt.start();

try {

Thread.currentThread().sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

mt.suspendThread();

System.out.println("can you gethere?");

try {

Thread.currentThread().sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

mt.resumeThread();

}

}

classMyThread extends Thread {

public boolean stop = false;

public void run() {

while (true) {

synchronized (this) {

System.out.println("running....");

if (stop)

try {

wait();

} catch(InterruptedException e) {

e.printStackTrace();

}

}

}

}

public void suspendThread() {

this.stop = true;

}

public void resumeThread() {

synchronized (this) {

this.stop = false;

notify();

}

}

}

wait与notify组合的方式看起来是个不错的解决方式,但其面向的主体是对象object,阻塞的是当前线程,而唤醒的是随机的某个线程或所有线程,偏重于线程之间的通信交互。假如换个角度,面向的主体是线程的话,我就能轻而易举地对指定的线程进行阻塞唤醒,这个时候就需要LockSupport,它提供的park和unpark方法分别用于阻塞和唤醒,而且它提供避免死锁和竞态条件,很好地代替suspend和resume组合。用park和unpark改造上述例子,代码如下:

public class ThreadPark {

public static voidmain(String[] args) {

MyThreadmt = new MyThread();

mt.start();

try {

Thread.currentThread().sleep(10);

} catch(InterruptedException e) {

e.printStackTrace();

}

mt.park();

System.out.println("canyou get here?");

try {

Thread.currentThread().sleep(3000);

} catch(InterruptedException e) {

e.printStackTrace();

}

mt.unPark();

}

static classMyThread extends Thread {

privateboolean isPark=false;

publicvoid run() {

while(true) {

if(isPark)

LockSupport.park();

System.out.println("running....");

}

}

public void park(){

isPark=true;

}

public void unPark(){

isPark=false;

  LockSupport.unpark(this);

}

}

}

把主体换成线程进行的阻塞看起来貌似比较顺眼,而且由于park与unpark方法控制的颗粒度更加细小,能准确决定线程在某个点停止,进而避免死锁的产生,例如此例中在执行System.out.println前线程就被阻塞了,于是不存在因竞争System类out对象而产生死锁,即便在执行System.out.println后线程才阻塞也不存在死锁问题,因为锁已释放。

LockSupport类为线程阻塞唤醒提供了基础,同时,在竞争条件问题上,它具有wait和notify无可比拟的优势。使用wait和notify组合时,某一线程在被另一线程notify之前必须要保证此线程已经执行到wait等待点,错过notify则可能永远都在等待,另外notify也不能保证唤醒指定的某线程。反观LockSupport,由于park与unpark引入了许可机制,许可逻辑为:①park将许可在等于0的时候阻塞,等于1的时候返回并将许可减为0;②unpark尝试唤醒线程,许可加1。根据这两个逻辑,对于同一条线程,park与unpark先后操作的顺序似乎并不影响程序正确地执行,假如先执行unpark操作,许可则为1,之后再执行park操作,此时因为许可等于1直接返回往下执行,并不执行阻塞操作。

最后,LockSupport的park与unpark组合真正解耦了线程之间的同步,不再需要另外的对象变量存储状态,并且也不需要考虑同步锁,wait与notify要保证必须有锁才能执行,而且执行notify操作释放锁后还要将当前线程扔进该对象锁的等待队列,LockSupport则完全不用考虑对象、锁、等待队列等问题。

喜欢研究java的同学可以交个朋友,下面是本人的微信号:

多线程之Java线程阻塞与唤醒的更多相关文章

  1. 多线程之Java中的等待唤醒机制

    多线程的问题中的经典问题是生产者和消费者的问题,就是如何让线程有序的进行执行,获取CPU执行时间片的过程是随机的,如何能够让线程有序的进行,Java中提供了等待唤醒机制很好的解决了这个问题! 生产者消 ...

  2. java线程阻塞唤醒的四种方式

    java在多线程情况下,经常会使用到线程的阻塞与唤醒,这里就为大家简单介绍一下以下几种阻塞/唤醒方式与区别,不做详细的介绍与代码分析 suspend与resume Java废弃 suspend() 去 ...

  3. java 线程之executors线程池

    一.线程池的作用 平时的业务中,如果要使用多线程,那么我们会在业务开始前创建线程,业务结束后,销毁线程.但是对于业务来说,线程的创建和销毁是与业务本身无关的,只关心线程所执行的任务.因此希望把尽可能多 ...

  4. java - 线程等待与唤醒

    Java多线程系列--“基础篇”05之 线程等待与唤醒 概要 本章,会对线程等待/唤醒方法进行介绍.涉及到的内容包括:1. wait(), notify(), notifyAll()等方法介绍2. w ...

  5. java线程阻塞问题排查方法

    我开发的worker,每隔几个月线上都会阻塞一次,一直都没查出问题.今天终于了了这个心结.把解决过程总结下和大家分享. 首先用jstack命令打出这个进程的全部线程堆栈.拿到线程dump文件之后,搜索 ...

  6. Java线程阻塞方法sleep()和wait()精炼详解

    版权声明:因为个人水平有限,文章中可能会出现错误,如果你觉得有描述不当.代码错误等内容或者有更好的实现方式,欢迎在评论区告诉我,即刻回复!最后,欢迎关注博主!谢谢 https://blog.csdn. ...

  7. java线程阻塞(sleep,suspend,resume,yield,wait,notify)

    为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了 ...

  8. Java多线程之Join方法阻塞线程

    package org.study2.javabase.ThreadsDemo.status; /** * @Auther:GongXingRui * @Date:2018/9/19 * @Descr ...

  9. Java多线程之sleep方法阻塞线程-模拟时钟

    package org.study2.javabase.ThreadsDemo.status; import java.text.SimpleDateFormat; import java.util. ...

随机推荐

  1. HDU 5909 Tree Cutting

    传送门 题意: 有一棵n个点的无根树,节点依次编号为1到n,其中节点i的权值为vi, 定义一棵树的价值为它所有点的权值的异或和. 现在对于每个[0,m)的整数k,请统计有多少T的非空连通子树的价值等于 ...

  2. 【HNOI2016】序列 莫队+单调栈+RMQ

    Description 给定长度为n的序列:a1,a2,…,an,记为a[1:n].类似地,a[l:r](1≤l≤r≤N)是指序列:al,al+1,…,ar-1,ar.若1≤l≤s≤t≤r≤n,则称a ...

  3. [bzoj4763]雪辉&[bzoj4812][Ynoi2017]由乃打扑克

    来自FallDream的博客,未经允许,请勿转载,谢谢. cut掉部分题面. 给一个n个点的树,点有点权,有m次询问,每次询问多条链的并有多少种不同的点权以及它的mex mex就是一个集合中最小的没有 ...

  4. [2017.4.7校内训练赛by hzwer]

    来自FallDream的博客,未经允许,请勿转载,谢谢. 报警啦.......hzwer又出丧题虐人啦..... 4道ctsc...有一道前几天做过了,一道傻逼哈希还wa了十几次,勉强过了3题..我好 ...

  5. Linux文件系统的介绍

    1.Linux的文件系统是一个典型的树形结构,只有一个根节点 如下图: 2.在Linux中一切皆文件 Linux 对数据文件(.mp3..bmp),程序文件(.c..h.*.o),设备文件(LCD.触 ...

  6. Spring消息之JMS.

    一.概念 异步消息简介 与远程调用机制以及REST接口类似,异步消息也是用于应用程序之间通信的. RMI.Hessian.Burlap.HTTP invoker和Web服务在应用程序之间的通信机制是同 ...

  7. mongoose多条件模糊查询实例

    mongoose多条件模糊查询 这是今天手头项目中遇到的一个问题,关于mongoose如何实现类似于SQL中 nick LIKE '%keyword%' or email LIKE '%keyword ...

  8. Docker安装tomcat和部署项目

    随着微服务的流行,Docker越来越流行,正如它的理念"Build, Ship, and Run Any App, Anywhere"一样,Docker提供的容器隔离技术使得开发人 ...

  9. 整理spring定时器corn表达式

    1.结构 corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份 2.各字段的含义   字段 允许值 允许的特殊字符 秒 0~59 - * / 分 0~59 - * / ...

  10. IP地址段遍历

    #region 搜索ftp服务器地址 /// <summary> /// 搜索ftp服务器 /// </summary> public void SearchFtpServer ...