线程同步的一些思考(二) 虚假唤醒的解决

文章讨论了Java中线程同步的重要性和虚假唤醒的问题,提出使用while循环代替if判断以避免虚假唤醒。同时,介绍了使用ReentrantLock、volatile、CountDownLatch、CyclicBarrier、Semaphore和Exchanger等不同方式实现线程同步和交替执行的示例。

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

线程同步的一些思考(二) 虚假唤醒的解决

说明

在之前**线程同步的一些思考(一)**中,其实有一些可以优化的地方,就是if的判断可以改成while

为什么呢,因为虚假唤醒情况的存在

代码示例如下:



public class PrintUtil {

  private static final Object lock = new Object();
  private static boolean isPrintA = false;

  public static void main(String[] args) throws InterruptedException {
    Thread threadA = new Thread(new PrintA());
    Thread threadB = new Thread(new PrintB());

//    extractedA(threadA, threadB);
    extractedB(threadA, threadB);
  }

  private static void extractedA(Thread threadA, Thread threadB) throws InterruptedException {
    threadA.start();
    Thread.sleep(1000);
    threadB.start();
  }
  private static void extractedB(Thread threadA, Thread threadB) throws InterruptedException {
    threadB.start();
    Thread.sleep(1000);
    threadA.start();
  }

  static class PrintA implements Runnable {

    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        synchronized (lock) {
          while (isPrintA) {
            try {
              lock.wait();
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
          System.out.println("A");
          isPrintA = true;
          lock.notify();
        }
      }
    }
  }

  static class PrintB implements Runnable {

    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        synchronized (lock) {
          while (!isPrintA) {
            try {
              lock.wait();
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
          System.out.println("B");
          isPrintA = false;
          lock.notify();
        }
      }
    }
  }
}

什么是虚假唤醒

虚假唤醒(Spurious Wakeup)是指一个线程在没有收到显式的唤醒信号的情况下,从等待状态中被唤醒。虽然虚假唤醒的发生机制可能因操作系统和具体实现而异,但一般是由于系统或实现的内部原因导致的。

虚假唤醒的发生原理是多方面的,包括操作系统、编译器优化、硬件等。下面是一些可能导致虚假唤醒的原因:

  1. 系统或实现内部的信号丢失:在某些情况下,操作系统或实现内部可能存在信号丢失的问题,导致线程在没有显式唤醒的情况下被唤醒。

  2. 硬件中断:在某些平台和硬件架构中,硬件中断可能会导致线程的唤醒,而这些中断可能与条件变量的状态无关。

  3. JVM 优化:JVM 可能会对等待线程进行一些优化,例如对等待线程进行批处理或重排序。这些优化可能会导致线程在没有显式唤醒的情况下被唤醒。

虚假唤醒的场景并不常见,但以下是一个简单的示例来说明虚假唤醒的可能性:

public class SpuriousWakeUpExample {
    private static final Object lock = new Object();
    private static boolean condition = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new WaitThread());
        Thread thread2 = new Thread(new NotifyThread());

        thread1.start();
        Thread.sleep(1000);
        thread2.start();
    }

    static class WaitThread implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                while (!condition) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("Condition is true");
            }
        }
    }

    static class NotifyThread implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                condition = true;
                lock.notify();
            }
        }
    }
}

在上述示例中,WaitThread 线程等待条件变量 condition 为 true,而 NotifyThread 线程负责将 condition 设置为 true 并调用 notify() 方法。

尽管看起如此,WaitThread 在条件不满足时进入等待状态,并在被 NotifyThread 唤醒后打印消息。然而,由于可能存在虚假唤醒,WaitThread 有时可能会在条件不满足的情况下被唤醒,导致消息的打印是不准确的。

虚假唤醒的发生是由于操作系统或 JVM 内部的实现细节造成的,具体的场景和原因可能因不同的系统和环境而异。尽管虚假唤醒是可能的,但它在实践中相对较少发生,特别是在合理使用条件变量并采用正确的同步机制的情况下。

