Java 从入门到精通(十六):线程通信与 wait()/notify(),为什么有些线程不是抢锁,而是在“等条件”?
Java 从入门到精通十六线程通信与 wait()/notify()为什么有些线程不是抢锁而是在“等条件”前一篇我们把线程同步这件事先讲透了为什么多个线程一起改共享变量结果会乱什么是临界区synchronized到底在做什么锁对象为什么必须统一为什么“加锁”本质上是在用排队换正确性但当你继续往下学很快会遇到一个新问题并发世界里线程并不总是在“抢着执行”很多时候它们其实是在“等条件”。比如这些场景仓库里没货消费者线程要先等生产者放货任务还没准备好工作线程不能硬往下跑缓冲区满了生产者不能继续塞数据主线程要等某个结果出现后再继续执行这时你会发现光有synchronized还不够。因为synchronized只能解决同一时刻谁能进临界区多个线程别同时乱改共享数据但它不能直接表达另一层更重要的逻辑现在不是“能不能进”的问题而是“条件满足了吗”。这就是线程通信要解决的事。所以这篇文章我想把 Java 并发入门里非常关键的一块讲透什么叫线程通信它和线程同步有什么区别为什么有些线程要“等待条件”而不是一直抢 CPUwait()、notify()、notifyAll()分别在做什么它们为什么必须和synchronized一起用经典的生产者-消费者模型应该怎么理解初学者最容易踩哪些坑你把这一篇吃透后面再去学LockCondition阻塞队列线程池任务协调CountDownLatch、Semaphore、CyclicBarrier会顺很多。一、先说结论线程通信解决的不是“互斥”而是“条件协调”很多初学者一开始会把同步和通信混在一起。其实它们关注的是两个不同层面。1线程同步关注的是“别同时乱来”比如多个线程一起改同一个变量这时重点是互斥。2线程通信关注的是“什么时候该等什么时候该继续”比如消费者线程发现仓库为空这时它不是要去和别的线程竞争执行资格而是当前条件不满足我先等。等到生产者放入数据再把它叫醒。所以你可以先这样理解同步解决冲突通信解决配合这两个能力经常一起出现但不是一回事。二、为什么不能靠死循环一直等很多新手第一次遇到“等条件”时会很自然地写出这种代码while(!ready){// 一直等}表面上好像能工作。但这种写法问题很大。1会空转占 CPU线程什么都不做却一直检查条件CPU 会被白白消耗掉。2不优雅也不稳定如果很多线程都这么写系统会出现大量无意义轮询。3很难和同步机制正确配合共享条件通常还伴随共享数据访问如果你只是死循环很容易把线程安全和条件等待写乱。所以更合理的做法不是“傻等”而是条件不满足时把线程挂起条件满足时再唤醒它。这正是wait()/notify()的设计目的。三、wait() 到底在做什么你先别急着背规范先抓住最实用的理解。wait()的作用可以概括成一句话当前线程先进入等待状态并释放当前持有的对象锁。这里有两个关键词特别重要。1进入等待状态也就是线程不会继续往下执行了它会暂停等别人来唤醒。2释放锁这点非常关键。如果线程等待时还一直霸占锁别的线程就进不来也就没法修改条件更没法唤醒它。所以wait()不是简单暂停而是我先把锁让出来自己去等待队列里待着等条件变好。这就是为什么它特别适合“有条件地继续执行”的场景。四、notify() 和 notifyAll() 又在做什么既然线程可以等那就必须有人来叫醒它。这就是notify()和notifyAll()的作用。1notify()从等待这个锁对象的线程里随机唤醒一个。注意是“一个”不是全部。2notifyAll()把等待这个锁对象的所有线程都唤醒。注意唤醒不等于立刻执行。被唤醒的线程只是从等待状态转成“有资格继续竞争锁”的状态。最终谁先真的继续执行还要看谁先重新拿到锁。所以这三个方法放在一起看更容易理解wait()我先等notify()叫一个起来notifyAll()大家都起来再竞争五、为什么 wait()/notify() 必须和 synchronized 一起用这是 Java 并发入门里一个特别重要的规则。很多初学者第一次看到会疑惑“既然是线程等待和唤醒为什么非得和锁绑在一起”因为线程通信讨论的通常不是某个孤立线程自己的状态而是围绕同一份共享数据多个线程如何在同一把锁保护下协调条件。更直白一点说条件通常依附在共享数据上共享数据需要同步保护等待和唤醒必须发生在同一套锁语义里所以 Java 才要求线程只有在持有某个对象监视器monitor的前提下才能对这个对象调用wait()、notify()、notifyAll()。如果你不在同步块或同步方法里直接调用通常会抛出java.lang.IllegalMonitorStateException这不是语法刁难而是为了保证线程通信和共享数据访问在同一套规则里成立。六、最基础的写法长什么样先看一个最小可理解例子。假设有一个线程要等任务准备好另一个线程负责把任务准备好。publicclassTaskDemo{privatefinalObjectlocknewObject();privatebooleanreadyfalse;publicvoidwaitForTask(){synchronized(lock){while(!ready){try{lock.wait();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}System.out.println(Thread.currentThread().getName() 开始处理任务);}}publicvoidprepareTask(){synchronized(lock){readytrue;System.out.println(Thread.currentThread().getName() 已准备好任务);lock.notifyAll();}}}使用时publicclassDemo{publicstaticvoidmain(String[]args){TaskDemotaskDemonewTaskDemo();ThreadworkernewThread(taskDemo::waitForTask,工作线程);ThreadproducernewThread(taskDemo::prepareTask,准备线程);worker.start();try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}producer.start();}}这里的执行逻辑是工作线程先进入同步块发现ready false调用wait()释放锁并进入等待准备线程进入同步块把ready改成true调用notifyAll()唤醒等待线程工作线程被唤醒后重新竞争锁拿到锁后继续往下执行这就是最基础的“条件等待 条件满足后唤醒”。七、为什么一定要用 while不要用 if这是非常重要、也特别容易被忽视的一个点。很多人一开始会写成if(!ready){lock.wait();}看起来好像也行。但更规范、更安全的写法通常是while(!ready){lock.wait();}为什么1线程被唤醒后条件未必真的满足被叫醒不等于条件一定还成立。因为可能多个线程一起被唤醒其中一个线程先拿到锁并改变了状态当前线程重新拿到锁时条件可能又不满足了2存在伪唤醒spurious wakeup也就是线程可能在没有明确notify的情况下返回等待。虽然初学阶段你不必深究 JVM 细节但规范层面就是要求你用循环重新检查条件。所以一定要建立这个习惯等待不是“醒了就干”而是“醒了先重新看条件”。这就是while的意义。八、经典场景生产者和消费者为什么特别适合讲 wait()/notify()因为它几乎把线程通信的核心逻辑全体现出来了。先看这个场景生产者线程负责往缓冲区放数据消费者线程负责从缓冲区取数据会遇到两个典型条件1缓冲区为空时消费者要等因为没东西可取。2缓冲区满时生产者要等因为不能无限往里塞。这时就不是“谁抢到锁谁干到底”那么简单了而是有货时消费者才能消费有空位时生产者才能生产这就是非常典型的条件协调问题。九、一个简化版的生产者-消费者示例我们先用一个只能存一个整数的仓库来理解。publicclassStore{privateintproduct;privatebooleanhasProductfalse;publicsynchronizedvoidproduce(intvalue){while(hasProduct){try{wait();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}productvalue;hasProducttrue;System.out.println(Thread.currentThread().getName() 生产了value);notifyAll();}publicsynchronizedvoidconsume(){while(!hasProduct){try{wait();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}System.out.println(Thread.currentThread().getName() 消费了product);hasProductfalse;notifyAll();}}测试代码publicclassDemo{publicstaticvoidmain(String[]args){StorestorenewStore();ThreadproducernewThread(()-{for(inti1;i5;i){store.produce(i);}},生产者);ThreadconsumernewThread(()-{for(inti1;i5;i){store.consume();}},消费者);producer.start();consumer.start();}}这个例子虽然简单但已经把最核心的逻辑表达出来了生产者发现已有产品就等待消费者发现没有产品就等待状态改变后通知对方继续竞争执行这就是线程通信最经典的模型。十、wait() 和 sleep() 看起来都像“暂停”到底差在哪这是入门阶段特别容易混淆的点。很多人觉得它们都能让线程停下来那是不是差不多其实差别很大。1sleep()是 Thread 类的方法谁调用谁休眠到时间自动恢复不会释放锁2wait()是 Object 类的方法必须在同步环境中调用进入等待直到被唤醒或中断会释放当前对象锁这两者的使用场景完全不同。如果你只是想模拟耗时、延迟一下可以用sleep()。如果你是在做线程间条件协调那通常应该考虑wait()。一句话区分就是sleep()是“我先睡会儿”wait()是“条件没到我先把锁让出来等通知”。十一、为什么很多教程更推荐 notifyAll() 而不是 notify()因为notify()虽然更“省”但在多线程场景下更容易出现唤醒错对象的问题。比如同一个锁对象上可能既有生产者在等也有消费者在等。如果你只随机唤醒一个线程结果可能是本来该唤醒消费者却唤醒了另一个生产者对方醒来后发现条件还是不满足又继续等程序虽然不一定立刻错但容易出现协调效率差、逻辑绕、甚至卡住的问题。而notifyAll()的思路是把所有等这个条件的线程都叫起来让它们自己重新竞争锁并检查条件。这样通常更安全更不容易埋下隐蔽 bug。代价是可能有些线程被无效唤醒会多一些上下文切换成本所以工程上常见的建议是初学阶段优先理解和使用notifyAll()等你对等待队列和条件拆分更熟再考虑更细粒度优化十二、初学者最容易踩的 8 个坑1不在同步块里调用 wait()/notify()这会直接抛异常。2用 if 代替 while会让条件检查不稳容易在复杂场景下出错。3调用了 wait() 却忘了对应条件什么时候改变等待和唤醒必须围绕共享状态变化来写不是随便调个 API 就完事。4只写 wait()不写 notify()/notifyAll()那线程可能就一直等下去。5以为 notify() 会立刻让对方执行不对。对方只是被唤醒还得重新竞争锁。6把 sleep() 当成线程通信手段sleep()不能表达“条件满足再继续”的语义只是拖时间。7锁对象不统一如果等待和唤醒不是针对同一个锁对象通信就无法成立。8没有把“条件”和“共享数据”绑定起来思考线程通信不是背方法名而是先问线程到底在等什么条件条件由哪个共享变量表示谁来修改它修改后谁应该被唤醒这才是正确思考顺序。十三、你应该怎么学线程通信才不容易乱如果你现在刚接触这块我建议按这个顺序建立理解。第一步先找“条件”比如缓冲区为空 / 不为空仓库已满 / 未满任务已准备 / 未准备结果已生成 / 未生成第二步再找共享状态这些条件通常不是一句空话而是由具体变量表示readyhasProductsizecount第三步最后才写 wait/notify也就是说你不是为了“用一下 API”而写它们而是为了表达条件不满足 → 进入等待条件变化后 → 唤醒相关线程一旦你这样学就不会把线程通信理解成单纯的语法题。十四、最后总结线程通信的本质不是让线程停下而是让它们学会“按条件配合”如果只用一句话概括这篇我会说线程同步解决的是“别同时乱改”线程通信解决的是“条件没到先等条件到了再继续”。你这篇真正应该带走的是下面这几个核心认识1并发里很多线程不是在抢着执行而是在等待某个条件成立所以通信和同步一样重要。2wait()的核心作用是让线程进入等待并释放锁它不是简单暂停而是为条件协调服务。3notify()/notifyAll()的作用是在条件变化后唤醒等待线程但被唤醒线程还要重新竞争锁。4wait()/notify()必须和synchronized配合使用因为等待、唤醒和共享数据访问必须建立在同一套锁语义下。5等待条件时用 while比用 if 更稳妥因为线程醒来后必须重新检查条件。6生产者-消费者模型是理解线程通信最经典的一块训练场你把它吃透后面很多并发工具都会好懂很多。从这里开始你对 Java 并发的理解就从“会加锁”进入到了“会协调线程配合”的阶段。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2521050.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!