【Java进阶】Java线程间通讯的几种方式

一、使用同一个共享变量控制

1、Synchronized、wait、notify

public class Demo1 {

    private final List<Integer> list =new ArrayList<>();

    public static void main(String[] args) {
        Demo1 demo =new Demo1();
        new Thread(()->{
            for (int i=0;i<10;i++){
                synchronized (demo.list){
                    if(demo.list.size()%2==1){
                        try {
                            demo.list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    demo.list.add(i);
                    System.out.print(Thread.currentThread().getName());
                    System.out.println(demo.list);
                    demo.list.notify();
                }
            }

        }).start();

        new Thread(()->{
            for (int i=0;i<10;i++){
                synchronized (demo.list){
                    if(demo.list.size()%2==0){
                        try {
                            demo.list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    demo.list.add(i);
                    System.out.print(Thread.currentThread().getName());
                    System.out.println(demo.list);
                    demo.list.notify();
                }
            }
        }).start();
    }
}

这段代码演示了如何使用synchronizedwaitnotify来实现两个线程间的通信,以确保它们交替地向一个ArrayList中添加数字。以下是代码的详细解释:

  1. 类定义:

    • Demo1类中有一个私有的、不可变的(由final修饰)成员变量list,它是ArrayList<Integer>类型的。
  2. 主函数:

    • main方法中,首先创建了Demo1的一个实例demo
    • 然后启动了两个线程,每个线程都执行一个特定的任务。
  3. 第一个线程的任务:

    • 使用一个for循环,循环10次。
    • 在每次循环中,首先获得demo.list的锁,这是通过synchronized (demo.list)实现的。
    • 检查当前列表的大小是否为奇数(通过demo.list.size()%2==1)。如果是,则调用demo.list.wait()使当前线程进入等待状态,并释放demo.list的锁,这样其他线程可以获取该锁并执行其任务。
    • 当线程从等待状态被唤醒时(通过另一个线程的notify调用),它会继续执行,并将当前的数字添加到列表中。
    • 打印当前线程的名称和更新后的列表。
    • 通过调用demo.list.notify()唤醒可能正在等待的另一个线程。
  4. 第二个线程的任务:

    • 它的工作方式与第一个线程非常相似,但有一个关键的区别:它检查列表的大小是否为偶数,并在这种情况下使线程进入等待状态。
  5. 交替执行:

    • 由于两个线程的工作方式,它们将交替地向列表中添加数字。当一个线程发现列表的大小是其期望的(奇数或偶数)时,它会暂停并等待另一个线程添加一个数字。然后,它会被另一个线程的notify调用唤醒,继续其执行,并再次使另一个线程等待。
  6. 注意事项:

    • 使用waitnotify时,必须在同步块或方法中这样做,否则会抛出IllegalMonitorStateException
    • 当多个线程可能访问共享资源(在这里是demo.list)时,使用同步是必要的,以确保数据的完整性和一致性。
    • 虽然在这个特定的例子中只有两个线程,但这种方法可以扩展到更多的线程,只要它们遵循相同的通信和同步协议。
  7. 潜在问题:

    • 这个代码可能存在一个潜在的问题,即“假唤醒”。理论上,一个线程可能会无故地(或由于系统中的其他原因)从wait方法中唤醒,即使没有其他线程明确地调用了notifynotifyAll。为了避免这种情况导致的问题,通常在wait调用周围使用一个循环来检查预期的条件是否仍然成立。如果条件不满足,则继续等待。这通常被称为“条件变量”的使用模式。

2、Lock、Condition

import java.util.ArrayList;  
import java.util.List;  
import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class Task {  
    // 创建一个可重入锁,用于同步访问共享资源(即列表)  
    private final Lock lock = new ReentrantLock();  
    // 创建两个条件变量,一个用于表示列表未满,一个用于表示列表非空  
    private final Condition notFull = lock.newCondition();  
    private final Condition notEmpty = lock.newCondition();  
  
    // 定义列表的最大容量  
    private static final int MAX_SIZE = 10;  
    // 创建一个ArrayList作为共享资源,用于在两个线程之间传递数据  
    private final List<String> list = new ArrayList<>(MAX_SIZE);  
  
    // add方法用于向列表中添加元素  
    public void add() {  
        for (int i = 0; i < 10; i++) {  
            lock.lock(); // 获取锁,开始同步代码块  
            try {  
                // 如果列表已满,则当前线程等待,直到其他线程从列表中移除元素  
                while (list.size() == MAX_SIZE) {  
                    notFull.await(); // 等待列表不满的条件成立  
                }  
                // 模拟耗时操作(比如网络请求或数据处理)  
                Thread.sleep(100);  
                // 向列表中添加一个新元素,并打印相关信息  
                list.add("add " + (i + 1));  
                System.out.println("The list size is " + list.size());  
                System.out.println("The add thread is " + Thread.currentThread().getName());  
                System.out.println("-------------");  
                // 通知可能在等待的移除线程,现在列表不为空,可以执行移除操作了  
                notEmpty.signal();  
            } catch (InterruptedException e) {  
                // 打印异常信息,实际开发中可能需要更复杂的错误处理逻辑  
                e.printStackTrace();  
            } finally {  
                lock.unlock(); // 释放锁,允许其他线程访问同步代码块  
            }  
        }  
    }  
  
    // sub方法用于从列表中移除元素  
    public void sub() {  
        for (int i = 0; i < 10; i++) {  
            lock.lock(); // 获取锁,开始同步代码块  
            try {  
                // 如果列表为空,则当前线程等待,直到其他线程向列表中添加元素  
                while (list.isEmpty()) {  
                    notEmpty.await(); // 等待列表非空的条件成立  
                }  
                // 模拟耗时操作(比如网络请求或数据处理)  
                Thread.sleep(100);  
                // 从列表中移除第一个元素,并打印相关信息  
                list.remove(0);  
                System.out.println("The list size is " + list.size());  
                System.out.println("The sub thread is " + Thread.currentThread().getName());  
                System.out.println("-------------");  
                // 通知可能在等待的添加线程,现在列表不满,可以执行添加操作了  
                notFull.signal();  
            } catch (InterruptedException e) {  
                // 打印异常信息,实际开发中可能需要更复杂的错误处理逻辑  
                e.printStackTrace();  
            } finally {  
                lock.unlock(); // 释放锁,允许其他线程访问同步代码块  
            }  
        }  
    }  
  
    // main方法作为程序的入口点,创建Task对象并启动两个线程来执行add和sub方法  
    public static void main(String[] args) {  
        Task task = new Task(); // 创建Task对象,它包含共享资源和同步机制  
        // 使用Lambda表达式和方法引用启动两个线程,分别执行add和sub方法,并为它们设置名称以便区分输出中的信息来源  
        new Thread(task::add, "AddThread").start(); // 启动添加线程  
        new Thread(task::sub, "SubThread").start(); // 启动移除线程  
    }  
}

这段代码定义了一个名为Task的类,它主要实现了线程安全的列表添加和移除操作。类内部使用了java.util.concurrent.locks包下的ReentrantLock可重入锁以及相关的Condition条件变量来同步访问共享资源(即一个ArrayList)。

Task类中,有两个主要的方法:addsubadd方法用于向列表中添加元素,而sub方法用于从列表中移除元素。这两个方法在被调用时都需要获取锁,以确保同一时间只有一个线程可以访问共享资源。

当添加线程调用add方法时,它首先检查列表是否已满。如果已满,则通过调用notFull.await()使当前线程等待,直到其他线程从列表中移除元素并发出通知。一旦列表不满,添加线程就会向列表中添加一个新元素,并通过调用notEmpty.signal()通知可能在等待的移除线程。

类似地,当移除线程调用sub方法时,它首先检查列表是否为空。如果为空,则通过调用notEmpty.await()使当前线程等待,直到其他线程向列表中添加元素并发出通知。一旦列表非空,移除线程就会从列表中移除一个元素,并通过调用notFull.signal()通知可能在等待的添加线程。

这种使用锁和条件变量的方式实现了线程间的同步和通信,确保了共享资源(即列表)在任何时候都不会被多个线程同时修改,从而避免了数据竞争和不一致的问题。同时,通过条件变量的等待和通知机制,有效地协调了添加线程和移除线程的执行顺序,使得它们能够按照预期的方式交替进行添加和移除操作。

3、利用volatile

volatile修饰的变量值直接存在主内存里面,子线程对该变量的读写直接写住内存,而不是像其它变量一样在local thread里面产生一份copy。volatile能保证所修饰的变量对于多个线程可见性,即只要被修改,其它线程读到的一定是最新的值。

public class Demo2 {
    private volatile List<Integer> list =new ArrayList<>();
    public static void main(String[] args) {
        Demo2 demo =new Demo2();
        new Thread(()->{
            for (int i=0;i<10;i++){
                    demo.list.add(i);
                    System.out.print(Thread.currentThread().getName());
                    System.out.println(demo.list);
            }

        }).start();

        new Thread(()->{
            for (int i=0;i<10;i++){
                    demo.list.add(i);
                    System.out.print(Thread.currentThread().getName());
                    System.out.println(demo.list);
                }
        }).start();
    }
}

4、利用AtomicInteger

和volatile类似

二、PipedInputStream、PipedOutputStream

这里用流在两个线程间通信,但是Java中的Stream是单向的,所以在两个线程中分别建了一个input和output

public class PipedDemo {

    private final PipedInputStream inputStream1;
    private final PipedOutputStream outputStream1;
    private final PipedInputStream inputStream2;
    private final PipedOutputStream outputStream2;

    public PipedDemo(){
        inputStream1 = new PipedInputStream();
        outputStream1 = new PipedOutputStream();
        inputStream2 = new PipedInputStream();
        outputStream2 = new PipedOutputStream();
        try {
            inputStream1.connect(outputStream2);
            inputStream2.connect(outputStream1);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    /**程序退出时,需要关闭stream*/
    public void shutdown() throws IOException {
        inputStream1.close();
        inputStream2.close();
        outputStream1.close();
        outputStream2.close();
    }


    public static void main(String[] args) throws IOException {
        PipedDemo demo =new PipedDemo();
        new Thread(()->{
            PipedInputStream in = demo.inputStream2;
            PipedOutputStream out = demo.outputStream2;

            for (int i = 0; i < 10; i++) {
                try {
                    byte[] inArr = new byte[2];
                    in.read(inArr);
                    System.out.print(Thread.currentThread().getName()+": "+i+" ");
                    System.out.println(new String(inArr));
                    while(true){
                        if("go".equals(new String(inArr)))
                            break;
                    }
                    out.write("ok".getBytes());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }).start();

        new Thread(()->{
            PipedInputStream in = demo.inputStream1;
            PipedOutputStream out = demo.outputStream1;

            for (int i = 0; i < 10; i++) {
                try {
                    out.write("go".getBytes());
                    byte[] inArr = new byte[2];
                    in.read(inArr);
                    System.out.print(Thread.currentThread().getName()+": "+i+" ");
                    System.out.println(new String(inArr));
                    while(true){
                        if("ok".equals(new String(inArr)))
                            break;
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }).start();
//        demo.shutdown();
    }
}

输出

Thread-0: 0 go
Thread-1: 0 ok
Thread-0: 1 go
Thread-1: 1 ok
Thread-0: 2 go
Thread-1: 2 ok
Thread-0: 3 go
Thread-1: 3 ok
Thread-0: 4 go
Thread-1: 4 ok
Thread-0: 5 go
Thread-1: 5 ok
Thread-0: 6 go
Thread-1: 6 ok
Thread-0: 7 go
Thread-1: 7 ok
Thread-0: 8 go
Thread-1: 8 ok
Thread-0: 9 go
Thread-1: 9 ok

三、利用BlockingQueue

BlockingQueue定义的常用方法如下:

  • add(Object):把Object加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则抛出异常。
  • offer(Object):表示如果可能的话,将Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。
  • put(Object):把Object加到BlockingQueue里,如果BlockingQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里有空间再继续。
  • poll(time):获取并删除BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。当不传入time值时,立刻返回。
  • peek():立刻获取BlockingQueue里排在首位的对象,但不从队列里删除,如果队列为空,则返回null。
  • take():获取并删除BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的对象被加入为止。

BlockingQueue有四个具体的实现类:

  • ArrayBlockingQueue:数组阻塞队列,规定大小,其构造函数必须带一个int参数来指明其大小。其所含的对象是以FIFO(先入先出)顺序排序的。
  • LinkedBlockingQueue:链阻塞队列,大小不定,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定。其所含的对象是以FIFO顺序排序的。
  • PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
  • SynchronousQueue:特殊的BlockingQueue,它的内部同时只能够容纳单个元素,对其的操作必须是放和取交替完成的。
  • DelayQueue:延迟队列,注入其中的元素必须实现 java.util.concurrent.Delayed 接口

所有BlockingQueue的使用方式类似,以下例子一个线程写入,一个线程读取,操作的是同一个Queue:

public class BlockingQueueDemo {

    public static void main(String[] args) {
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
        //读线程
        new Thread(() -> {
            int i =0;
            while (true) {
                try {
                    String item = queue.take();
                    System.out.print(Thread.currentThread().getName() + ": " + i + " ");
                    System.out.println(item);
                    i++;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        //写线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    String item = "go"+i;
                    System.out.print(Thread.currentThread().getName() + ": " + i + " ");
                    System.out.println(item);
                    queue.put(item);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
  • 18
    点赞
  • 73
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Java中,线程可以通过以下几种方式实现: 1. 继承Thread类:可以创建一个继承自Thread类的子类,并重写run()方法来定义线程的执行逻辑。然后通过创建该子类的实例来启动线程。 ```java class MyThread extends Thread { public void run() { // 线程执行逻辑 } } // 创建并启动线程 MyThread thread = new MyThread(); thread.start(); ``` 2. 实现Runnable接口:可以创建一个实现了Runnable接口的类,并实现run()方法,然后通过创建该类的实例作为参数来构造Thread对象,并调用start()方法启动线程。 ```java class MyRunnable implements Runnable { public void run() { // 线程执行逻辑 } } // 创建并启动线程 Thread thread = new Thread(new MyRunnable()); thread.start(); ``` 3. 使用Callable和Future:可以创建一个实现了Callable接口的类,并实现call()方法,该方法可以返回一个结果。然后通过创建ExecutorService线程池的实例,调用submit()方法提交Callable任务,并返回一个Future对象,通过调用Future对象的get()方法可以获取任务执行的结果。 ```java import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; class MyCallable implements Callable<String> { public String call() throws Exception { // 线程执行逻辑 return "Result"; } } // 创建线程池 ExecutorService executor = Executors.newFixedThreadPool(1); // 提交任务并获取Future对象 Future<String> future = executor.submit(new MyCallable()); // 获取任务执行结果 String result = future.get(); // 关闭线程池 executor.shutdown(); ``` 这些是Java中常用的线程实现方式,每种方式都有其适用的场景和特点,可以根据具体需求选择合适的方式来实现多线程
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

顽石九变

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值