为了防止虚假唤醒,常见的做法是在等待状态下使用 while 循环进行条件检查,而不是使用 if 语句。这样可以确保在唤醒后再次检查条件,以避免虚假唤醒所带来的问题。

需要注意的是,在使用 wait()、notify() 和 notifyAll() 方法时,必须在 synchronized 块中进行调用,并且只能在持有同一个对象的锁的线程之间进行通信。

其它线程同步方式的实现

ReentrantLock


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class PrintUtil {
  private static ReentrantLock lock = new ReentrantLock();
  private static boolean isPrintA = true;
  private static Condition condition = lock.newCondition();

  public static void main(String[] args) throws InterruptedException {
    Thread threadA = new Thread(new PrintA());
    Thread threadB = new Thread(new PrintB());

    threadB.start();
    Thread.sleep(1000);
    threadA.start();
  }

  static class PrintA implements Runnable {
    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        lock.lock();
        try {
          while (!isPrintA) {
            condition.await();
          }
          System.out.println("A");
          isPrintA = false;
          condition.signal();
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          lock.unlock();
        }
      }
    }
  }

  static class PrintB implements Runnable {
    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        lock.lock();
        try {
          while (isPrintA) {
            condition.await();
          }
          System.out.println("B");
          isPrintA = true;
          condition.signal();
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          lock.unlock();
        }
      }
    }
  }
}
volatile

Atomics原子类也是类似,不进行补充了

Thread.yield() 是一个静态方法,它的作用是告诉线程调度器当前线程愿意放弃当前的 CPU 时间片,让其他具有相同优先级的线程有机会运行。调用 Thread.yield() 方法并不会阻塞当前线程,而是使当前线程从运行状态转换为可运行状态,然后等待线程调度器重新选择执行哪个线程

public class PrintUtil {
    private static volatile boolean isPrintA = true;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new PrintA());
        Thread threadB = new Thread(new PrintB());

        threadB.start();
        Thread.sleep(1000);
        threadA.start();
    }

    static class PrintA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                while (!isPrintA) {
                    Thread.yield();
                }
                System.out.println("A");
                isPrintA = false;
            }
        }
    }

    static class PrintB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                while (isPrintA) {
                    Thread.yield();
                }
                System.out.println("B");
                isPrintA = true;
            }
        }
    }
}
CountDownLatch
import java.util.concurrent.CountDownLatch;

public class PrintUtil {
    private static CountDownLatch latchA = new CountDownLatch(1);
    private static CountDownLatch latchB = new CountDownLatch(1);

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new PrintA());
        Thread threadB = new Thread(new PrintB());

        threadA.start();
        threadB.start();

        // 通过释放第一个 latch,使 PrintA 线程先执行
        latchA.countDown();
        threadA.join();
        threadB.join();
    }

    static class PrintA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    latchA.await(); // 等待 latchA 释放
                    System.out.println("A");
                    latchA = new CountDownLatch(1); // 重置 latchA
                    latchB.countDown(); // 通知 PrintB 线程可以执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class PrintB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    latchB.await(); // 等待 latchB 释放
                    System.out.println("B");
                    latchB = new CountDownLatch(1); // 重置 latchB
                    latchA.countDown(); // 通知 PrintA 线程可以执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在上述示例中,我们使用两个 CountDownLatch 对象 latchA 和 latchB 来控制线程的执行顺序。PrintA 线程负责打印 “A”,在每次打印完成后释放 latchA,然后等待 latchB 的释放。PrintB 线程负责打印 “B”,在每次打印完成后释放 latchB,然后等待 latchA 的释放。

通过适当的释放和等待 CountDownLatch,可以实现线程的交替打印。在 main 方法中,我们先释放 latchA,使 PrintA 线程先执行,然后通过 join() 方法等待两个线程执行完毕。

