线程的阻塞

本文深入探讨了线程阻塞的概念及其常见原因,包括睡眠、等待、输入输出等待、同步控制方法调用导致的阻塞。阐述了如何通过中断机制使任务跳出阻塞状态,并详细解释了中断过程中的注意事项,以及如何使用Future对象实现任务中断。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     

 所谓的阻塞,就是线程能够运行,但是某个条件阻止它的运行,当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间,直到线程重新进入就绪状态,它才有可能执行操作。就绪并代表是在运行啊,所谓的就绪,就是可运行也可不运行,只要调度器分配时间片给线程,线程就可以运行,因为我们都知道,调度器是如何分配线程,是不确定的。为什么任务会进入阻塞的状态,一般有以下几个原因:
        1.通过调用sleep(milliseconds)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行;
        2.通过调用wait()使线程挂起,直到线程得到了notify()或notifyAll()消息(或者java SE5的java.util.concurrent类库中等价的signal()或signalAll()),线程才会进入就绪状态;
        3.任务在等到某个输入或输出完成;
        4.任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取这个锁;
        阻塞的任务是必须终止的,我们不能等待其到达代码中可以检查其状态值的某一点,因而决定让它主动终止,那么就必须强制这个任务跳出阻塞状态。那么怎样使任务跳出阻塞状态?就是中断。中断?没错,就是中断,但是中断是会抛出异常的啊!没问题,只要不抛出就行!Thread.interrupted()提供了离开run()而不抛出异常的方式。为了调用interrupt(),我们就必须持有Thread对象,但是现在java的concurrent类库在避免对Thread对象的直接操作,尽量通过Executor来执行所有操作。如果我们在Executor上调用shutdownNow(),那么它 将发送一个interrupt()调用给它启动的所有线程。为什么呢?因为当我们完成工程中的某个部分或者整个程序时,通常会希望同时关闭某个特定Executor的所有任务。但是我们也想要只中断某个单一的任务,于是就通过调用submit()而不是executor()来启动任务,就可以持有任务的上下文。submit()将返回一个Future<?>,其中有一个未修饰的参数。为什么要用这个泛型的<?>?其实我也对这个感到兴趣,但是一时半会还是理解不了,所以先放一边吧。持有这种Future的关键在于可以在其上调用cancel(),因此可以使用它来中断某个特定的任务,如果我们将true传递给cancel(),那么就会拥有在该线程上调用interrupt()以停止这个线程的权限。所以,cancel()是一种中断由Executor启动的单个线程的方式。如:
       Future<?> f = exec.submit(RunnableClass);
       f.cancel(true);
       但是我们要注意,不是所有阻塞都可以被中断的,第一种情况可以被中断,但是第3和第4种是不可以被中断的,至于第2种有待研究。
       第3种的解决方案就是利用各种nio类来使阻塞的nio通道自动响应中断。对于第4种,使用显示的ReentrantLock对象,因为在它上面阻塞的任务是具备可以被中断的能力,这与在synchronized方法或临界区上阻塞的任务完全不同。这时我们可以直接调用interrupt()来中断被阻塞的任务,如:lock.interrupt();
       当我们在线程上调用interrupt()时,中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作内部时,但是如果根据程序运行的环境,我们可能已经编写了可能会产生这种阻塞调用的代码,那么该怎么办?如果我们只能通过调用在阻塞调用上抛出异常来退出,那么我们就无法总是可以离开run()循环。因此如果调用interrupt()已停止某个任务,那么在run()循环碰巧没有产生任何阻塞调用的情况下,我们的任务就需要第二种方式来退出。于是我们就需要利用中断状态。中断状态是可以通过调用interrupted()来检查中断状态,这不仅可以告诉我们interrupt()是否被调用过,而且还可以清除中断状态。清除中断状态可以确保并发结构不会就某个任务被中断这个问题通知我们两次,我们可以经由单一的InterruptedException或单一的成功的Thread.interrupted()测试来得到这种通知。如果想要再次检查以了解是否被中断,则可以在调用Thread.interrupted()时将结果存储起来。接下来就是展示检查的惯用法,应该在run()方法中使用它来处理在中断状态被设置时,被阻塞和不被阻塞的各种可能,如:
      public void run(){
           try{
                while(!Thread.interrupted()){
                         ....
                         try{
                              //被阻塞的可能
                              try{
                                   //不被阻塞的可能
                              }finally{ ...cleanup();}
                         }finally{.....cleanup();}
                  }
             }catch(InterruptedException e){}
      }
