java线程间的通信(学习笔记)

简述

合理使用java多线程可以更好地利用服务器资源。一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当我们需要多个线程之间相互协作的时候,就需要我们掌握java线程的通信方式。

锁与同步

在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁,一个锁同一时间只能被一个线程持有

在线程之间,有一个同步的概念,可以解释为:线程同步是线程之间按照一定的顺序执行

public class Test {

    public static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException{

        new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 10; i++) {
                    System.out.println("Thread A:"+i);
                }
            }
        },"Thread A").start();
		Thread.sleep(1000);
        new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 10; i++) {
                    System.out.println("Thread B:"+i);
                }
            }
        },"Thread B").start();

    }
}

这里声明了一个名字为lock的对象锁,在ThreadAThreadB内需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock

根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放锁lock,线程B才能获得锁lock

这里在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先获得锁。因为如果同时start,线程A和线程B都是处于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行。

等待/通知机制

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

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 Test {

    public static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 10; i++) {
                    System.out.println("Thread A:"+i);
                    try {
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"Thread A").start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (lock){
                for (int i = 0; i < 10; i++) {
                    System.out.println("Thread B:"+i);
                    try {
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"Thread B").start();

    }
}

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

信号量

JDK 提供了一个类似于“信号量”功能的类Semaphore

这里介绍的一种基于volatile关键字实现的信号量通信。

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

示列代码:

public class Test {

    public static volatile int signal = 0;

    public static void main(String[] args) throws InterruptedException {
        //让线程A输出0,线程B输出1,线程A输出2,线程B输出3,依次类推
        int b = 1;
        new Thread(()->{
            while (signal<10){
                if(signal%2==0){
                    System.out.println("Thread A:"+signal);
                    signal++;
                }
            }
        },"Thread A").start();
        Thread.sleep(1000);
        new Thread(()->{
            while (signal<10){
                if(signal%2==1){
                    System.out.println("Thread B:"+signal);
                    signal= signal + 1;
                }
            }
        },"Thread B").start();

    }

}

signal++并不是一个原子操作,所以在实际开发中,会根据需要使用synchronized给它上锁,或者使用AtomicInteger等原子类。并且上面的程序也并不是线程安全的,因为执行while语句后,可能当前线程就暂停等待时间片了,等线程醒来,可能signal已经大于等于5了。

信号量的应用场景

假如在一个停车场中,车位是我们的公共资源,线程就如同车辆,而看门的管理员就是起的“信号量”的作用。因为在这种场景下,多个线程(超过2个)需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候可以用到信号量。

管道

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

基于字符的示列代码:

public class Test {
    public static void main(String[] args) throws InterruptedException, IOException {
        PipedReader reader = new PipedReader();
        PipedWriter writer = new PipedWriter();
        //这里要连接才能通信
        writer.connect(reader);

        new Thread(()->{
            System.out.println("this is read");
            int receive = 0;
            try {
                while ((receive=reader.read())!=-1){
                    System.out.print((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        },"Thread reader").start();

        Thread.sleep(1000);

        new Thread(()->{
            System.out.println("this is write");
            try {
                writer.write("test");
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        },"Thread writer").start();
    }
}
//输出
this is read
this is write
test

示列的代码的执行流程:

1.线程Thread reader开始执行

2.线程Thread reader使用管道reader.read()进入“阻塞”

3.线程Thread writer开始执行

4.线程Thread writer用writer.write(“test”)往管道写入字符串

5.线程Thread writer使用writer.close()结束管道写入,并执行完毕

6.线程Thread reader接收到管道输出的 字符串并打印

7.线程Thread reader执行完毕

管道通信的应用场景:

使用管道多半与I/O流相关。当一个线程需要向另一个线程发送一个信息(比如字符串或文件)时,就需要使用管道通信了。

其它通信相关

join方法

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

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

如果主线程想等待子线程执行完毕后,获得子线程的处理完的某个数据,就要用到join方法了。

示列代码:

public class Test {

    public static void main(String[] args) throws IOException, InterruptedException {

        Thread thread = new Thread(() -> {
            System.out.println("test");
        });
        thread.start();
        thread.join();
        System.out.println("如果没加join方法,会先被打印出来!");
    }
}
  • 注意join()方法有两个重载方法,一个是join(long millis),一个是join(long millis,int nanos)。

  • join()方法及其重载方法底层都是利用了wait(long)这个方法。

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

    //源码 millis:毫秒 – 以毫秒为单位的等待时间 nanos: 0-999999额外的纳秒等待
    public final synchronized void join(long millis, int nanos)
        throws InterruptedException {
    
            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    
            if (nanos < 0 || nanos > 999999) {
                throw new IllegalArgumentException(
                                    "nanosecond timeout value out of range");
            }
    
            if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
                millis++;
            }
    
            join(millis);
        }
    

sleep方法

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

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

区别:

  • sleep方法不会释放当前的锁,而wait方法会
  • wait可以指定时间,也可以不指定;而sleep必须指定时间
  • wait释放CPU资源,同时释放锁;sleep释放CPU资源,但是不释放锁,所以容易死锁
  • wait必须放在同步块或同步方法中;而sleep可以在任意位置
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值