CyclicBarrier
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class PrintUtil {
    private static CyclicBarrier barrier = new CyclicBarrier(2);

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new PrintA());
        Thread threadB = new Thread(new PrintB());

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();
    }

    static class PrintA implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println("A");
                    barrier.await(); // 等待其他线程到达栅栏
                    barrier.reset(); // 重置栅栏
                }
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    static class PrintB implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    barrier.await(); // 等待其他线程到达栅栏
                    System.out.println("B");
                    barrier.reset(); // 重置栅栏
                }
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

在上述示例中,我们创建了一个 CyclicBarrier 对象 barrier,并将参与的线程数设置为 2。PrintA 和 PrintB 线程分别负责打印 “A” 和 “B”,在每次打印完成后调用 barrier.await() 方法等待其他线程到达栅栏。当所有线程都到达栅栏后,它们会被释放并继续执行,然后重置栅栏。

在 main 方法中,我们启动两个线程并等待它们执行完毕。

需要注意的是,CyclicBarrier 是可重用的栅栏,每次到达栅栏的线程都会被释放并继续执行,而栅栏的初始状态是由参与的线程数确定的。因此,在每次打印完成后,我们需要显式地调用 barrier.reset() 方法来重置栅栏,以便下一轮的交替打印。

使用 CyclicBarrier 可以实现线程的有序执行和同步,确保线程按照指定的顺序交替执行。

Semaphore
import java.util.concurrent.Semaphore;

public class PrintUtil {
    private static Semaphore semaphoreA = new Semaphore(1);
    private static Semaphore semaphoreB = new Semaphore(0);

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new PrintA());
        Thread threadB = new Thread(new PrintB());

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();
    }

    static class PrintA implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    semaphoreA.acquire(); // 获取 semaphoreA 的许可
                    System.out.println("A");
                    semaphoreB.release(); // 释放 semaphoreB 的许可
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class PrintB implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    semaphoreB.acquire(); // 获取 semaphoreB 的许可
                    System.out.println("B");
                    semaphoreA.release(); // 释放 semaphoreA 的许可
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在上述示例中,我们创建了两个 Semaphore 对象 semaphoreA 和 semaphoreB,并初始化它们的许可数。PrintA 线程负责打印 “A”,在每次打印完成后调用 semaphoreB.release() 方法释放 semaphoreB 的许可,然后等待 semaphoreA 的许可。PrintB 线程负责打印 “B”,在每次打印完成后调用 semaphoreA.release() 方法释放 semaphoreA 的许可,然后等待 semaphoreB 的许可。

通过适当的获取和释放 Semaphore 的许可,可以实现线程的交替打印。在 main 方法中,我们启动两个线程并等待它们执行完毕。

使用 Semaphore 可以控制线程的并发数量,实现线程间的同步和协调。在本例中,semaphoreA 和 semaphoreB 的许可数都是 1,保证了线程的交替执行。

Exchanger
import java.util.concurrent.Exchanger;

public class PrintUtil {
    private static Exchanger<Boolean> exchanger = new Exchanger<>();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new PrintA());
        Thread threadB = new Thread(new PrintB());

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();
    }

    static class PrintA implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println("A");
                    exchanger.exchange(true); // 交换数据并等待对方线程打印
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class PrintB implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    exchanger.exchange(false); // 交换数据并等待对方线程打印
                    System.out.println("B");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在上述示例中,我们创建了一个 Exchanger 对象 exchanger,用于交换线程的打印信号。PrintA 线程负责打印 “A”,在每次打印完成后调用 exchanger.exchange(true) 方法与 PrintB 线程进行数据交换,并等待 PrintB 线程打印。PrintB 线程负责打印 “B”,在每次打印完成后调用 exchanger.exchange(false) 方法与 PrintA 线程进行数据交换,并等待 PrintA 线程打印。

通过 Exchanger 的交换机制,可以实现线程的交替打印。在 main 方法中,我们启动两个线程并等待它们执行完毕。

需要注意的是,Exchanger 只能在两个线程之间进行数据交换,因此适用于交替执行的场景。如果有多个线程需要交替执行,可以结合使用 Exchanger 和其他同步工具来实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值