likes
comments
collection
share

从有点基础开始学JUC:简谈线程间通信

作者站长头像
站长
· 阅读数 66

线程间通信

概念

线程间通信是指多个线程之间通过交换信息来实现协作和数据共享的过程。

主要为了满足以下的需求:

  1. 共享数据:多个线程需要访问和修改相同的数据结构或变量。
  2. 任务协作:多个线程需要按照一定的顺序和条件来协调执行,以完成特定的任务。
  3. 消息传递:线程之间需要通过传递消息来交换信息,以实现通信和同步。

synchronized

一种实现线程间通信的手段是synchronized,这往往也是最常见的方法

看以下示例:

public class NumTest {

    private int num = 0;

    public synchronized void incr() throws InterruptedException {
        while (num != 0) {
            wait();
        }

        ++num;

        System.out.println(Thread.currentThread().getName() +  " 执行了incr,num现在为: " + num);

        notifyAll();
    }

    public synchronized void decr() throws InterruptedException {
        while (num != 1) {
            wait();
        }

        --num;

        System.out.println(Thread.currentThread().getName() +  " 执行了decr,num现在为: " + num);

        notifyAll();
    }
    
}

我们在这里写了一个资源类,专门有一个资源num,和两个加减num的方法,然后我们在其中使用wait()使线程休眠,然后使用notifyAll()唤醒其他的线程。

注意:在这里我们使用while循环包裹住wait()方法,防止虚假唤醒,因为wait()方法有着“哪里睡着,哪里醒来“的特点,所以我们不能使用if来包裹wait()方法,而是使用while,防止虚假唤醒

虚假唤醒当线程从等待状态中被唤醒时,发现未满足其正在等待的条件时,就会发生虚假唤醒。 或者说,线程间的通信未按照我们期望的条件唤醒。例如这里的while代码改成if,就会导致if判断和wait()方法仅执行一次,当其被唤醒时出现条件不满足,但线程并不继续等待直接执行的情况

然后我们来创建线程执行的代码

public class SynchronizedTest {

    public static void main(String[] args) {

        NumTest numTest = new NumTest();

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; ++i) {
                    numTest.incr();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "线程一").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; ++i) {
                    numTest.decr();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "线程二").start();

    }

}

很简单,直接创建两个线程,然后执行。

看一下执行的结果

线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0

进程已结束,退出代码0

Lock

相较于synchronized,lock接口更加灵活,当然,也更加危险,作为一个显式锁,当程序离开被锁保护的代码块时,其不会像监视器锁那样自动释放,而需要我们手动释放锁。所以,一定要记得*在finnalylock.unlock() *!!!

下面,我们看一个示例:

public class NumTest {

    private int num = 0;

    private final Lock lock = new ReentrantLock();

    private final Condition condition = lock.newCondition();

    public void incr() throws InterruptedException {

        lock.lock();

        
        try {

            while (num != 0) {
                condition.await();
            }

            ++num;

            System.out.println(Thread.currentThread().getName() +  " 执行了incr,num现在为: " + num);
            
            condition.signalAll();
            
        } finally {
            lock.unlock();
        }
    }

    public synchronized void decr() throws InterruptedException {
        lock.lock();
        
        try {

            while (num != 1) {
                condition.await();
            }
            --num;

            System.out.println(Thread.currentThread().getName() +  " 执行了decr,num现在为: " + num);

            condition.signalAll();
            
        } finally {
            lock.unlock();
        }
    }

}

这个示例我们使用了ReentrantLock给线程加锁,同时用到了Condition接口用于实现线程的等待和唤醒。

我们接下来看看Condition接口,和Lock接口用于拓展现有的监视器锁类似,Condition接口是为了拓展同步代码块中的wait, notify机制。

传统的wait()方法调用的线程都会到同一个监视器锁的wait set中等待,但notify()和notifyAll()却可能将其全部唤醒,也就是说,可能我根本不希望那个线程唤醒,但是却因此将其唤醒,白白浪费了性能。所以,Condition接口应运而生,其只会唤醒其当前lock对象相关的线程,而不是一股脑将一大堆与此毫不相干的线程唤醒

再然后是创建线程并执行的代码:

public class LockTest {

    public static void main(String[] args) {

        NumTest numTest = new NumTest();

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; ++i) {
                    numTest.incr();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "线程一").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; ++i) {
                    numTest.decr();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "线程二").start();

    }

}

直接创建两个线程,然后执行。

看一下执行的结果:

线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0
线程一 执行了incr,num现在为: 1
线程二 执行了decr,num现在为: 0

进程已结束,退出代码0

这就是这篇文章的内容了,欢迎大家的讨论,如有错漏,也请指出,谢谢~

转载自:https://juejin.cn/post/7241908631083302969
评论
请登录