『深入学习Java』(五) Java 并发经典题

175 阅读2分钟

『深入学习Java』(五) Java 并发经典题

生产者/消费者

这里模拟了一个消息队列,来说明生产者/消费者问题。

我们需要实现几个特点:

  • 消息队列有固定长度。
  • 消费者一直消费队列消息,若无消息可消费则暂停。
  • 生产者一直生产消息,若队列已满则暂停。

实际上 Java 中的各种阻塞队列,也是采用的这种模式。

  1. 消息体定义 ```java
@Data
public static class Msg {
    private String id;
    private String msg;
}
  1. 消息队列定义
    public static class MsgQueue {

        private LinkedList<Msg> queue;
        private int size;

        public MsgQueue(int size) {
            this.queue = new LinkedList<>();
            this.size = size;
        }

        public Msg take() {
            synchronized (queue) {
                // 如果没数据,则等待
                while (queue.isEmpty()) {
                    log.info("队列没数据了。等一会儿再取吧。");
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {..}
                }
                Msg pop = queue.pop();
                log.info("消费队列数据。剩余队列大小:{}", queue.size());
                // 添加完数据后,唤醒等待线程。
                queue.notifyAll();
                return pop;
            }
        }

        public synchronized void put(Msg msg) {
            synchronized (queue) {
                // 如果队列满了,则进入等待
                while (queue.size() >= size) {
                    log.info("队列数据满了。等一会儿再添加吧。");
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {..}
                }
                queue.add(msg);
                log.info("添加队列数据。队列大小:{}", queue.size());
                // 添加完数据后,唤醒等待线程
                queue.notifyAll();
            }
        }
    }

执行结果

20:51:04.219 [Thread-5] INFO ThreadLockTest - 消费队列数据。剩余队列大小:2
20:51:04.219 [Thread-5] INFO ThreadLockTest - 消费队列数据。剩余队列大小:1
20:51:04.219 [Thread-5] INFO ThreadLockTest - 消费队列数据。剩余队列大小:0
20:51:04.219 [Thread-5] INFO ThreadLockTest - 队列没数据了。等一会儿再取吧。
20:51:04.219 [Thread-1] INFO ThreadLockTest - 添加队列数据。队列大小:1
20:51:04.219 [Thread-1] INFO ThreadLockTest - 添加队列数据。队列大小:2
20:51:04.219 [Thread-5] INFO ThreadLockTest - 消费队列数据。剩余队列大小:1
20:51:04.219 [Thread-5] INFO ThreadLockTest - 消费队列数据。剩余队列大小:0
20:51:04.219 [Thread-5] INFO ThreadLockTest - 队列没数据了。等一会儿再取吧。
20:51:04.219 [Thread-1] INFO ThreadLockTest - 添加队列数据。队列大小:1
20:51:04.219 [Thread-1] INFO ThreadLockTest - 添加队列数据。队列大小:2
20:51:04.219 [Thread-5] INFO ThreadLockTest - 消费队列数据。剩余队列大小:1
20:51:04.219 [Thread-5] INFO ThreadLockTest - 消费队列数据。剩余队列大小:0
.....

固定顺序

线程1输出a,线程2输出b。要求固定按照ba运行。

    final Object o = new Object();
    boolean flag = false;

    @Test
    public void test05() {

        Thread thread1 = new Thread(() -> {
            synchronized (o) {
                // 如果 thread2 还没执行,就等一会儿。
                while (!flag) {
                    try {
                        o.wait();
                    } catch (InterruptedException e) {...}
                }
                System.out.println("a");
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (o) {
                System.out.println("b");
                flag = true;
                o.notifyAll();
            }

        });

        thread1.start();
        thread2.start();
    }

给定次序打印

线程1 输出5次a,线程2 输出5次b,线程3 输出5次c。要求输出打印结果 abcabcabcabcabc。

Object.wait/notify 版本

public void test04() {
        TurnPrint turnPrint = new TurnPrint("1", 5);
        new Thread(() -> turnPrint.print("1","2","a")).start();
        new Thread(() -> turnPrint.print("2","3","b")).start();
        new Thread(() -> turnPrint.print("3","1","c")).start();

    }
    public static class TurnPrint {
        private int times;
        private String flag;
        public TurnPrint(String flag,int times) {
            this.times = times;
            this.flag = flag;
        }
        public void print(String thisFlag,String nextFlag,String printStr) {
            for (int i = 0; i < times; i++) {

                synchronized (this) {
                    while (!Objects.equals(thisFlag,flag)) {
                        try {
                            this.wait();
                        } catch (InterruptedException e) {}
                    }
                    System.out.println(printStr);;
                    flag=nextFlag;
                    this.notifyAll();
                }
            }
        }
    }

运行结果

abcabcabcabcabc

Lcok-Condition 版本

Condition 相对了 wait/notify 的区别在于,我可以知道需要第一个执行的线程在哪个 Conditon 上。

所以我们可以先让所有线程都 wait,然后精准唤醒需要第一个执行的线程。

@Test
    public void test06() {

        ReentrantLock lock1 = new ReentrantLock();
        Condition c1 = lock1.newCondition();
        Condition c2 = lock1.newCondition();
        Condition c3 = lock1.newCondition();

        Test06Print test06Print = new Test06Print(5, c1, lock1);

        new Thread(() -> test06Print.print(c1,c2,"a")).start();
        new Thread(() -> test06Print.print(c2,c3,"b")).start();
        new Thread(() -> test06Print.print(c3,c1,"c")).start();

        test06Print.notifyFirst();
    }

    @AllArgsConstructor
    public static class Test06Print {
        private int times;
        private Condition firstCondition;
        private Lock lock;

        public void print(Condition thisCondi, Condition nextCondi, String str) {
            for (int i = 0; i < times; i++) {
                lock.lock();
                try {
                    thisCondi.await();
                    System.out.println(str);
                    nextCondi.signal();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }
        }

        public void notifyFirst() {
            lock.lock();
            try {
                firstCondition.signal();
            } finally {
                lock.unlock();
            }
        }
    }