写点什么

线程通信

作者:周杰伦本人
  • 2022 年 5 月 11 日
  • 本文字数:4517 字

    阅读完需:约 15 分钟

线程通信

volatile 和 synchronized 关键字

任意线程对 Object(Object 由 synchronized 保护)的访问,首先要获得 Object 的监视器。如果获取失败,线程进入同步队列,线程状态变为 BLOCKED。当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

等待/通知机制

notify():通知一个在对象上等待的线程,使其从 wait()方法返回,而返回的前提是该线程获取到了对象的锁


notifyAll():通知所有等待该对象上的线程


wait():调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用 wait0 方法后,会释放对象的锁


wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达 n 毫秒,如果没有通知就超时返回


wait(long,int):对于超时时间更细粒度的控制,可以达到纳秒


package com.example.xppdemo.chapter4;
import com.example.xppdemo.chapter4.SleepUtils;
import java.text.SimpleDateFormat;import java.util.Date;import java.util.concurrent.TimeUnit;
public class WaitNotify { static boolean flag = true; static Object lock = new Object();
public static void main(String[] args) throws Exception { Thread waitThread = new Thread(new Wait(), "WaitThread"); waitThread.start(); TimeUnit.SECONDS.sleep(1); Thread notifyThread = new Thread(new Notify(), "NotifyThread"); notifyThread.start(); }
static class Wait implements Runnable { public void run() {// 加锁,拥有lock的Monitor synchronized (lock) {// 当条件不满足时,继续wait,同时释放了lock的锁 while (flag) { try { System.out.println(Thread.currentThread() + " flag is true. wait @ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); lock.wait(); } catch (InterruptedException e) { } }// 条件满足时,完成工作 System.out.println(Thread.currentThread() + " flag is false. running @ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); } } }
static class Notify implements Runnable { public void run() {// 加锁,拥有lock的Monitor synchronized (lock) {// 获取lock的锁,然后进行通知,通知时不会释放lock的锁,// 直到当前线程释放了lock后,WaitThread才能从wait方法中返回 System.out.println(Thread.currentThread() + " hold lock. notify @ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); lock.notifyAll(); flag = false; SleepUtils.second(5); }// 再次加锁 synchronized (lock) { System.out.println(Thread.currentThread() + " hold lock again. sleep @ " + new SimpleDateFormat(" HH: mm: ss ").format(new Date())); SleepUtils.second(5); } } }}
复制代码


结果:


Thread[WaitThread,5,main] flag is true. wait @ 20:03:13Thread[NotifyThread,5,main] hold lock. notify @ 20:03:14Thread[NotifyThread,5,main] hold lock again. sleep @  20: 03: 19 Thread[WaitThread,5,main] flag is false. running @ 20:03:24
复制代码



WaitThread 首先获取了对象的锁,然后调用对象的 wait()方法,从而放弃了锁并进入了对象的等待队列 WaitQueue 中,进入等待状态。由于 WaitThread 释放了对象的锁,NotifyThread 随后获取了对象的锁,并调用对象的 notify()方法,将 WaitThread 从 WaitQueue 移到同步队列 SynchronizedQueue 中,此时 WaitThread 的状态变为阻塞状态。NotifyThread 释放了锁之后,WaitThread 再次获取到锁并从 wait()方法返回继续执行。


1)使用 wait()、notify()和 notifyAll()时需要先对调用对象加锁。


2)调用 wait()方法后,线程状态由 RUNNING 变为 WAITING,并将当前线程放置到对象的等待队列。


3)notify()或 notifyAll()方法调用后,等待线程依旧不会从 wait()返回,需要调用 notify()或 notifAll()的线程释放锁之后,等待线程才有机会从 wait()返回。


4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而 notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由 WAITING 变为 BLOCKED。


5)从 wait()方法返回的前提是获得了调用对象的锁。


等待方模板:


synchronized(对象) {while(条件不满足) {  对象.wait();}对应的处理逻辑}
复制代码


1)获取对象的锁。


2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。


3)条件满足则执行对应的逻辑。


对应的伪代码如下。


通知方


synchronized(对象) {  改变条件  对象.notifyAll();}
复制代码


1)获得对象的锁。


2)改变条件。


3)通知所有等待在对象上的线程。


wait 与 sleep 区别:


