线程同步的一些思考(二) 虚假唤醒的解决
说明
在之前**线程同步的一些思考(一)**中,其实有一些可以优化的地方,就是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)是指一个线程在没有收到显式的唤醒信号的情况下,从等待状态中被唤醒。虽然虚假唤醒的发生机制可能因操作系统和具体实现而异,但一般是由于系统或实现的内部原因导致的。
虚假唤醒的发生原理是多方面的,包括操作系统、编译器优化、硬件等。下面是一些可能导致虚假唤醒的原因:
-
系统或实现内部的信号丢失:在某些情况下,操作系统或实现内部可能存在信号丢失的问题,导致线程在没有显式唤醒的情况下被唤醒。
-
硬件中断:在某些平台和硬件架构中,硬件中断可能会导致线程的唤醒,而这些中断可能与条件变量的状态无关。
-
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 和其他同步工具来实现。