线程之间的协作

一、概览

在线程之间共享受限资源中,通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。线程之间的协作使得多个任务可以一起工作去解决某个问题。现在问题不是彼此之间的干涉,而是彼此之间的协调,因为在这类问题中,某些部分必须在其他部分被解决之前解决。

任务协作的关键是通过任务之间的握手,握手也是通过互斥这个基础特性来实现。互斥能够确保只有一个任务可以响应某个信号,这样就可以根除任何任务之间的竞争条件。握手可以通过Object的方法wait()和notify()来安全的实现,Java SE5的并发类库还提供了具有await()和signal()方法的Condition对象。

二、wait()和notifyAll()

wait()可以等待某个外部条件发生变化,而改变这个条件超出了当前方法的控制能力。通常这种条件将有另一个任务来改变。我们不希望不断进行空循环,来测试条件是否满足,这种方式称为忙等待,是一种不良的cpu周期使用方式。因此使用wait()将任务挂起,并且只有notify()或notifyAll()发生时,任务才会被唤醒并去检测所产生的变化。因此wait()提供了一种在任务间对活动对象的调用方式。

调用sleep()时锁并没有释放,调用yield()也是这种情况。当任务在方法里遇到wait()的调用时线程执行被挂起,对象上的锁被释放。因为wait()将释放锁,因此另一个任务可以获得锁,因此该对象上的其他synchronized方法可以在wait()期间被调用。这一点至关重要,因为其他的方法通常会产生变化,这种改变正是使被挂起的任务重新唤醒所感兴趣的变化。调用wait()时就是在声明:我已刚刚做完能做的所有事情,因此要在这里等待,但是我希望其他synchronized操作在条件合适的情况下可以执行。

sleep()与wait()的区别是:

  • 在wait()期间对象锁时释放的
  • 可以通过notify()、notifyAll(),或者时间到期,从wait()中恢复。

wait()、notify和notifyAll()是基类Object的一部分,不属于Thread的一部分。不过这是有道理的,因为这些方法操作的锁也是所有对象的一部分,所以可以把wait()放进任何同步控制方法里,而不用考虑这个类是否继承自Thread还是实现了Runnable接口。实际上,我们只能在同步代码块或同步方法里调用wait()、notify和notifyAll(),如果在非同步方法里调用这些方法,能编译通过但是会报IllegalMonitorStateException异常,并伴随一些含糊的信息,比如“当前线程不是拥有者”。消息是意思是任务在调用这些方法前必须“拥有”对象的锁。

实例:Car的涂蜡和抛光程序,涂蜡在抛光之前执行。

public class WaxOMatic {

    public static void main(String[] args) throws InterruptedException {

        Car car = new Car();
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new WaxOn(car));
        service.execute(new WaxOff(car));
        TimeUnit.SECONDS.sleep(5);

    }
}

class Car{
    private boolean waxOn = false;

    public synchronized void waxed(){
        waxOn = true;
        notifyAll();
    }

    public synchronized void buffed(){
        waxOn = false;
        notifyAll();
    }

    public synchronized void waitForWaxing() throws InterruptedException {
        while (waxOn == false){
            wait();
        }
    }

    public synchronized void waitForBuffing() throws InterruptedException {
        while (waxOn == true){
            wait();
        }
    }
}

class WaxOn implements Runnable{

    private Car car;

    public WaxOn(Car car){
        this.car = car;
    }

