多线程之Java线程阻塞与唤醒

本文涉及的产品
语种识别,语种识别 100万字符
图片翻译,图片翻译 100张
文本翻译,文本翻译 100万字符
简介: 线程的阻塞和唤醒在多线程并发过程中是一个关键点,当线程数量达到很大的数量级时,并发可能带来很多隐蔽的问题。如何正确暂停一个线程,暂停后又如何在一个要求的时间点恢复,这些都需要仔细考虑的细节。

线程的阻塞和唤醒在多线程并发过程中是一个关键点,当线程数量达到很大的数量级时,并发可能带来很多隐蔽的问题。如何正确暂停一个线程,暂停后又如何在一个要求的时间点恢复,这些都需要仔细考虑的细节。在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则完全不用考虑对象、锁、等待队列等问题。


点击订购作者书籍《Tomcat内核设计剖析》



目录
相关文章
|
21天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
148 60
【Java并发】【线程池】带你从0-1入门线程池
|
10天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
61 23
|
1月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
62 20
|
17天前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
86 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
1月前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
116 14
|
1月前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
58 13
|
1月前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
1月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
2月前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
132 17
|
3月前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者