Java多线程——线程间通信

发布于:2023-01-24 ⋅ 阅读:(610) ⋅ 点赞:(0)

锁与同步

在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果被一个线程持有,那其他线程如果需要得到这个锁,就得等这个线程和这个锁释放。
在我们的线程之间,有一个同步的概念。线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。可以以解释为:线程同步是线程之间按照一定的顺序执行。
为了达到线程同步,我们可以使用锁来实现它。

public class ChapterMain3 {
    public static void main(String[] args) throws Exception {
        //锁与同步
        Object lock = new Object();
        Thread a = new Thread(() -> {
            synchronized (lock){
                for (int i = 0; i < 10; i++){
                    System.out.println("Thread A" + i);
                }
            }
        });
        a.start();
        Thread.sleep(10);
        Thread b = new Thread(() -> {
            synchronized (lock){
                for (int i = 0; i < 10; i++){
                    System.out.println("Thread B" + i);
                }
            }
        });
        b.start();
    }
}

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

等待/通知机制

前面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。而等待/通知机制是另一种方式,Java多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。

  • wait()方法:让线程进入等待状态,并释放已经获取的锁,如果没有获取锁则抛出异常。
  • notify()方法:随机唤醒一个正在等待的线程
  • notifyAll()方法:唤醒所有正在等待的线程

实现A、B线程交替打印如下:

public class ChapterMain3 {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        //等待/通知机制
        Thread a = new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println("Thread A" + i);
                    try {
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        });
        a.start();
        Thread.sleep(100);
        Thread b = new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println("Thread B" + i);
                    try {
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        b.start();
    }
}

在这里插入图片描述

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

信号量

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

Semaphore类使用

Semaphore维护了一个许可集,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire()和release()获取和释放访问许可。
使用示例:

public class ChapterMain3 {
    public static void main(String[] args) throws Exception {
        //信号量
        Semaphore semaphore = new Semaphore(1);
        Thread a = new Thread(() -> {
            try {
                semaphore.acquire();
                for (int i = 0; i < 5; i++) {
                    System.out.println("Thread A" + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                semaphore.release();
            }
        });
        a.start();
        Thread b = new Thread(() -> {
            try {
                semaphore.acquire();
                for (int i = 0; i < 5; i++) {
                    System.out.println("Thread B" + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                semaphore.release();
            }
        });
        b.start();
    }
}

在这里插入图片描述

volatile实现信号量

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

public class ChapterMain3 {
    private static volatile int signal = 0;
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        //volatile实现信号量
        Thread a = new Thread(() -> {
            while (signal < 5) {
                if (signal % 2 == 0) {
                    System.out.println("Thread A:" + signal);
                    synchronized (lock) {
                        signal++;
                    }
                }
            }
        });
        a.start();
        Thread.sleep(100);
        Thread b = new Thread(() -> {
            while (signal < 5) {
                if (signal % 2 == 1) {
                    System.out.println("Thread B:" + signal);
                    synchronized (lock) {
                        signal++;
                    }
                }
            }
        });
        b.start();
    }
}

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

管道

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

public class ChapterMain3 {
    public static void main(String[] args) throws Exception {
        PipedReader reader = new PipedReader();
        PipedWriter writer = new PipedWriter();
        writer.connect(reader);
        Thread a = new Thread(() -> {
            System.out.println("这是writer");
            try {
                writer.write("test");
            } catch (IOException exception) {
                exception.printStackTrace();
            } finally {
                try {
                    writer.close();
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        });
        a.start();
        Thread.sleep(100);
        Thread b = new Thread(() -> {
            int receive = 0;
            System.out.println("这是reader");
            try {
                while ((receive = reader.read()) != -1) {
                    System.out.println((char) receive);
                }
            } catch (IOException exception) {
                exception.printStackTrace();
            }
        });
        b.start();
    }
}

在这里插入图片描述
管道通信的应用场景:
这个很好理解。使用管道多半与I/O流相关。当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。

其它通信相关

join方法

join()方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。
有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。
如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法了。

public class ChapterMain3 {
    public static void main(String[] args) throws Exception {
        //join
        Thread join = new Thread(() -> {
            System.out.println("我是子线程,sleep一秒");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是子线程,执行完毕");
        });
        join.start();
        join.join();
        System.out.println("主线程结束");

    }
}

在这里插入图片描述

sleep方法

sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。它有这样两个方法:

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

sleep和wait区别:

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

ThreadLocal类

ThreadLocal是一个本地线程副本变量工具类。内部是一个弱引用的Map来维护。有些称ThreadLocal为线程本地变量或线程本地存储。严格来说,ThreadLocal类并不属于多线程间的通信,而是让每个线程有自己”独立“的变量,线程之间互不影响。它为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。

public class ChapterMain3 {
    public static void main(String[] args) throws Exception {
        // ThreadLocal类
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        Thread a = new Thread(() -> {
            threadLocal.set("我是线程A设置的值");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
        });
        a.start();
        Thread b = new Thread(() -> {
            threadLocal.set("我是线程B设置的值");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
        });
        b.start();
    }
}

在这里插入图片描述
可以看到,虽然两个线程使用的同一个ThreadLocal实例,但是它们各自可以存取自己当前线程的一个值。ThreadLocal作用,如果我们希望将类的某个静态变量(user ID或者transaction ID)与线程状态关联,则可以考虑使用ThreadLocal。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来进行操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。

InheritableThreadLocal

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

本文含有隐藏内容,请 开通VIP 后查看