侧边栏壁纸
博主头像
再见理想博主等级

只争朝夕,不负韶华

  • 累计撰写 112 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

多线程通信方式

再见理想
2022-06-30 / 0 评论 / 0 点赞 / 332 阅读 / 1,864 字

问题

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作,即多个线程在操作同一份数据时, 避免对同一共享变量的争夺。 线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。

实现线程通信的方式:

  • 使用 volatile 关键字
  • 使用 Object 类的 wait()/notify()
  • 使用 JUC 工具类 CountDownLatch
  • 使用 ReentrantLock 结合 Condition
  • 基本 LockSupport 实现线程间的阻塞和唤醒

一、使用 volatile 关键字

基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想。大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式

public class VolatileSyncTest {

    static volatile boolean notice = false;

    public static void main(String[] args) {
        // 定义线程A
        Thread threadA = new Thread(() -> {
            System.out.println("线程A开始执行");
            notice = true;
            System.out.println("线程A执行完成");
        });

        // 定义线程B,须等待notice=true时再执行
        Thread threadB = new Thread(() -> {
            System.out.println("线程B开始执行");
            while (true) {
                if (notice) {
                    System.out.println("线程B收到通知,开始执行自己的业务...");
                    break;
                }
            }
            System.out.println("线程B执行完成");
        });

        // 启动测试 先启动线程B,再启动线程A
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }
}

执行结果打印:

线程B开始执行
线程A开始执行
线程A执行完成
线程B收到通知,开始执行自己的业务...
线程B执行完成

二、使用 Object 类的 wait/notify

wait/notify 是基于 monitor 实,也就是 synchronized 内置锁。当调用 wait 方法时,线程进入 WaitSet 中;当被 notify 唤醒时,线程重新进入 EntryList,等待竞争锁。

public class WaitNotifyTest {
    public static void main(String[] args) {
        //定义一个对象
        Object o = new Object();

        // 定义线程A
        Thread threadA = new Thread(() -> {
            synchronized (o) {
                System.out.println("线程A开始执行");
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程A继续执行");
            }
        });

        // 定义线程B
        Thread threadB = new Thread(() -> {
            synchronized (o) {
                System.out.println("线程B开始执行");
                o.notify();
            }
        });

        // 测试 先启动线程A,1s后再启动线程B
        threadA.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadB.start();
    }
}

由输出结果,在线程 A 发出 notify() 唤醒通知之后,依然是走完了自己线程的业务之后,线程 B 才开始执行,正好说明 notify() 不释放锁,而 wait() 释放锁。

执行结果打印:

线程A开始执行
线程B开始执行
线程A继续执行

三、使用JUC工具类 CountDownLatch

jdk1.5 之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了并发编程代码的书写,CountDownLatch 基于 AQS 框架,相当于也是维护了一个线程间共享变量 state。

public class CountDownLatchSync {
    public static void main(String[] args) throws InterruptedException {
        //定义一个线程池和CountDownLatch
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CountDownLatch countDownLatch = new CountDownLatch(10);

        //定义线程A
        Thread threadA = new Thread(() -> {
            System.out.println("线程A开始执行");
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("countDownLatch count=" + countDownLatch.getCount());
            System.out.println("线程A继续执行");
        });
        //测试 先启动线程A,再new 10个线程,countDownLatch--操作
        threadA.start();
        System.out.println("countDownLatch count=" + countDownLatch.getCount());
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                System.out.println("线程:" + Thread.currentThread().getName() + "开始执行,countDownLatch--");
                countDownLatch.countDown();
            });
        }
    }
}

执行结果打印:

countDownLatch count=10
线程A开始执行
线程:pool-1-thread-1开始执行,countDownLatch--
线程:pool-1-thread-2开始执行,countDownLatch--
线程:pool-1-thread-4开始执行,countDownLatch--
线程:pool-1-thread-5开始执行,countDownLatch--
线程:pool-1-thread-3开始执行,countDownLatch--
线程:pool-1-thread-6开始执行,countDownLatch--
线程:pool-1-thread-7开始执行,countDownLatch--
线程:pool-1-thread-8开始执行,countDownLatch--
线程:pool-1-thread-9开始执行,countDownLatch--
线程:pool-1-thread-10开始执行,countDownLatch--
countDownLatch count=0
线程A继续执行

四、使用 ReentrantLock 结合 Condition

public class ReentrankLockSync {
    public static void main(String[] args) throws InterruptedException {
        // 定义ReentrankLock和Condition
        // Condition对象:让线程在合适的时间等待,或在某特定时刻得到通知,继续执行
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        // 定义线程A
        Thread threadA = new Thread(() -> {
            System.out.println("线程A开始执行," + DateUtil.now());
            lock.lock();
            System.out.println("线程A获得锁," + DateUtil.now());
            try {
                condition.await();
                System.out.println("线程A继续执行," + DateUtil.now());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
            System.out.println("线程A释放锁," + DateUtil.now());
        });
        // 测试
        threadA.start();
        System.out.println("主线程睡眠2s," + DateUtil.now());
        Thread.sleep(2000);
        lock.lock();
        System.out.println("主线程获得锁,唤醒等待线程," + DateUtil.now());
        condition.signal();
        System.out.println("主线程睡眠3s后释放锁," + DateUtil.now());
        Thread.sleep(3000);
        lock.unlock();
    }
}

这种方式使用起来并不是很好,代码编写复杂,而且线程 B 在被 A 唤醒之后由于没有获取锁还是不能立即执行,也就是说,A 在唤醒操作之后,并不释放锁。这种方法跟 Object 的 wait()/notify() 一样。

执行结果打印:

主线程睡眠2s,2022-06-30 11:35:05
线程A开始执行,2022-06-30 11:35:05
线程A获得锁,2022-06-30 11:35:05
主线程获得锁,唤醒等待线程,2022-06-30 11:35:07
主线程睡眠3s后释放锁,2022-06-30 11:35:07
线程A继续执行,2022-06-30 11:35:10
线程A释放锁,2022-06-30 11:35:10

五、基本 LockSupport 实现线程间的阻塞和唤醒

LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。

线程阻塞需要消耗凭证(permit),这个凭证最多只有1个!当调用 park 方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;

public class LockSupport2Sync {
    public static void main(String[] args) throws InterruptedException {
        // 定义线程A
        Thread threadA = new Thread(() -> {
            System.out.println("线程A开始执行,进入等待," + DateUtil.now());
            LockSupport.park();
            System.out.println("线程A继续执行,进入二次等待" + DateUtil.now());
            LockSupport.park();
            System.out.println("线程A继续执行," + DateUtil.now());
        });
        threadA.start();
        System.out.println("主线程睡眠3s,生成一个凭证," + DateUtil.now());
        Thread.sleep(3000);
        LockSupport.unpark(threadA);
        System.out.println("主线程睡眠3s,生成一个凭证," + DateUtil.now());
        Thread.sleep(3000);
        LockSupport.unpark(threadA);
    }
}

执行结果打印:

主线程睡眠3s,生成一个凭证,2022-06-30 14:16:04
线程A开始执行,进入等待,2022-06-30 14:16:04
主线程睡眠3s,生成一个凭证,2022-06-30 14:16:07
线程A继续执行,进入二次等待2022-06-30 14:16:07
线程A继续执行,2022-06-30 14:16:10
0

评论区