多线程间的通信

线程间的通信

@author lisiwen

@createTime 2020/08/26

1.锁与同步

​ 在java中锁的概念都是基于对象的,所以我们通常又称它为对象锁,锁与线程的关系就是一个锁只能在同一时间被一个线程拥有,其他线程需要等待锁呗释放,只有当当前线程释放了锁,其他线程才能拥有这个锁。

​ 在线程之间也有一个同步的概念,解释一下同步的概念,同步概念就仿佛是A和B抄大佬作业的时候,如果突然大佬说他作业有答案错了,需要改几道题的答案,这时候可能就会导致A和B抄出来的作业是不一致的,为了A和B能抄出来一样的作业,解决办法就是让dalao在A和B抄之前就已经修改好作业,或者说是在A和B抄完之后再修改答案。这就是线程A和线程B的同步。

​ 可以结束为:线程同步就是线程之间按照一定的顺序执行。

为了达到线程同步,可以使用锁来实现它

先看一个无锁的程序

public class NoneLock {

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Thread A " + i);
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Thread B " + i);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }
}

执行这个程序,可以看到线程A和线程B在各自独立的工作,分别打印自己的值。如下是某次运行结果

....
Thread A 48
Thread A 49
Thread B 0
Thread A 50
Thread B 1
Thread A 51
Thread A 52
....

那如果现在想先执行A再执行B怎么办呢,这个时候最简单的方式是试用一个对象锁。

public class ObjectLock {
    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread A " + i);
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread B " + i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }
}

​ 这里声明了一个lock对象锁。在线程A和线程B中需要同步的代码块中使用了synchronized关键字加上了同一个对象所lock。

​ 之前说过了根据线程之间的关系,只有一个线程可以获得锁,所以当A释放了锁之后B才能获得锁。

​ 值得注意的是这里在A线程启动了时候对当先线程sleep了10毫秒,主要是因为防止B先获得锁,因为同事start的话,A线程和B线程都有可能同时处于就绪状态,操作系统肯能会让B先执行,这样就会先输出B后输出A,与预期不符。

2.等待/通知机制

上面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。

而等待/通知机制是另一种方式。

Java多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。

notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。

前面我们讲到,一个锁同一时刻只能被一个线程持有。而假如线程A现在持有了一个锁lock并开始执行,它可以使用lock.wait()让自己进入等待状态。这个时候,lock这个锁是被释放了的。

这时,线程B获得了lock这个锁并开始执行,它可以在某一时刻,使用lock.notify(),通知之前持有lock锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。

需要注意的是,这个时候线程B并没有释放锁lock,除非线程B这个时候使用lock.wait()释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock锁。

我们用代码来实现一下:

public class WaitAndNotify {
    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadA: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadB: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

// 输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4

在这个Demo里,线程A和线程B首先打印出自己需要的东西,然后使用notify()方法叫醒另一个正在等待的线程,然后自己使用wait()方法陷入等待并释放lock锁。

需要注意的是等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。

3.信号量

DK提供了一个类似于“信号量”功能的类Semaphore。但本文不是要介绍这个类,而是介绍一种基于volatile关键字的自己实现的信号量通信。

后面会有专门的章节介绍volatile关键字,这里只是做一个简单的介绍。

volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。

比如我现在有一个需求,我想让线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。我应该怎样实现呢?

代码:

public class Signal {
    private static volatile int signal = 0;

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 0) {
                    System.out.println("threadA: " + signal);
                    signal++;
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 1) {
                    System.out.println("threadB: " + signal);
                    signal = signal + 1;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

// 输出:
threadA: 0
threadB: 1
threadA: 2
threadB: 3
threadA: 4

我们可以看到,使用了一个volatile变量signal来实现了“信号量”的模型。这里需要注意的是,volatile变量需要进行原子操作。

需要注意的是,signal++并不是一个原子操作,所以我们在实际开发中,会根据需要使用synchronized给它“上锁”,或者是使用AtomicInteger等原子类。

这种实现方式并不一定高效,本例只是演示信号量

signal++不是原子操作是因为 signal++是分为两步1. temp= signal+1 ,2. signal=temp

信号量的应用场景:

假如在一个停车场中,车位是我们的公共资源,线程就如同车辆,而看门的管理员就是起的“信号量”的作用。

因为在这种场景下,多个线程(超过2个)需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候就可以用到信号量。

其实JDK中提供的很多多线程通信工具类都是基于信号量模型的。我们会在后面第三篇的文章中介绍一些常用的通信工具类。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值