在调用 wait 方法时,线程必须要持有被调用对象的锁,当调用 wait 方法后,线程就会释放掉该对象的锁。


调用 Thread 类的 sleep 方法时,线程是不会释放对象的锁的。


wait notify notifyAll 方法总结:


  1. 当调用 wait 时,首先确保 wait 方法的线程已经持有对象的锁。

  2. 当调用 wait 后,该线程就会释放这个对象的锁,然后进入等待队列

  3. 当线程调用了 wait 后进入等待队列时,它可以等待其他线程调用相同对象的 notify 或者 notifyAll 来唤醒自己

  4. 线程被其他线程唤醒后,该线程就会与其他线程一同开始竞争这个对象的锁;只有当线程获取到这个锁后,线程才会继续往下执行。

  5. 调用 wait 方法的代码片段需要放在一个 synchronized 块或者 synchronized 方法中,这样才能确保线程在调用 wait 方法前已经获取到锁了

  6. 调用 notify 方法时,它会唤醒该对象等待队列中的任意一个线程,当某个线程被唤醒后,与其他线程一起竞争对象的锁。

  7. 调用 notifyAll 方法,唤醒等待队列的所有线程,这些线程被唤醒后,又会开始竞争对象的锁。

  8. 某个时刻,只有一个线程可以拥有对象的锁。

Thread.join()

一个线程 A 执行了 thread.join()语句,其含义是:当前线程 A 等待 thread 线程终止之后才从 thread.join()返回。线程 Thread 除了提供 join()方法之外,还提供了 join(long


millis)和 join(longmillis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程 thread 在给定的超时时间里没有终止,那么将会从该超时方法中返回。


当线程终止时,会调用线程自身的 notifyAll()方法,会通知所有等待在该线程对象上的线程。


每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从 join()方法返回,


package com.example.xppdemo.chapter4;
import java.util.concurrent.TimeUnit;
public class Join { public static void main(String[] args) throws Exception { Thread previous = Thread.currentThread(); for (int i = 0; i < 10; i++) {// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回 Thread thread = new Thread(new Domino(previous), String.valueOf(i)); thread.start(); previous = thread; } TimeUnit.SECONDS.sleep(5); System.out.println(Thread.currentThread().getName() + " terminate."); } static class Domino implements Runnable { private Thread thread; public Domino(Thread thread) { this.thread = thread; } public void run() { try { thread.join(); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + " terminate."); } }}
复制代码


每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回


Thread.join()源码:


public final synchronized void join(long millis)throws InterruptedException {    long base = System.currentTimeMillis();    long now = 0;
if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); }
if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } }}
复制代码


当线程终止时,会调用线程自身的 notifyAll()方法,会通知所有等待在该线程对象上的线程。可以看到 join()方法的逻辑结构与等待/通知经典范式一致,即加锁、循环和处理逻辑 3 个步骤。

等待通知超时

// 对当前对象加锁public synchronized Object get(long mills) throws InterruptedException {    long future = System.currentTimeMillis() + mills;    long remaining = mills;    // 当超时大于0并且result返回值不满足要求    while ((result == null) && remaining > 0) {        wait(remaining);remaining = future - System.currentTimeMillis();    }    return result;}
复制代码


调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。

ThreadLocal

ThreadLocal,即线程变量,是一个以 ThreadLocal 对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个 ThreadLocal 对象查询到绑定在这个线程上的一个值。


import java.util.concurrent.TimeUnit;
public class Profiler { // 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次 private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() { protected Long initialValue() { return System.currentTimeMillis(); } };
public static final void begin() { TIME_THREADLOCAL.set(System.currentTimeMillis()); }
public static final long end() { return System.currentTimeMillis() - TIME_THREADLOCAL.get(); }
public static void main(String[] args) throws Exception { Profiler.begin(); TimeUnit.SECONDS.sleep(1); System.out.println("Cost: " + Profiler.end() + " mills"); }}
复制代码


在 AOP(面向方面编程)中,可以在方法调用前的切入点执行 begin()方法,而在方法调用后的切入点执行 end()方法,这样依旧可以获得方法的执行耗时。

发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2020.02.29 加入

还未添加个人简介

评论

发布
暂无评论
线程通信_5月月更_周杰伦本人_InfoQ写作社区