【五一创作】java 线程唤醒机制

目录

java 线程状态简介

Object 类中与线程唤醒相关的方法

等待/通知机制的基本原理

实现一个生产者-消费者模型

避免虚假唤醒问题

使用 Condition 接口实现更灵活的线程通信

使用 LockSupport 类控制线程状态

常见线程唤醒应用场景


 

java 线程状态简介

  1. New:一个新创建的线程对象开始于此状态,但是它还没有同时开启。

  2. Runnable:当线程已经在JVM中运行,并且可以在任何时候进行调度时,它就处于Runnable状态。

  3. Blocked:当线程被阻止进入Synchronized同步块/方法(如被锁定)时,它就进入了Blocked状态。

  4. Waiting:线程处于Waiting状态意味着它正在等待另一个线程执行某些操作(例如唤醒),以使其恢复。

  5. Timed Waiting:与Waiting状态相似,但它会在一段时间后自动退出该状态,例如由于等待I/O操作完成。

  6. Terminated:线程已经结束执行,不再运行。

 

   

Object 类中与线程唤醒相关的方法

在Java程序中,Object类提供了一些方法与线程唤醒相关。这三个方法分别是wait()、notify()和notifyAll()方法。它们的作用是管理多线程之间的同步。

  • wait(): 当一个线程调用wait()方法时,它会释放对象的锁并进入等待状态。调用wait()方法的线程会一直处于等待状态,直到其他线程调用相同对象的notify()或notifyAll()方法来通知该线程继续执行。

  • notify(): 当一个线程调用notify()方法时,它会通知其他正在等待该对象的锁的线程以去竞争该对象的锁。由于在notify()方法调用后,等待该对象的线程都会去竞争该对象的锁,因此不确定哪个线程会获得该对象的锁。

  • notifyAll(): 与notify()方法类似,但不仅会唤醒一个线程,而是唤醒所有正在等待该对象的锁的线程。这些已被唤醒的线程将开始竞争该对象的锁。

珍惜生命,远离死锁!要注意以下规则:

  1. 这些方法必须在已经获取对象锁时进行调用。如果没有该对象的锁,则会抛出IllegalMonitorStateException异常。

  2. 这些方法必须在try-catch块中进行调用,以处理可能抛出的InterruptedException异常。

  3. 等待/通知调用必须在同步块中。这是为了确保正在等待与正在提供线程之间的实时性。

下面是一个使用wait()、notify()和notifyAll()方法的示例 Java 代码:

public class WaitNotifyDemo {
    public static void main(String[] args) {
        MyObject obj = new MyObject();
        ThreadA ta = new ThreadA(obj);
        ThreadB tb = new ThreadB(obj);
        ta.start();
        tb.start();
    }
}

class MyObject {
    boolean flag;
    
    synchronized void waitMethod() {
        while (!flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+" waitMethod()");
    }
    
    synchronized void notifyMethod() {
        this.flag = true;
        this.notifyAll();
        System.out.println(Thread.currentThread().getName()+" notifyMethod()");
    }
}

class ThreadA extends Thread {
    private MyObject obj;
    
    ThreadA(MyObject obj) {
        this.obj = obj;
    }
    
    @Override
    public void run() {
        obj.waitMethod();
    }
}

class ThreadB extends Thread {
    private MyObject obj;
    
    ThreadB(MyObject obj) {
        this.obj = obj;
    }
    
    @Override
    public void run() {
        obj.notifyMethod();
    }
}

 

在此示例中,MyObject类实现了wait()、notify()和notifyAll()方法,ThreadA类将使用wait()方法等待线程、ThreadB类将调用notify()方法来通知等待的线程。

在主程序中创建两个线程,其中ThreadA线程将进入等待状态,而ThreadB线程将调用notify()方法。因此,ThreadA线程将被唤醒并输出消息。

在Java中,每个对象都有其自身的锁(也称为监视器锁)。当一个线程获得了一个对象的锁,它就可以执行该对象中所有被锁定的代码块。当另一个线程试图获取相同对象的锁时,它将被阻塞在那里,只有等到之前的线程释放锁后才能获取锁进入临界区。

需要注意的是,在使用wait()、notify()和notifyAll()方法时,其必须在由synchronized关键字修饰的同步代码块或同步方法内部使用,以确保线程安全。如果方法不是同步的,则调用对象的wait()、notify()或notifyAll()方法将会引发IllegalMonitorStateException异常。

