Java 并发编程学习总结:实现线程等待和唤醒的方法

本文是学习尚硅谷周阳老师《JUC并发编程》的总结(文末有链接)。

在 Java 并发编程:实现线程等待和唤醒有 3 种方法:

  • Object 类的 wait/notify 方法
  • Condition 类的 await/signal 方法
  • LockSupport 类的 park/unpark 方法

Object 类的 wait/notify 方法

实现代码:

package juc;

import java.util.concurrent.TimeUnit;

public class LockSupportDemo {
    public static void main(String[] args)
    {
        Object objectLock = new Object();

        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + " come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 被唤醒");
            }
        }, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + " come in");
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + " 发出通知");
            }
        }, "t2").start();
    }
}

注意:

  • wait 和 notify 方法必须要在同步块或同步方法里面,且成对出现使用
  • 必须先 wait 后 notify

Condition 类的 await/signal 方法

实现代码:

package juc;

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

public class LockSupportDemo {
    public static void main(String[] args)
    {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " come in");
                condition.await();
                System.out.println(Thread.currentThread().getName() + " 被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " come in");
                condition.signal();
                System.out.println(Thread.currentThread().getName() + " 发出通知");
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

注意:

  • await 和 signal方法必须先获取锁,即在 lock/unlock 对里面才能使用
  • 必须先 await 后 signal

LockSupport 类的 park/unpark 方法

上述两种方法在使用时都有如下限制:

  • 线程要先获得并持有锁,必须在锁块(synchronized 或 lock)中
  • 必须要先等待再唤醒,线程才能够被唤醒

因此才出现了 LockSupport 类,通过此类可以不受上面的限制更方便地实现线程等待和唤醒。

LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法,底层调用的是 UnSafe 类的 native 代码。

LockSupport 提供 park() 和 unpark() 方法提供阻塞线程和唤醒线程的功能。

实现代码:

package juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

public class LockSupportDemo {
    public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " 被唤醒");
        }, "t1");
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in");
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + " 发出通知");
        }, "t2").start();
    }
}

通过 LockSupport 类可以先 unpark 给 t1 线程颁发许可证,等到 t1 运行到 park 时直接放行通过,不会阻塞。

package juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

public class LockSupportDemo {
    public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + " come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " 被唤醒");
        }, "t1");
        t1.start();



        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in");
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + " 发出通知");
        }, "t2").start();
    }
}

输出:

t2 come in
t2 发出通知
t1 come in
t1 被唤醒

Process finished with exit code 0

LockSupport 和每个使用它的线程都有一个许可(permit)关联,每个线程都有一个相关的 permit ,permit 最多只有 1 个,重复调用 unpark() 也不会累积 permit。

可以通过如下代码验证:

package juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

public class LockSupportDemo {
    public static void main(String[] args)
    {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + " come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " 通过第1个park");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " 通过第2个park");
            System.out.println(Thread.currentThread().getName() + " 被唤醒");
        }, "t1");
        t1.start();



        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in");
            LockSupport.unpark(t1);
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + " 发出通知");
        }, "t2").start();
    }
}

输出:

t2 come in
t2 发出通知
t1 come in
t1 通过第1个park

// 程序阻塞到这里

虚假唤醒

实现线程等待和唤醒时需要注意是否有虚假唤醒问题。

例如下面示例代码,本意是线程 A 和 线程 C 在 number 为 0 时对其加 1,线程 B 和线程 D 在 number 为 1 时对其减 1,最后结果预期是 0。

package juc;

/**
 * 虚假唤醒Demo
 */

class share {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        if (number != 0) {
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + " : " + number);
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (number == 0) {
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + " : " + number);
        this.notifyAll();
    }
}

public class SpuriousWakeupDemo {
    public static void main(String[] args) {
        share share = new share();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

实际运行结果可能是:

A : 1
D : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
D : -1
D : -2
D : -3
D : -4
D : -5
D : -6
D : -7
D : -8
D : -9
A : -8

// 程序还可能阻塞住

出现这个问题的原因就是虚假唤醒,上述代码运行情况可能是:

  1. 线程 A 先抢到锁,将 number 加 1 ,number 为 1,通知其它线程,第 1 次方法执行结束,释放锁(因为 synchronized 是加在方法上面的,方法执行完后释放锁)

  2. 线程 C 抢到锁,判断 if (number != 0) 不满足条件,调用 this.wait() 等待,释放锁;

  3. 线程 A 抢到锁,第 2 次执行方法,判断 if (number != 0) 不满足条件,调用 this.wait() 等待,释放锁;

  4. 线程 B 抢到锁,将 number 减 1, number 为 0,通知其它线程,第 1 次方法执行结束释放锁;

  5. 线程 A 抢到锁,收到线程 B 的通知结束等待,执行 wait 后面的代码将 number 加 1, number 为 1,通知其它线程,第 2 次方法执行结束,释放锁;

  6. 线程 C 抢到锁,收到线程 A 的通知结束等待,执行 wait 后面的代码将 number 加 1,number 为 2,通知其它线程,第 2 次方法执行结束,释放锁;

    … …

    这里就出现了 number 大于 1 的情况,虽然和上面输出结果不一样,但都是不符合预期的结果。

解决方法是将判断条件 if (number == 0) 改为 while (number == 0):

package juc;

/**
 * 虚假唤醒Demo
 */

class share {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        while (number != 0) {
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + " : " + number);
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        while (number == 0) {
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + " : " + number);
        this.notifyAll();
    }
}

public class SpuriousWakeupDemo {
    public static void main(String[] args) {
        share share = new share();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

修改后的输出为:

A : 1
D : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
C : 1
B : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0
A : 1
D : 0

Process finished with exit code 0

参考

  • 尚硅谷JUC并发编程: 第 50 - 55 节 (https://www.bilibili.com/video/BV1ar4y1x727?spm_id_from=333.788.videopod.episodes&vd_source=9266b9af652d5902d068c94b9d60116f&p=50)
  • 【尚硅谷】大厂必备技术之JUC并发编程:第10节(https://www.bilibili.com/video/BV1Kw411Z7dF?p=10&spm_id_from=333.788.videopod.episodes&vd_source=9266b9af652d5902d068c94b9d60116f)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值