    @Override
    public void run() {
        try {
        while (!Thread.interrupted()){
            System.out.println("Wax on");
            TimeUnit.MILLISECONDS.sleep(2000);
            car.waxed();
            car.waitForBuffing();
        }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

class WaxOff implements Runnable{

    private Car car;

    public WaxOff(Car car){
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()){
                car.waitForWaxing();
                System.out.println("Wax Off!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.buffed();

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

实例中用一个检查感兴趣的条件的while循环包围wait()这很重要。这是因为:

  • 可能有多个任务出于相同的的原因等待同一个锁,而第一个唤醒任务可能会改变状况。如果属于这种情况,那么这个任务该被挂起,直至其感兴趣的条件发生变化。
  • 在这个任务从其wait()中被唤醒的时刻,有可能会有某个其他的任务已经做出了改变,从而使得这个任务在此时不能执行,或者执行其操作已显得无关紧要。此时应该再次通过调用wait()将其挂起。
  • 也有可能某些任务出于不同的原因在等待你的对象上的锁。这种情况下,你需要检查是否已经有正确的原因唤醒,如果不是就再次调用wait()。

错失信号

当两个线程使用notify()/wait()或notifyAll()/wait()进行协作时,有可能就会错过某个信号。假设T1是通知T2的线程,而这两个线程都是通过使用下面(有缺陷)方式实现的:

T1:

synchronized(sharedMonitor){
    <set up condition for T2>
    sharedMonitor.notify();
}

T2:

while(someCondition){
    //point1
    synchronized(sharedCondition){
        sharedMonitor.wait();
    }
}

假设T2对someCondition求值发现其为true.在point1,线程调度器可能切换到T1。而T1将执行其设置,然后调用notify()。当T2继续执行时对T2来说时机已经太晚了,以至于不能意识到这个条件已经发生了变化,因此会盲目进入wait()。此时notify()将错失,而T2也将无线等待这个已发送过去的信号,从而产生死锁。

该问题的解决方式为防止在someCondition变量上产生竞争条件,T2正确的执行方式:

 synchronized(sharedCondition){
    while(someCondition){
        sharedMonitor.wait();
    }
}

修改后如果T1先执行,那控制返回T2时它将发现条件发生了变化,从而不会进入wait()。反过来如果T2首先执行,那它将进入wait(),并稍后会有T1唤醒,因此信号不会错过。

三、 notify()和notifyAll()区别

在技术上可以有多个任务在单个对象上处于wait()状态,因此调用notifyAll()比调用notify()更安全。使用notify()而不是使用notifyAll()是一种优化,但是需要注意一下情况:

  • 使用notify()时,在众多需要等待锁的任务中只有一个会被唤醒,因此如果使用notify()就必须确保被唤醒的是恰当的任务。
  • 为了使用notify(),所有任务必须等待相同的条件,因为如果你有多个任务等待不同的条件,你就不会知道是否唤醒了恰当的任务。如果使用notify(),当条件发生变化时,必须只有一个任务从中受益。
  • 这些限制对所有可能存在的子类都必须总是起作用。

如果这些规则中有任何一条不满足你就必须使用notifyAll(),而不是notify()。

一直有一个比较困惑的描述:notifAll()将唤醒“所有正在等待的任务”。这是否意味着程序的任何地方,任何处于wait()状态中的任务都将被任何notifyAll()唤醒呢?显然不是,下面的例子演示了这种情况:

public class NotifyVsNotifyAll {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++){
            executor.submit(new Task());
        }

        executor.execute(new Task2());
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            boolean prod = false;
            @Override
            public void run() {
                if (prod){
                    System.out.println(" notify()");
                    Task.block.prod();
                    prod = false;
                }else {
                    System.out.println(" notifyAll()");
                    Task.block.prodAll();
                    prod = true;
                }
            }
        }, 400, 400);

        TimeUnit.SECONDS.sleep(5);
        timer.cancel();

        System.out.println("Timer canceled");
        TimeUnit.MILLISECONDS.sleep(500);

        System.out.println("Task2.block.prodAll()");
        Task2.blocker.prodAll();
        TimeUnit.MILLISECONDS.sleep(500);

        executor.shutdownNow();

    }
}

class Blocker {
    synchronized void waitingCall() {
        try {
            while (!Thread.interrupted()) {
                wait();
                System.out.println(Thread.currentThread() + " " );
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized void prod(){
        notify();
    }

    synchronized void prodAll(){
        notifyAll();
    }
}

class Task implements Runnable{

    static Blocker block = new Blocker();

    @Override
    public void run() {
        block.waitingCall();
    }
}

class Task2 implements Runnable{

    static Blocker blocker = new Blocker();

    @Override
    public void run() {
        blocker.waitingCall();

    }
}

在Blocker中prod()和prodAll(),这些方法是synchronized的,这意味着它们将获得自身的锁,因此当它们调用notify()或notifyAll()时,只在这个锁上调用是符合逻辑的——因此只唤醒在等待这个特定锁的任务。

生产者消费者与队列

wait()和notifyAll()方法以一种非常低级的方式解决任务互操作的,即每次交互时都要握手。在许多情况下,我们使用更高的抽象级别,使用同步队列来解决任务协作问题,同步队列在许多情况下只允许一个任务插入或删除元素。在java.util.concurrent.BlockingQueue接口中提供了这个队列.我们常用的有:

  • LinkedBlockingQueue它是一个无界队列
  • ArrayBlockingQueue它具有固定尺寸,可以在其被阻塞之前,向其中放置固定数量的元素。

如果消费者任务试图从队列中获取对象,而队列此时为空,那么这些队列还可以挂起消费者任务,并且当有更多的元素可用时恢复消费者任务。阻塞队列可以解决非常大量的问题,其方式与wait()和notifyAll()相比,则简单可靠的多。

<think>嗯,用户想了解线程之间协作方式和方法。首先,我需要回忆一下线程协作的基本概念。线程协作指的是多个线程在执行任务时,需要相互协调,比如等待某个条件满足或通知其他线程继续执行。根据之前学过的知识,线程协作主要有几种方式:Object的wait()/notify()、Lock和Condition、信号量、阻塞队列等等。 用户提供的引用里提到,Java中线程协作可以用Object的wait()和notify()方法,还有使用Lock和Condition的情况。引用[2]和[3]都提到了这些方法,特别是Condition的await()和signal()。另外,引用[4]提到使用显式的Lock和Condition对象时需要注意唤醒哪个线程,必须传入正确的引用。 接下来,我需要把这些方法整理出来,分点说明。首先,Object的监视器方法,也就是wait()、notify()和notifyAll()。这些方法必须在同步代码块或同步方法中使用,否则会抛出异常。举个例子,生产者消费者模式中,生产者生产数据后通知消费者,而消费者在没有数据时等待。 然后是Lock和Condition的方式。ReentrantLock配合Condition可以实现更灵活的线程通信,比如多个条件变量。比如,可以创建两个Condition,一个给生产者用,一个给消费者用,这样唤醒的时候更有针对性,避免全部唤醒导致的性能问题。 信号量(Semaphore)也是一种方式,通过控制许可的数量来协调线程。比如,限制同时访问某个资源的线程数,或者作为资源池的管理工具。 还有阻塞队列(BlockedQueue),它内部已经实现了线程安全的入队和出队操作,生产者往队列放数据,消费者从队列取数据,当队列满或空时会自动阻塞,简化了线程间的协作。 需要检查每种方法的适用场景和注意事项。比如,wait()和notify()必须搭配synchronized使用,而Condition需要配合Lock使用,并且可以精确控制唤醒的线程。信号量和阻塞队列可能更适合特定场景的资源管理。 用户可能想知道这些方法的区别和如何选择,所以在回答中应该比较它们的优缺点。比如,Object的方法比较基础,但不够灵活;Lock和Condition提供了更细粒度的控制;阻塞队列适合生产者消费者模式,代码更简洁。 还要注意代码示例的正确性,比如正确的加锁和解锁,避免死锁。例如,在使用Condition时,必须在lock()和unlock()之间调用await()和signal()。 最后,生成相关问题的时候,要考虑用户可能的后续疑问,比如性能比较、常见问题或实际应用案例。</think>### 线程协作方式及方法详解 #### 1. **基于`Object`的监视器方法(`wait()`/`notify()`)** - **核心原理**:通过`Object`类的`wait()`、`notify()`、`notifyAll()`实现线程协作,需配合`synchronized`关键字使用。 - **适用场景**:简单的线程等待-通知模型,例如生产者-消费者问题。 - **示例**: ```java // 共享资源对象 class SharedResource { private boolean flag = false; public synchronized void produce() { while (flag) { try { wait(); // 等待消费者消费 } catch (InterruptedException e) { /* ... */ } } flag = true; notifyAll(); // 通知消费者 } public synchronized void consume() { while (!flag) { try { wait(); // 等待生产者生产 } catch (InterruptedException e) { /* ... */ } } flag = false; notifyAll(); // 通知生产者 } } ``` - **注意事项**: - 必须在同步代码块或方法内调用,否则抛出`IllegalMonitorStateException`[^2]。 - `notify()`随机唤醒一个等待线程,`notifyAll()`唤醒所有等待线程[^4]。 --- #### 2. **基于`Lock`与`Condition`的显式协作** - **核心原理**:通过`ReentrantLock`和`Condition`对象实现更灵活的线程通信,支持多个条件队列。 - **适用场景**:需要区分不同等待条件的场景(如生产者-消费者模式中的缓冲区满/空)。 - **示例**: ```java import java.util.concurrent.locks.*; class SharedResource { private Lock lock = new ReentrantLock(); private Condition notFull = lock.newCondition(); // 条件1:缓冲区未满 private Condition notEmpty = lock.newCondition(); // 条件2:缓冲区未空 private boolean flag = false; public void produce() { lock.lock(); try { while (flag) { notFull.await(); // 等待缓冲区未满 } flag = true; notEmpty.signal(); // 唤醒消费者 } catch (InterruptedException e) { /* ... */ } finally { lock.unlock(); } } public void consume() { lock.lock(); try { while (!flag) { notEmpty.await(); // 等待缓冲区未空 } flag = false; notFull.signal(); // 唤醒生产者 } catch (InterruptedException e) { /* ... */ } finally { lock.unlock(); } } } ``` - **优势**: - 支持多个条件变量,唤醒更精准(如仅唤醒生产者或消费者)[^3][^4]。 - 可设置超时等待,避免死锁。 --- #### 3. **信号量(`Semaphore`)** - **核心原理**:通过控制许可证数量管理资源访问。 - **适用场景**:限制线程并发数,或作为资源池管理。 - **示例**: ```java Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问 void accessResource() { try { semaphore.acquire(); // 获取许可 // 使用资源 } catch (InterruptedException e) { /* ... */ } finally { semaphore.release(); } // 释放许可 } ``` --- #### 4. **阻塞队列(`BlockingQueue`)** - **核心原理**:通过线程安全的队列实现自动阻塞/唤醒。 - **适用场景**:生产者-消费者模式,无需手动同步。 - **示例**: ```java BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10); // 生产者 void produce(int item) { try { queue.put(item); } catch (InterruptedException e) { /* ... */ } } // 消费者 int consume() { try { return queue.take(); } catch (InterruptedException e) { return -1; } } ``` --- ### 方法对比与选择建议 | **方式** | **优点** | **缺点** | |------------------------|-----------------------------------|-----------------------------------| | `Object`监视器方法 | 简单易用,无需额外依赖 | 无法区分条件,唤醒不精确 | | `Lock`与`Condition` | 条件灵活,唤醒精准 | 需手动管理锁和条件 | | `Semaphore` | 控制并发数高效 | 不适用于复杂协作逻辑 | | `BlockingQueue` | 完全解耦生产者和消费者 | 仅适用于特定数据流场景 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值