Bootstrap

第五章 Java多线程——Java线程间的通信

5.1 锁与同步

在Java中,锁的概念都是基于对象的,所以我们又称它为对象锁。 而这个锁呢,是只能由一个线程来持有,其它线程想要持有就必须要等持有锁的线程释放锁。 就类似于一堆大男人都想和一个大美女结婚,而这个美女只能和其中一个结婚,那剩下的怎么办?就只能等到他们离婚(这就是释放锁)。

在线程之间有一个同步的概念,就是说 线程之间按照一定的顺序执行。 想要达到线程同步,可以使用锁来实现它。

无锁的程序:

/**
* @author :ls
* @date :Created in 2022/4/20 10:29
* @description:
*/
public class T1 {
    static class ThreadA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("ThreadA---> "+i);
            }
        }
    }
    static class ThreadB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("ThreadB---> "+i);
            }
        }
    }

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

输出部分结果:

ThreadA---> 91
ThreadA---> 92
ThreadA---> 93
ThreadA---> 94
ThreadA---> 95
ThreadA---> 96
ThreadA---> 97
ThreadA---> 98
ThreadB---> 25
ThreadA---> 99
ThreadB---> 26
ThreadB---> 27
ThreadB---> 28
ThreadB---> 29

结果嘛,就是A、B两个线程各自执行,而且每次输出结果都是不同的。

现在如果想要A先执行,再由B执行,怎么搞? 对 加锁!

/**
* @author :ls
* @date :Created in 2022/4/20 10:29
* @description:
*/
public class T1 {
    private static Object lock = new Object();

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

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(100);  //防止B先抢到锁
        new Thread(new ThreadB()).start();
    }
}

这里声明了⼀个名字为 lock 的对象锁。我们在 ThreadA 和 ThreadB 内需要同步的代码块里,都是用 synchronized 关键字加上了同一个对象锁 lock 。
上文我们说到了,根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放 lock ,线程B才能获得锁 lock 。

5.2 等待/通知机制

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

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

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

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

代码如下:

/**
* @author :ls
* @date :Created in 2022/4/20 10:45
* @description:
*/
public class T2 {
    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

5.3 信号量

信号量(Semaphore), 有时被称为信号灯,是在多线程环境下使用的一种设施,它负责协调各个线程,以保证它们能够正确、合理的使用公共资源。

在这里 使用 volatile 关键字来实现信号量通信。

/**
* @author :ls
* @date :Created in 2022/4/20 11:03
* @description:
*/
public class T3 {
    private static volatile int num=0;

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            while (num<5){
                if (num%2==0){
                    System.out.println("ThreadA--->" + num);
                    synchronized(this){
                        num++;
                    }
                }
            }
        }
    }
    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (num<5){
                if (num%2==1){
                    System.out.println("ThreadB--->" + num);
                    synchronized(this){
                        num++;
                    }
                }
            }
        }
    }

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

使用了以个 volatile 变量 signal 来实现了“信号量”的模型。这里需要注意的是, volatile 变量需要进行原子操作。num++ 并不是⼀个原子操作,所以我们需要使用 synchronized 给它“上锁”。

信号量应用场景:

  • 假如在⼀个停车场中,车位是我们的公共资源,线程就如同车辆,而看门的管理员就是起的“信号量”的作用。
  • 因为在这种场景下,多个线程(超过2个)需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候就可以用到信号量。
  • 其实JDK中提供的很多多线程通信工具类都是基于信号量模型的。我们会在后面第三篇的文章中介绍一些常用的通信工具类。

5.4 管道

管道是基于“管道流”的通信方式。JDK提供了 PipedWriter 、 PipedReader 、PipedOutputStream 、 PipedInputStream 。其中,前面两个是基于字符的,后面两个是基于字节流的。

代码示例:

/**
* @author :ls
* @date :Created in 2022/4/20 14:13
* @description:
*/
public class T4 {
    static class ReaderThread implements Runnable {
        private PipedReader reader;
        public ReaderThread(PipedReader reader) {
            this.reader = reader;
        }
        @Override
        public void run() {
            System.out.println("this is reader!");
            int receive = 0;
            try{
                while((receive = reader.read()) !=-1){
                    System.out.print((char) receive);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    static class WriterThread implements Runnable {
        private PipedWriter writer;
        public WriterThread(PipedWriter writer) {
            this.writer = writer;
        }
        @Override
        public void run() {
            System.out.println("this is writer!");
            int receive = 0;
            try{
                writer.write("test!!");
            }catch (Exception e){
                e.printStackTrace();
            }finally{
                try{
                    writer.close();
                }catch (Exception e1){
                    e1.printStackTrace();
                }
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        PipedReader PipedReader = new PipedReader();
        PipedWriter PipedWriter = new PipedWriter();
        PipedWriter.connect(PipedReader);  //这里注意一定要连接,才能通信


        new Thread(new ReaderThread(PipedReader)).start();
        Thread.sleep(1000);
        new Thread(new WriterThread(PipedWriter)).start();
    }
}

输出:

this is reader!
this is writer!
test!!

示例代码的执行流程:

  1. 线程ReaderThread开始执行
  2. 线程ReaderThread使用管道reader.read()进入”阻塞“
  3. 线程WriterThread开始执行
  4. 线程WriterThread用writer.write(“test”)往管道写入字符串
  5. 线程WriterThread使用writer.close()结束管道写入,并执行完毕
  6. 线程ReaderThread接受到管道输出的字符串并打印
  7. 线程ReaderThread执行完毕

管道通信应用场景:
使用管道多半和I/O流相关,当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。

5.5 其他通信相关

上面是一些线程间通信的基本原理和方法,除此外,还有一些与线程通信相关的知识点,这里一并介绍。

5.5.1 join方法

join()方法时Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成之后,在继续执行当前线程。

有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时计算,主线程往往将早于子线程结束之前结束。

如果主线程想等待子线程执行完毕后,获取子线程中的处理结果,就可以用join方法。

代码示例:

/**
* @author :ls
* @date :Created in 2022/4/20 14:39
* @description:
*/
public class T5 {
    static class ThreadA implements Runnable {
        @Override
        public void run() {
            System.out.println("1.这里是子线程, 先睡两秒");
            try {
                Thread.sleep(2000);
                System.out.println("2.这里是子线程,睡眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadA());
        thread.start();
        thread.join();
        System.out.println("3.这里是主线程!!!");
    }

}

输出结果(不加join):

3.这里是主线程!!!
1.这里是子线程, 先睡两秒
2.这里是子线程,睡眠结束

输出结果(加join):

1.这里是子线程, 先睡两秒
2.这里是子线程,睡眠结束
3.这里是主线程!!!

这里需要注意的是:join() 方法有两个重载方法,一个是 join(long), 一个是join(long,int)

实际上,通过源码可以看出,join()方法及其重载方法底层都是利用了 wait(long) 这个方法。

对于join(long int) 通过查看源码发现,底层并没有精确到纳秒,而是对第二个参数做了简单的判断和处理

5.5.2 sleep方法

sleep 方法是 Thread类的一个静态方法。 这应该也是我们最常用的方法了吧。

  • Thread,sleep(long)
  • Thread.sleep(long,int)

第⼆个⽅法貌似只对第⼆个参数做了简单的处理,没有精确到纳秒。实际上还是调⽤的第⼀个⽅法。

这里有一点是: sleep方法是不会释放锁的,而wait方法是会释放锁的!!

他们的区别(sleep方法与wait方法):----->(常见面试题)

  • wait可以指定时间,也可以不指定。但是sleep是必须指定的。
  • wait释放CPU资源,同时释放锁。sleep释放CPU资源,但是不释放锁,所以容易死锁。
  • wait必须放在同步块或同步方法中,而sleep可以在任意位置。

5.5.3 ThreadLocal类

ThreadLocal是一个本地线程副本变量工具类。内部是一个弱引用的Map来维护。

当然ThreadLocal一般也称为线程本地变量线程本地存储。严格来说,ThreadLocal类并不属于多线程间的通信,而是让每个线程有自己“独立”的变量,线程之间互相不影响。它为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。

ThreadLoacl类最常用的就是set方法和get方法。 代码实例:

/**
* @author :ls
* @date :Created in 2022/4/20 15:10
* @description:
*/
public class T6 {
    static class ThreadA implements Runnable {
        private ThreadLocal<String> threadLocal;
        public ThreadA(ThreadLocal<String> threadLocal){
            this.threadLocal = threadLocal;
        }
        @Override
        public void run() {
            threadLocal.set("A");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("ThreadA输出:"+ threadLocal.get());
        }
    }
    static class ThreadB implements Runnable {
        private ThreadLocal<String> threadLocal;
        public ThreadB(ThreadLocal<String> threadLocal){
            this.threadLocal = threadLocal;
        }
        @Override
        public void run() {
            threadLocal.set("B");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("ThreadB输出:"+ threadLocal.get());
        }
    }

    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        new Thread(new ThreadA(threadLocal)).start();
        new Thread(new ThreadB(threadLocal)).start();
    }
}

输出:

ThreadB输出:B
ThreadA输出:A

可以看到,虽然两个线程使用的同一个ThreadLocal实例(通过构造方法传入),但是它们各自可以存取自己当前线程的一个值。

那ThreadLocal有什么作用呢?如果只是单纯的想要线程隔离,在每个线程中声明一个私有变量就好了呀,为什么要使 用ThreadLocal?

如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关联,则可以考虑使 用ThreadLocal。

最常见的ThreadLocal使 用场景为 用来解决数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明⼀些私有变量来进行操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。

5.5.4 InheritableThreadLocal

InheritableThreadLocal类与ThreadLocal类稍有不同,Inheritable是继承的意思。它不仅仅是当前线程可以存取副本值,⽽且它的⼦线程也可以存取这个副本值。

;