文档

Java™ 教程-Java Tutorials 中文版
Trail: Essential Classes
Lesson: Concurrency
主页>必要类>并发

问题和练习的答案:并发

问题

  1. 问题:你可以将 Thread 对象传递给 Executor.execute 吗?这样的调用会有意义吗?为什么或者为什么不?

    答案:Thread 实现 Runnable 接口,因此你可以将 Thread 的实例传递给 Executor.execute。但是以这种方式使用 Thread 对象没有意义。如果对象直接从 Thread 实例化,则其 run 方法不会执行任何操作。你可以使用有用的 run 方法定义 Thread 的子类 — 但是这样的类会实现执行器不使用的功能。

练习

  1. 练习:编译并运行 BadThreads.java
    
    public class BadThreads {
    
        static String message;
    
        private static class CorrectorThread
            extends Thread {
    
            public void run() {
                try {
                    sleep(1000); 
                } catch (InterruptedException e) {}
                // Key statement 1:
                message = "Mares do eat oats."; 
            }
        }
    
        public static void main(String args[])
            throws InterruptedException {
    
            (new CorrectorThread()).start();
            message = "Mares do not eat oats.";
            Thread.sleep(2000);
            // Key statement 2:
            System.out.println(message);
        }
    }
    

    应用程序应打印出“Mares do eat oat”。是否保证始终这样做?如果不是,为什么不是呢?更改 Sleep 的两次调用的参数会有帮助吗?你如何保证主线程可以看到 message 的所有更改?

    解答:该程序几乎总是打印出“Mares do eat oat”。但是,由于 "Key statement 1" 和 "Key statment 2" 之间没有happens-before 关系,因此无法保证此结果。即使 "Key statement 1" 实际上在 "Key statement 2" 之前执行,这也是如此。记住,happens-before 关系是关于可见性,而不是序列。

    有两种方法可以保证主线程可以看到 message 的所有更改:

    • 在主线程中,保留对 CorrectorThread 实例的引用。然后在引用 message 之前在该实例上调用 join
    • 使用同步方法在对象中封装 message。除非通过这些方法,否则不要引用 message

    这两种技术都建立了必要的 happens-before 关系,使 message 的更改可见。

    第三种技术是简单地将 message 声明为 volatile。这保证了对 message(如 "Key statement 1" 中)将与 message 的任何后续读取具有 happens-before 关系(如 "Key statement 2" 中所示)。但它并不能保证 "Key statement 1" 将 literally (字面上) 发生在 "Key statement 2" 之前。它们 可能 按顺序发生,但由于调度不确定性和 sleep 的未知粒度,这不能保证。

    更改两个 sleep 调用的参数也无济于事,因为这不会保证 happens-before 关系。

  2. 练习:修改 Guarded Blocks 中的 producer-consumer 示例,以使用标准库类而不是 Drop 类。

    解答:java.util.concurrent.BlockingQueue 接口定义了一个队列为空时阻塞的 get 方法,以及队列已满时阻塞的 put 方法。这些实际上是由 Drop 定义的相同操作。除了 Drop 不是队列!但是,还有另一种查看 Drop 的方法:它是一个容量为零的队列。由于 any (任何) 元素在队列中都没有空间,每个 get 会阻塞直到相应的 take ,并且每个 take 阻塞直到相应的 get。有一个 BlockingQueue 的实现正是这种行为:java.util.concurrent.SynchronousQueue

    BlockingQueue 几乎是 Drop 的直接替代品。Producer 中使用 BlockingQueue 的主要问题是,putget 方法会抛出 InterruptedException。这意味着现有的 try 必须向上移动一个级别:

    
    import java.util.Random;
    import java.util.concurrent.BlockingQueue;
    
    public class Producer implements Runnable {
        private BlockingQueue<String> drop;
    
        public Producer(BlockingQueue<String> drop) {
            this.drop = drop;
        }
    
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            Random random = new Random();
    
            try {
                for (int i = 0;
                     i < importantInfo.length;
                     i++) {
                    drop.put(importantInfo[i]);
                    Thread.sleep(random.nextInt(5000));
                }
                drop.put("DONE");
            } catch (InterruptedException e) {}
        }
    }
    
    Consumer 需要进行类似的更改:
    
    import java.util.Random;
    import java.util.concurrent.BlockingQueue;
    
    public class Consumer implements Runnable {
        private BlockingQueue<String> drop;
    
        public Consumer(BlockingQueue<String> drop) {
            this.drop = drop;
        }
    
        public void run() {
            Random random = new Random();
            try {
                for (String message = drop.take();
                     ! message.equals("DONE");
                     message = drop.take()) {
                    System.out.format("MESSAGE RECEIVED: %s%n",
                                      message);
                    Thread.sleep(random.nextInt(5000));
                }
            } catch (InterruptedException e) {}
        }
    }
    
    对于 ProducerConsumerExample,我们只需更改 drop 对象的声明:
    
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.SynchronousQueue;
    
    public class ProducerConsumerExample {
        public static void main(String[] args) {
            BlockingQueue<String> drop =
                new SynchronousQueue<String> ();
            (new Thread(new Producer(drop))).start();
            (new Thread(new Consumer(drop))).start();
        }
    }
    

Previous page: Questions and Exercises: Concurrency