除此之外,还需要注意以下几点:

  1. wait()方法和notify()/notifyAll()方法只需要操作同一个对象锁,否则是无法实现唤醒等待中的线程。

  2. 在调用wait()方法后,线程会释放持有的同步对象锁,而在被唤醒后重新竞争锁。

  3. 当线程正在等待时,它仍然持有对象锁,并且其他线程不能获取该对象的锁。

  4. notify()方法只会唤醒正在等待该对象锁的一个线程,而notifyAll()可以唤醒所有正在等待的线程。

下面是一个完整的Java程序,演示了使用wait()、notify()和notifyAll()方法进行线程通信的过程:

 

public class ThreadCommunication {
    public static void main(String[] args) throws InterruptedException {
        
        Object lock = new Object(); // 创建一个共享对象锁
        
        Thread t1 = new Thread(new Runnable(){
            // 线程1中调用wait()
            @Override
            public void run() {
                synchronized(lock) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 开始等待...");
                        lock.wait();
                        System.out.println(Thread.currentThread().getName() + " 继续执行...");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "线程1");
        
        Thread t2 = new Thread(new Runnable() {
            // 线程2中调用notify()
            @Override
            public void run() {
                synchronized(lock) {
                    System.out.println(Thread.currentThread().getName() + " 通知其他线程...");
                    lock.notifyAll();
                }
            }
            
        }, "线程2");
        
        t2.start(); // 先启动线程2
        Thread.sleep(1000);
        t1.start(); // 再启动线程1
        
    }
}

 

在这个例子中,我们定义了一个共享的Object对象作为锁。线程1首先获取这个锁调用wait()方法进入等待状态,并释放锁。线程2获取同样的锁后调用notifyAll()方法来唤醒所有等待中的线程。

值得注意的是,实际运行中,线程的执行顺序和时间是无法确定的,所以如果不进行适当的同步或者控制等待和唤醒的顺序可能会导致死锁或竞争等问题。

等待/通知机制的基本原理

Java的等待/通知机制(wait/notify)是一种线程间协作的技术,能够实现线程间的同步和通信。其基本的工作原理如下:

锁对象

Java中每个对象都有一个锁(也称为监视器锁或内置锁),通过synchronized关键字可以获取该对象锁。在使用等待/通知机制的时候,必须使用共享的对象作为锁。

等待集合

每个锁都会有一个等待集合,用于存储由于调用了wait()方法而进入等待状态的线程。在等待的过程中,线程会释放持有的锁,其他线程可以获得锁并执行相应的代码。

wait()方法

当一个线程调用某个对象的wait()方法时,它会释放对象锁并进入该对象的等待集合,直到其他线程调用该对象的notify()方法或notifyAll()方法来唤醒它。

notify()方法

当一个线程调用某个对象的notify()方法时,它会将该对象的等待集合中的一个线程从等待状态转化为可运行状态,使其可以继续执行被阻塞的代码块。如果同时有多个线程处于等待状态,则只会随机唤醒其中的一个线程。

notifyAll()方法

当一个线程调用某个对象的notifyAll()方法时,它会将该对象的等待集合中的所有线程从等待状态转化为可运行状态,使其可以继续执行被阻塞的代码块。

        需要注意的是,wait()、notify()和notifyAll()方法均需要在synchronized关键字修饰的同步代码块或方法内调用,以确保线程安全。这些方式可以用于解决多线程间的协作问题,实现线程间的同步和通讯。

另外,在使用等待/通知机制时,需要注意以下几点:

  1. wait()方法必须被包含在一个循环语句内部,以便在重新获得锁后判断条件是否满足。

  2. notify()和notifyAll()方法只有在持有相应对象锁的情况下才能被调用。

  3. 在调用wait()方法时,线程必须先获得相应对象的锁并且释放这个锁;在调用notify()或notifyAll()方法之前,线程也必须先获得锁。

实现一个生产者-消费者模型

import java.util.LinkedList;

public class ProducerConsumerExample {
    public static void main(String[] args) {
        LinkedList<Integer> buffer = new LinkedList<Integer>();
        int maxSize = 5;
        
        Producer producer = new Producer(buffer, maxSize);
        Consumer consumer = new Consumer(buffer);
        
        Thread producerThread = new Thread(producer);
        Thread consumerThread = new Thread(consumer);
        
        producerThread.start();
        consumerThread.start();
    }
}

class Producer implements Runnable {
    private LinkedList<Integer> buffer;
    private int maxSize;
    
    public Producer(LinkedList<Integer> buffer, int maxSize) {
        this.buffer = buffer;
        this.maxSize = maxSize;
    }
    
    public void run() {
        while (true) {
            synchronized (buffer) {
                while (buffer.size() == maxSize) {
                    try {
                        System.out.println("[Producer] Buffer is full. Waiting...");
                        buffer.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                int number = (int) (Math.random() * 10);
                buffer.add(number);
                System.out.println("[Producer] Produced " + number);
                
                buffer.notifyAll();
            }
        }
    }
}

class Consumer implements Runnable {
    private LinkedList<Integer> buffer;
    
    public Consumer(LinkedList<Integer> buffer) {
        this.buffer = buffer;
    }
    
    public void run() {
        while (true) {
            synchronized (buffer) {
                while (buffer.isEmpty()) {
                    try {
                        System.out.println("[Consumer] Buffer is empty. Waiting...");
                        buffer.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                int number = buffer.removeFirst();
                System.out.println("[Consumer] Consumed " + number);
                
                buffer.notifyAll();
            }
        }
    }
}

上述代码中,Producer 类和 Consumer 类分别实现了生产者和消费者。它们都是通过实现 Runnable 接口来创建线程。在 run() 方法中,它们都必须获得共享的缓冲区(在本例中是一个 LinkedList 对象)的锁对象,并检查缓冲区的条件。如果条件被满足(比如生产者可以往缓冲区中添加元素或者消费者可以从缓冲区中取出元素),则完成操作并使用 notifyAll() 方法来通知其他线程。

注意,在等待时,每个线程必须调用 wait() 方法以避免浪费 CPU 资源。这也有助于减少死锁的风险。

多个生产者/消费者的情况下,可以通过为每个线程实例化一个独立的锁对象并确保访问共享数据时获取相应的锁对象来避免死锁。此外,还可以使用非阻塞算法来减少竞争,从而提高性能和可伸缩性。

总的来说,Java 的等待/通知机制可以用来构建强大的多线程应用程序。在实现生产者-消费者模型时,必须小心处理并发问题,并尝试避免死锁和其他潜在的问题。

 

 

避免虚假唤醒问题

虚假唤醒是指一个线程在没有被通知或者中断的情况下,从等待状态(通过 wait() 方法)恢复执行的情况。这种情况发生的原因是操作系统层面的某些条件发生变化,导致阻塞的线程不再被阻塞,此时阻塞的线程会经历一个虚假唤醒,造成其他线程无法正常运行。

虚假唤醒问题潜藏于基于等待/通知机制的线程同步代码中,可能会导致以下问题:

  1. 非预期的对象状态:处理对象状态的代码没有正确预测对象状态,从而产生意想不到的结果,发生错误。

  2. 安全问题:调用 wait() 方法和 notify() 方法必须在同步代码块中使用,否则将会抛出 IllegalMonitorStateException 异常。在处理条件等待时,如果没有正确地使用 synchronized 块来锁定访问资源,则会暴露资源破坏错误的风险。

  3. 性能问题:当等待过程被虚假的唤醒并重复运行,它会消耗一些计算资源。由于“虚假唤醒”是一个微小的、出现几率很小的情况,因此难以检测其性能影响。然而,消耗的资源可能会在大量线程调用 wait() 时累积,给系统带来较大的负荷。

要避免虚假唤醒问题,可以采用以下措施:

1.在使用 wait() 方法时,总是将其放置在一重循环中,在循环体内部判断条件变化。这样即使在虚假唤醒时认为自己没有满足某个条件并不会出现问题。例如:

 
 
synchronized (obj) {
    while (!condition()) {
        obj.wait();
    }
    // critical section
}

2.使用 if 而不是 while 规避触发一个虚假的唤醒。如果使用了 if,简单地编码实现允许多次调用它,而由于使用了 while 循环,这会导致在弹出阻塞状态后执行令人困惑的未期望代码。

3.尝试使用 Lock 和 Condition 等 j.u.c 包类的方法来代替 wait() 和 notify() 等方法等。这些方法提供了更细粒度的线程同步控制,并具有更高的灵活性。

4.给予 wait() 方法正确的等待时间:当 wait() 方法返回时,应检查等待时间是否逾期。可以通过 System.currentTimeMillis() 或者 System.nanoTime() 的返回值计算出过去的时间,然后用当前时间与所期望的时间比较来检查是否允许继续执行等待操作。

总之,在编写多线程应用程序时,应该注意虚假唤醒问题,采取必要的措施来避免这种问题,以确保程序能够正确、安全地运行。

 

 

使用 Condition 接口实现更灵活的线程通信

ReentrantLock 类是 Java 并发编程中一种比 synchronized 关键字更高级的同步工具,可以使用它来实现更精准、更灵活的线程同步。与 synchronized 相比,ReentrantLock 提供了更多丰富的功能,例如可重入锁、公平锁、读写锁等。

Condition 接口是 ReentrantLock 类提供的一个重要扩展。Condition 提供了一种新的方法来实现线程之间的通信和协调,其提供的 await() 和 signal() 方法可以替代传统的 wait() 和 notify() 方法。与传统的 wait() 和 notify() 方法不同的是,Condition 可以创建多个等待队列,线程可以进入不同的等待队列,并在被另一个线程信号唤醒时重新加入到等待队列中。

使用 ReentrantLock + Condition 接口实现更灵活的线程通信:

1.实例化 ReentrantLock 类:

private final ReentrantLock lock = new ReentrantLock();

2.通过 ReentrantLock 实例化 Condition 对象:

private final Condition condition = lock.newCondition();

3.线程等待状态

lock.lock();    //获取锁
try {
    while (条件不满足) {
        condition.await();  //条件不满足,线程进入等待状态并释放锁
    }
} finally {
    lock.unlock();  //记得释放锁
}

4.线程唤醒

lock.lock();    //获取锁
try {
    condition.signalAll();  //唤醒所有等待线程(也可使用 signal() 方法唤醒一个)
} finally {
    lock.unlock();  //记得释放锁
}

与 synchronized 关键字相比,ReentrantLock + Condition 接口提供了以下优点:

  1. ReentrantLock 提供了更丰富的锁支持,如可重入锁、读写锁、公平锁等。

  2. Condition 接口提供了更灵活的线程通信机制,可以创建多个等待队列。

  3. ReentrantLock 可以中断正在等待锁的线程。

  4. ReentrantLock 让用户能够通过 tryLock() 方法尝试获取锁,避免了由于一直等待锁而导致程序阻塞的问题。

总之,使用 ReentrantLock 类及其 Condition 接口可以更加细粒度地控制线程同步和通信,从而更好地保证线程安全和性能。即使现在应用程序中并不需要使用 ReentrantLock,学习它也是获得高级 Java 并发知识的良好方式。

使用 LockSupport 类控制线程状态

LockSupport 类是用来创建锁和其他同步类的基本实现工具,该类提供了 park() 和 unpark() 方法来控制线程的阻塞和唤醒。

使用 park() 方法可以阻塞当前线程,在没有特殊理由的情况下,它会使当前线程进入休眠状态。相对地,unpark() 方法可以唤醒一个被 park() 方法阻塞的线程。 LockSupport 线程阻塞和唤醒的功能比 Object wait() 和 notify() 更加灵活,让线程状态的控制更加精细。

LockSupport 中 park() 和 unpark() 的用法:

1.调用 park() 方法使线程阻塞

LockSupport.park();

2.使用 unpark() 方法唤醒阻塞线程

LockSupport.unpark(thread);

其中,unpark() 方法参数为需要唤醒的线程对象,可以通过 Thread.currentThread() 来获取当前线程。

LockSupport 类的优点是:

  1. 不需要加锁就可以阻塞和唤醒线程,提高了程序的性能。

  2. 可以精确控制线程状态,比 wait() 和 notify() 更灵活。

  3. 可以防止死锁和其他严重问题的出现。

总之,LockSupport 类及其 park() 和 unpark() 方法是 Java 并发编程中强大而又简洁的工具。虽然 LockSupport 类不能完全替代 synchronized 和 Lock,但是在某些场景下它可以更加方便和高效地控制线程状态。

常见线程唤醒应用场景

  1. 实现超时等待:可以使用 Object 的 wait(long timeout) 方法或者 LockSupport.parkNanos(long nanos) 方法来实现,在特定时间内等待某个事件的发生。

  2. 线程中断:当一个线程不再需要执行时,可以把该线程的中断标志设置为 true,然后在阻塞操作中响应中断信号。

  3. 多线程协作:多个线程之间需要互相通知,协同工作。此时可以使用 wait() 和 notify() 或者 await() 和 signal() 等方法来实现。

  4. 生产者-消费者模式:在生产者-消费者模式中,消费者需要在队列为空时等待生产者向队列中添加元素,在队列满时等待消费者取走元素。可以通过 wait() 和 notify() 或者 Condition 接口来实现。

  5. 使用信号量控制并发:可以利用 Semaphore 信号量来控制并发,例如通过 acquire() 方法获取访问资源的许可证,release() 方法释放该资源的访问许可证等。

  6. 控制线程顺序执行:可以通过 CountDownLatch、CyclicBarrier、Phaser 等类来控制线程的顺序执行,达到某种协同的效果。

  7. 避免线程死锁:可以通过设置超时机制,在等待其他线程释放资源的操作中避免线程死锁的发生。

总之,Java 提供了丰富的线程同步和通信机制,可以根据应用场景选择合适的工具来实现。需要注意的是,在使用线程唤醒的同时,要遵循统一的编码规范和最佳实践,以保证程序的正确性和可维护性。

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值