Java并发笔记三——活跃性和guarded blocks

活跃性

并行应用程序的及时执行能力被称为它的活跃度(liveness)。本节将介绍最常见的一种活跃度的问题——死锁,以及简要提及另外两个活跃性的问题——饥饿和活锁。

死锁

死锁是指两个或两个以上的线程永远被阻塞,一直等待对方的资源。下面是一个例子。

Alphonse和Gaston是朋友,都很有礼貌。礼貌的一条严格规则是,当你给一个朋友鞠躬时,你必须保持鞠躬,直到你的朋友鞠躬回给你。不幸的是,这条规则有个缺陷,那就是如果两个朋友同一时间向对方鞠躬,那就永远不会完了。这个示例应用程序中,死锁模型是这样的:

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

当两个线程尝试调用bowBack时非常有可能两个线程都将被阻塞。无论是哪个线程永远不会结束,因为每个线程都在等待对方结束bow。

饥饿和活锁(Starvation and Livelock)

饥饿和活锁相比死锁问题不太常见,但对于每一个并发软件的设计师来说仍然很有可能会遇到。

饥饿(Starvation)

饥饿描述了一个线程由于不能经常访问共享资源而无法取得进展的情况。这种情况一般出现在共享资源被某些“贪婪”线程占用,而导致资源长时间不能被其他线程使用。例如,假设对象提供了一个同步的方法,但需要很长时间才能返回。如果一个线程频繁调用该方法,其他需要频繁同步访问同一个对象的线程通常会被阻塞。

活锁(Livelock)

一个线程常常处于响应另一个线程的动作,如果其他线程的动作也是另外一个线程的响应,那么就可能出现活锁。与死锁一样,活锁进程也无法取得进一步进展。但是,线程并没有被阻塞,他们只是会忙于响应对方来恢复工作。这可以类比为,两人面对面试图通过一条走廊: Alphonse 移动到他的左则让路给 Gaston ,而 Gaston 移动到他的右侧想让 Alphonse 过去,两个人同时让路,但其实两人都挡住了对方没办法过去,他们仍然彼此阻塞。

Guarded Blocks

多线程之间经常需要协同工作,最常见的合作方式是使用Guarded Blocks,它循环检查一个条件,直到条件为true才跳出循环继续执行。为了正确使用Guarded Blocks需要注意以下几个步骤:

举个例子,假设guardedJoy()方法必须要等待另一线程为共享变量joy设值才能继续执行。那么理论上可以用一个简单的条件循环来实现,但在等待过程中guardedJoy方法不停的检查循环条件实际上是一种资源浪费。

    public void guardedJoy() {
        // Simple loop guard. Wastes
        // processor time. Don't do this!
        while(!joy) {}
        System.out.println("Joy has been achieved!");
}

更加高效的方法是调用Object.wait将当前线程挂起,直到有另一线程发起事件通知(尽管通知的事件不一定是当前线程等待的事件)。

    public synchronized void guardedJoy() {
        // This guard only loops once for each special event, which may not
        // be the event we're waiting for.
        while (!joy) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        System.out.println("Joy and efficiency have been achieved!");
}

注意:一定要在循环里面调用wait方法去检测等待的条件,不要认为中断是你等待的特定条件,或者条件仍然是true的。

和其他可以暂停线程执行的方法一样,wait方法会抛出InterruptedException,在上面的例子中,因为我们关心的是joy的值,所以忽略了InterruptedException。

为什么这个版本的guardedJoy是synchronized的?假设d是用来调用wait的对象,当一个线程调用d.wait,它必须要拥有d的内部锁(否则会抛出异常),获得d的内部锁的最简单方法是在一个synchronized方法里面调用wait。

当一个线程调用wait方法时,它释放锁并挂起执行。在将来的某个时间点,另一个线程请求并获得这个锁并调用Object.notifyAll来通知所有等待该锁的线程发生了一些重要的事情。

    public synchronized notifyJoy() {
        joy = true;
        notifyAll();
}

当第二个线程释放这个该锁后,第一个线程重新获得该锁,从wait方法调用返回并继续执行。


注意:还有另外一个通知方法,notify(),它只会唤醒一个线程。但由于它并不允许指定哪一个线程被唤醒,所以一般只在大规模并行应用(即系统有大量相似任务的线程)中使用。因为对于大规模并发应用,我们其实并不关心哪一个线程被唤醒

现在我们使用guarded blocks创建一个生产者/消费者应用。这类应用需要在两个线程之间共享数据:生产者生产数据,消费者使用数据。两个线程通过共享对象通信。在这里,线程协同工作的关键是:生产者发布数据之前,消费者不能够去读取数据;消费者没有读取旧数据前,生产者不能发布新数据。

在这个例子中,数据是通过Drop对象共享的一系列文本消息:

    public class Drop {
        // Message sent from producer to consumer.
        private String message;
        // True if consumer should wait for producer to send message,
        // False if producer should wait for consumer to retrieve message.
        private boolean empty = true;

        public synchronized String take() {
            // Wait until message is available.
            while (empty) {
                try {
                    wait();
                } catch (InterruptedException e) {}
            }
            // Toggle status.
            empty = true;
            // Notify producer that status has changed.
            notifyAll();
            return message;
        }

        public synchronized void put(String message) {
            // Wait until message has been retrieved.
            while (!empty) {
                try { 
                    wait();
                } catch (InterruptedException e) {}
            }
            // Toggle status.
            empty = false;
            // Store message.
            this.message = message;
            // Notify consumer that status has changed.
            notifyAll();
        }
    }

Producer是生产者线程,发送一组消息,字符串”DONE”表示所有消息都已经发送完成。为了模拟现实应用中的不可预见性,生产者线程还会在消息发送间隙产生随机的暂停。

    import java.util.Random;

    public class Producer implements Runnable {
        private Drop drop;

        public Producer(Drop 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();

            for (int i = 0; i < importantInfo.length; i++) {
                drop.put(importantInfo[i]);
                try {
                    Thread.sleep(random.nextInt(5000));
                } catch (InterruptedException e) {}
            }
            drop.put("DONE");
        }
}

Consumer是消费者线程,读取消息并打印出来,直到读取到字符串DONE为止。消费者 线程在消息读取时也会随机的暂停。

    import java.util.Random;

    public class Consumer implements Runnable {
        private Drop drop;

        public Consumer(Drop drop) {
            this.drop = drop;
        }

        public void run() {
            Random random = new Random();
            for (String message = drop.take(); !message.equals("DONE"); message = drop.take()) {
                System.out.format("MESSAGE RECEIVED: %s%n", message);
                try {
                    Thread.sleep(random.nextInt(5000));
                } catch (InterruptedException e) {}
            }
        }
    }

最后,ProducerConsumerExample是主线程,它启动生产者线程和消费者线程

    public class ProducerConsumerExample {
        public static void main(String[] args) {
            Drop drop = new Drop();
            (new Thread(new Producer(drop))).start();
            (new Thread(new Consumer(drop))).start();
        }
}



注意:Drop类是用来演示Guarded Blocks如何工作的。为了避免重新发明轮子,当你尝试创建自己的共享数据对象时,请查看Java Collections Framework中已有的数据结构。如需更多信息,请参考 Questions and Exercises


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值