这里的惯用法特别强调,在我们经由异常离开循环时,必须要正确的清理资源。被设计用来响应interrupt()的类必须建立一种策略,来确保它将保持一致的状态,这通常意味着所有需要清理的对象创建的操作的后面,都必须紧跟try-finally子句,从而使得无论run()循环如何退出,清理都会发生。
### Java 线程阻塞与唤醒的方法 在多线程编程中,线程阻塞和唤醒是实现并发控制的重要机制。Java 提供了多种方法来实现线程阻塞和唤醒,主要包括 `wait()`、`notify()`、`notifyAll()`、`sleep()`、`join()` 以及 `LockSupport` 类等。 #### 阻塞线程的方法 1. **`wait()`**:该方法使当前线程进入等待状态,并释放持有的对象锁。线程会一直等待,直到其他线程调用 `notify()` 或 `notifyAll()` 方法唤醒它。需要注意的是,`wait()` 必须在同步代码块或同步方法中调用,否则会抛出 `IllegalMonitorStateException`。 2. **`sleep(long millis)`**:该方法使当前线程暂停执行指定时间(以毫秒为单位),但不会释放任何锁资源。线程在睡眠结束后会自动恢复运行。 3. **`join()`**:该方法用于等待另一个线程执行完成后再继续执行当前线程。例如,线程 A 调用 `threadB.join()`,A 将被阻塞,直到线程 B 执行完毕。 4. **`LockSupport.park()`**:该方法通过 `LockSupport` 类提供的静态方法实现线程阻塞操作。它可以替代传统的 `wait()` 和 `notify()`,并且不需要持有对象锁即可使用。 以下是一个简单的示例,展示如何使用 `wait()` 和 `notify()`: ```java class WaitNotifyExample { private boolean flag = false; public void waitForFlag() throws InterruptedException { synchronized (this) { while (!flag) { wait(); // 线程进入等待状态 } System.out.println("Flag is true, continuing execution"); } } public void setFlag() { synchronized (this) { flag = true; notify(); // 唤醒等待的线程 } } public static void main(String[] args) { WaitNotifyExample example = new WaitNotifyExample(); Thread waiter = new Thread(() -> { try { example.waitForFlag(); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread notifier = new Thread(() -> { try { Thread.sleep(2000); // 模拟延迟 example.setFlag(); } catch (InterruptedException e) { e.printStackTrace(); } }); waiter.start(); notifier.start(); } } ``` #### 唤醒线程的方法 1. **`notify()`**:唤醒一个正在等待该对象监视线程。如果有多个线程在等待,则选择其中一个线程唤醒,具体选择哪个线程由 JVM 决定。 2. **`notifyAll()`**:唤醒所有正在等待该对象监视线程。这些线程会在锁释放后竞争重新获取锁并继续执行。 3. **`interrupt()`**:中断线程可以唤醒因调用 `sleep()` 或 `join()` 而阻塞线程,并抛出 `InterruptedException`。这种方式适用于需要提前终止线程的情况。 4. **`LockSupport.unpark(Thread thread)`**:该方法通过 `LockSupport` 类提供的静态方法实现对指定线程的唤醒操作。它可以在任意时刻调用,即使目标线程尚未调用 `park()`,也不会导致异常。 以下是一个使用 `LockSupport` 的示例: ```java import java.util.concurrent.locks.LockSupport; public class LockSupportExample { public static void main(String[] args) { Thread mainThread = Thread.currentThread(); Thread counterThread = new Thread(() -> { for (int i = 1; i <= 1000; i++) { System.out.println(i); if (i == 500) { LockSupport.unpark(mainThread); // 唤醒主线程 } } }); counterThread.start(); LockSupport.park(); // 主线程进入阻塞状态 System.out.println("Main thread was unparked."); } } ``` #### 特殊情况:IO 阻塞的处理 对于因 I/O 操作而阻塞线程Java 的中断机制无法直接唤醒它们。此时需要依赖具体的 I/O 操作提供的机制,例如设置超时时间、检查操作状态等[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值