美文网首页
[Java Tutorials] 06 | Java Concu

[Java Tutorials] 06 | Java Concu

作者: 夏海峰 | 来源:发表于2018-12-27 18:52 被阅读5次

并发机制

计算机用户基于并发支持,使得计算机系统可以在同一时刻同时执行多项任务。比如计算机系统可以在同一时刻运行一个文字处理器、运行另一个文件下载的程序、还可以同时管理打印列队、还可以同时播放音频等。

即使是一个单独的程序,也希望能够在同一时刻执行多项任务。比如一个音乐播放程序,它必须要在同一时刻从网络上读取二进制的数据流,并对其进行解压缩,还要对播放进行管理,还要不断地更新当前歌词的展示。

即使是一个文字处理器,也通常需要同时响应键盘和鼠标事件,无论它是否忙于对文本的格式化或者更新显示。像这样能够在同一时刻执行多项任务的程序,我们称之为并发性软件程序

并发编程,解释了如何编写可以在同一时刻同时执行多个任务的应用程序。Java平台的设计,从底层都支持并发编程,基于Java语言中最基本的并发支持,或者Java类库。从Java 5.0 开始,Java平台就包含了高水平的并发API。本章将介绍Java平台中最基本的并发支持,同时对java.util.concurrent包中一些高水平的API进行汇总总结。

一、进程与线程

在并发编程世界里,有两个最基本的执行单元:process进程 与 thread线程。在Java语言中,并发编程最关心的是thread线程;当然,进程也是同样重要的。

一个计算系统在正常情况下,有很多活跃的进程和线程。即使在计算机系统中仅有一个单一的执行内核,因此事实上,它在任何时刻也只有一个线程在运行而已。单一内核的运行时被众多的进程和线程所共享,这是基于一个被称为时间分区的操作系统特性来实现的。

如今流行的计算机系统,越来越普遍地使用多处理器,并且每个处理器都支持多核。这极大地增强了计算机支持并发执行的能力。值得注意的是,即使不是多处理器多核的计算机,依旧是支持并发。

1)什么是进程?

一个进程,其自身包含着自有的运行环境。一个进程通常拥有着完整的私有的一系列的运行时资源,尤其是每个进程都有自己的内存空间。大多数时候,一个进程就可以被看成是一个应用程序,但是事实上有时候一个应用程序可能是由多个进程协同运行的。为了能够实现进程之间的通信,大多数的计算机系统都支持 IPC(Inter Process Communication)资源,比如管道技术和socket技术。IPC不仅支持同一系统上的两个进程之间的通信,还支持跨系统的两个进程之间的通信。

大多数的 JVM 实现,都被视为一个单一的进程。一个Java应用程序可以基于JVM环境,使用 ProcessBuilder 对象来创建额外的进程。但是,支持多进程的Java程序已经超出本章节的课程范畴,因此本章节不会对支持多进程的Java程序进行讲解。

2)什么是线程?

线程有时候会被称为“轻量级的进程”。无论是进程,还是线程,都需要提供相应的执行环境。但是,创建一个线程所需要的系统资源远远少于创建一个进程。

线程,是存在于进程之中的,即线程是基于进程而存在的。每一个进程,都至少有一个线程。线程会共享进程的所有资源,包括内存资源、文件资源等。这使得程序变得高效,并且能避免一些潜在的问题。

多线程运行是Java平台最基本的特性之一。每一个应用程序都至少有一个线程,也可以有多个线程。比如内存管理、信息处理就是计算机程序的系统线程。从应用程序的视角来看,你可以从一个被称为“主线程”的线程处开始运行程序。在这个“主线程”中,我们有能力创建额外的线程,下一小节我们演示这个知识点。

二、Thread 线程对象

每一个线程,都是一个 Thread 实例。有两种基本的策略使用 Thread 对象来创建并发性应用程序。

  • 其一,直接地控制和管理线程的创建,在每次实例化线程对象时,初始化一个异步的任务。
  • 其二,把线程管理从应用程序中抽象出来,并把应用程序的任务传递给执行器。

本部分教程将使用 Thread 对象来创建并发性应用程序。执行器将会在其它高级并发对象中进行讨论。

定义并开始一个线程

创建新线程的代码,必须运行在另一个正在运行的线程之中,有两种方式做到这一点:

1)定义线程的第一种方式

提供一个 Runnable对象。 Runnable接口提供了一个 run()方法,用这个方法包裹创建新线程所需的代码,再把 Runnable对象传递至 Thread构造器中去即可。示例代码如下:

public class HelloRunnable implements Runnable {
    public void run() {
        // 新线程的任务逻辑
        System.out.println("a new thread.");
    }
    public static void main(String[] args) {
        // 在主线程中,创建新的线程对象并运行它
        (new Thread(new HelloRunnable())).start();
    }
}

2)定义线程的第二种方式

提供一个 Thread子类。事实上 Thread类本身已经实现了 Runnable接口。在我们的应用程序中,我们可定义一个子类去继承 Thread类,并提供 run()方法的实现代码即可。示例代码如下:

public class HelloThread extends Thread {
    public void run() {
        // 新线程的任务逻辑
        System.out.println("a new thread.");
    }
    public static void main(String[] args) {
        // 在主线种中,创建新的线程并运行它
        (new HelloThread()).start();
    }
}

上述两个demo中,我们都调用了 Thread.start() 方法来开始一个线程。关于线程的定义,上述两种方式,你更喜欢哪一种方式?第一种方式,我们使用 Runnable接口,是非常普通的,因为我们可以直接定义一个子类,而不是 Thread子类。第二种方式,在我们的应用程序中,将会更容易使用,但有一个不得不面对的现实,那就是你必须定义一个任务子类去继承Thread。本教程,更倾向于使用第一种方式,因为这可以把任务逻辑和线程对象分离开来。第一种实现方式,不仅仅能使代码更加灵活,而且非常适合后续会讲到的高级别的线程管理API。

Thread类,定义了一系列有用的方法,以用于管理线程。其中有用于提供信息或者影响线程状态的 static静态方法;另一些方法用于在其它线程中进行调用,以对这个线程进行管理。接下来的课程中,会陆陆续续讲到这些。

sleep() 暂停线程

Thread.sleep()方法可以让当前线程暂停一段时间。这是一种非常的手段,可以让运行时被应用程序中的其它线程使用,或者腾出资源让其它应用程序在计算机系统中开始运行。sleep()还可以用于延缓程序的执行,如下示例:

public class SleepMessages {
    public static void main(String[] args) throws InterruptedException {
        String[] infos = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        for (int i=0; i<infos.length; i++) {
            Thread.sleep(4000);
            System.out.println(infos[i]);
        }
    }
}

sleep()方法可以以毫秒的方式指定休眠时长,还可以以纳秒的方式指定休眠时长。但是,这个休眠时长不能保证是精确的,因为它终究会受限于操作系统底层的实现。sleep()的休眠时间会被操作系统中断而终止掉,这在接下来的课程中会进一步讲解。无论如何,你都不能保证调用sleep()方法可以精确地休眠指定的时长。

另外,上述代码中,我们使用 throws 关键字抛出了 InterruptedException。当另一个线程尝试中止当前这个正在休眠期间的线程时,这个正在休眠的线程将会抛出InterruptedException异常。由于本demo中没有定义另一个线程来终止这个线程,所以我们无须对 InterruptedException 进行捕获。

interrupt 线程中断

所谓的interrupt中断,即指示一个线程停止其当前任务,然后去执行其它任务。这由程序员来决定当前线程该如何恰当地响应一个 interrupt 中断,这是本小节学习的重点。

一个线程,通过调用 interrupt()方法,可以发送一个 interrupt中断。为了让这种中断机制正确地执行,被中断的线程必须支持其自有的中断。

1)支持中断

如何让一个线程支持其自己的中断呢?这决定于这个线程当前正在做什么。如果当前线程正在频繁地调用一个抛出了InterruptedException异常的方法,那么一旦它捕获了这个异常,线程将从 run()中 return返回。很多抛出InterruptedException异常的方法,比如sleep(),都被设计成一旦接收到中断指令就会取消其当前任务并立即返回。

如果一个运行时间很长的线程,没用调用那些会抛出InterruptedException异常的方法,在接收到中断指令时会发什么呢?那么它必须周期性地调用 Thread.interruptd() 并让该方法返回true。

2)中断状态

中断机制使用一个已知的flag作为中断状态。调用 Thread.interrupt() 可以设置这个flag。当一个线程检测到一个调用了静态方法Thread.interrupted()的中断指令时,中断状态flag将会被清除。非静态方法 isInterrupted() 可用于查询另一个线程的中断状态,而不会改变它的中断状态。

通常,那些会抛出InterruptedException异常的方法,会清除中断状态。但是,也有可能发生这样的情况,当另一个线程调用了中断指令时,这个中断状态又会立即被设置回来。

线程对象 join() 方法

join()方法允许一个正在运行的线程等待另一个线程的完成。

比如,t.join() 它会导致当前线程停止运行,直到 t 线程运行结束。join()方法允许程序员指定一个等待的时长。需要注意的是,sleep() / join() 都是依赖于操作系统的时序机制,因此我们不能保证这个指定的等待时长会是精确的。

像sleep()方法一样,join()方法可以响应一个抛出了 InterruptedException异常的中断指令。

一个简单的线程demo

下面demo将引入一些新概念。示例中的 SimpleThreads 中包含两个线程,第一个是主线程(所有的Java程序都会有)。在主线程中又创建了一个新的线程。代码如下:

// SimpleThreads.java

public class SimpleThreads {
    static void threadMessage(String msg) {
        String threadName = Thread.currentThread().getName();
        System.out.format("%s: %s%n", threadName, msg);
    }
    
    private static class MessageLoop implements Runnable {
        public void run() {
            String[] msgs = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (int i=0; i<msgs.length; i++) {
                    Thread.sleep(4000);
                    threadMessage(msgs[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        long patience = 1000 * 60 * 60;
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("args must be an integer.");
                System.exit(1);
            }
        }
        threadMessage("Starting MessageLoop Thread:");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();
        
        threadMessage("Waiting for MessageLoop thread to finish:");
        
        while(t.isAlive()) {
            threadMessage("Still Waiting:");
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience) && t.isAlive())  {
                threadMessage("Tired of Waiting:");
                t.interrupt();
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}

编译上述代码并执行,控制台中的打印结果如下图所示:

SimpleThreads.java

三、Synchronization 同步

线程间的通信,主要通过field属性和对象引用的共享访问来实现的。这种通信形式是十分高效的,但有可能导致两种类型的错误发生,分别是线程干扰错误 和 内存一致性错误。使用 synchronization同步化工具可以阻止这些错误的发生。

然而,同步化也会带来一些争议,比如当两个以上的线程访问同时访问相同的资源时,这会导致Java运行时在执行线程时过慢,甚至会终止掉当前正在运行的线程。关于这种线程争议,详细参见Liveness

本小节将讨论以下几个话题:

  • Thread 接口:描述了当多个线程访问共享数据时,错误是如何发生的。
  • 内存一致性错误:描述了因为不一致的内存共享视图而导致的错误。
  • 同步化的方法:提供了一种可以高效地阻止线程干扰错误和内存一致性错误的方案。
  • 隐式加锁和同步化:描述了一种通常的同步化方案,并讲解了如何基于隐式加锁来实现同步化。
  • 原子性访问:讲述了规避与其它线程发生冲突的一般性操作方案。

线程干扰

思考下面这个Counter类:

class Counter {
    private int c = 0;
    public void increment() {
        c++;
    }
    public void decrement() {
        c--;
    }
    public int getC() {
        return c;
    }
}

1)Counter这个类很容易理解,当调用它的 increment() 方法时,c 会加1;当调用它的 decrement() 方法时,c 会减1。

2)当同一个Counter对象,被多个线程引用时,这种多个线程之间的干扰就可能导致最终结果和我们预期的结果不一致。在不同的线程中,发生相互的干扰,并作用在同一个数据上。这就意味着,需要严格按照某个特定的执行顺序来运行,才能保证得到预期的结果。

3)我们知道,当JVM在调用Counter对象的 increment() / decrement() 方法时,它是由如下三个步骤来完成的:

  • 第1步:获取当前 c 的值;
  • 第2步:对 c 进行加 1 或减 1;
  • 第3步:再把 c 存储回去。

4)当多个线程同时操作同一个Counter对象时,可见,我们无法保证JVM严格按顺序来执行。假如JVM的执行顺序是如下这个步骤:

  • Step1:线程A 读取 c
  • Step2:线程B 读取 c
  • Step3:线程A 让 c++
  • Step4:线程B 让 c--
  • Step5:线程A 把 c 存储回去
  • Step6:线程B 把 c 存储回去

5)从(4)的执行顺序,我们很容易发现,Step6 对 Step5 进行了覆盖写入。相当于线程A的 c++ 操作没有起到作用。因为多个线程对同一数据源进行操作,其执行顺序是不可预知的,所以这种线程之间的干扰所导致的bug是非常难侦测和修复的。

内存一致性错误

当多个不同的线程有着不一致的数据视图,但这个数据视图本应该是一致的时候,就会发生内存一致性错误。导致内存一致性错误的原因非常复杂,并且超出了本节教程的范畴。幸运的是,我们无须搞懂它的细节原因;我们需要做的只是使用一些策略去规避它即可。

规避内存一致性错误的核心是搞清楚错误发生前的关系。这种关系仅仅是一个简单的保证,保证内存写入时,一个特定的语句对所有其它特定的语句都是可见的。

1)为了说明这个知识点,我们先假设已经定义并初始化了一个 int 类型的字段:

int counter = 0;

2)这个counter字段被两个不同的线程所共享,线程A 和 线程B。假如,在线程A中执行counter自加:

counter++;

3)不久后,线程B 中打印了这个 counter:

System.out.println(counter);

4)分析:如果(2)(3)中的这两条语句是在同一个线程中执行,那么这将是安全的,并且会打印出我们预期的 counter=1。但是,这两条语句是在两个分离的线程中,因此打印出来的 counter很有可能是 0。因为没有任何保证“线程A中的自加”对线程B是可见的。除非我们可以为这两条语句建立起执行前的关系保证。

5)事实上,有很多种方案可以实现这种“执行前的关系保证”,其中之一便是同步化,这将在下一小节中进行讨论。关于创建这种执行前的关系保证,更多方案请参见Summary page of the java.util.concurrent package

同步化的方法

Java语言提供了两种基本的同步化风格,分别是:同步化的方法 和 同步化的语句。较为复杂的“同步化语句”将在下一小节中进行讨论,本小节仅讨论“同步化的方法”。

1)使用 synchronized 关键字,即可创建一个同步化的方法:

public class SyncCounter {
    private int c = 0;
    public synchronized void increment() {
        c++;
    }
    public synchronized void decrement() {
        c--;
    }
    public synchronized void getC() {
        return c;
    }
}

2)创建一个 SyncCounter类的实例counter,这使得counter对象的同步化方法有以下两个影响:

  • 第一个影响是,实例counter的同一个同步化方法被多处调用时,不可能发生交叉执行的情况。当一个线程调用counter对象的某个同步化方法时,所有其它调用了该同步化方法的线程都会暂停执行,直到当前线程中的这个同步化方法执行完成。
  • 第二个影响是,当一个对象的同步化方法已经存在时,它会自动地建立起与后续调用该对象的该方法的“执行前的关系”。这保证了任何改变当前对象counter状态的行为对所有其它线程都是透明可见的。

3)值得注意的是,不能用 synchronized关键字修饰类的构造器,否则会报语法错误。同步化的构造器方法是没有意义的,因为只有需要创建对象的线程构造器才应该在它被实例化时访问它。另需注意,当创建一个需要被多个线程共享的对象时,务必小心,不要过早地“泄露”这个对象的引用。

4)同步化的方法,是一种非常简单的策略用来阻止线程干扰错误和内存一致性错误。当一个对象被多个线程共享使用时,使用同步化的方法来改变该对象的状态,是非常高效的。但是,与此同时,也会带来 liveness问题,这将在后续课程中进一步讨论。

内部锁与同步化

同步化是基于一种被称为内部锁(或监控锁)的内部实体而构建的。这个内部锁扮演着同步化两个方面的角色:其一是它强制独自地访问一个对象的状态,其二是它建立起了“执行前的关系”以保证基本的可见性。

事实上,每一个对象都有一个内部锁与之关联。通常来说一个线程需要独占并一致性地访问一个对象的成员属性,因此这个线程必须在访问这个对象之前先获取到这个对象的内部锁,并在其完成了对该对象的读写操作之后释放掉这个对象的内部锁。只要当前线程拥有了这个对象的内部锁,其它线程将无法再获取到这个内部锁,那些尝试获取这个内部锁的线程将被阻塞。

一旦当前线程获取到了这个对象的内部锁时,它将和其它将要获取该对象的内部锁的行为建立起“执行前的关系”。这便是同步化实现的基础原理。

1)同步化方法中的“锁”

当线程中调用了某个对象的同步化方法时,它会自动地获取这个对象的内部锁;并在这个同步化方法return时释放掉该对象的内部锁。即使这个同步化方法是因为未知异常而导致被return时,其内部锁同样会被释放。

也许你会好奇当一个static静态的同步化方法被调用时会发生什么?因为static方法是关联在类上的,而不是关联在对象之上。事实上,在这种情形下,线程会获取与这个类关联的类对象的内部锁。如此,访问一个类的静态成员,将被这个类任意的实例对象的内部锁所控制。

2)同步化的语句

另一种创建同步化代码的方式是使用同步化的语句。和同步化方法不同的是,同步化语句必须指定对象提供内部锁。如下代码:

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

在上述代码中,addName() 方法需要同步地改变 lastName 和 nameCount 这两个状态,并且需要避免其它对象的方法同步地调用它。在没有同步化语句声明的情况下,就必须有一个独立的非同步的方法来调用这个 nameList.add() 方法。

同步化语句,对优化细粒度同步的并发非常有用。举个例子,代码如下:

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    
    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }
    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

上述代码,MsLunch类有两个实例属性 c1和c2,但它们并不会一起被使用,所有更新c1和c2的行为必须是同步的。但是没有谁能保证c1和c2的更新不会交叉在一起,如果真的这么做了,那么将发生非必要的阻塞以减弱并发。相比使用同步化的方法或者其它使用与之关联的锁,我们的做法是创建两个唯一的对象用以提供“锁”。

上述这种编程风格需要特别小心。你必须完全地确保安全地交叉地操作会受到影响的成员属性。

可重复进入的同步

一个线程不能获取一个已经被其它线程获取过的内部锁。但是一个线程可以再次获取自己已经拥有了的内部锁。允许线程多次获取同一个内部锁,使得“可重复进入的同步”成为可能。这描述了一种情景:在一段同步化的代码中,可以直接地或间接地调用另一个包含有同步化代码的方法,这两段同步化的代码可以使用同一个对象的内部锁。

假如不支持“可重复进入的同步”,那么同步化的代码就不得不提供额外的预防措施以规避线程对自己的阻塞。

四、Liveness

并发性应用程序有能力及时地执行管理,被称为它的活跃性。本小节将讨论最普遍的活跃问题类型————死锁,并尝试简单地介绍一下另外两个活跃性问题————starvation and livelock。

Deadlock 死锁

死锁描述了这样的一个情景————两个以上的线程被永远地阻塞,并彼此之间相互等待。看下面的示例:

public class DeadLock {
    // 定义一个 Friend 类
    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 f) {
            System.out.format("%s:%s" + " has bowed to me!%n", this.name, f.getName());
            f.bowBack(this);
        }
        public synchronized void bowBack(Friend f) {
            System.out.format("%s:%s" + " has bowed back to me!%n", this.name, f.getName());
        }
    }
    
    public static void main(String[] args) {
        final Friend lucy = new Friend("Lucy");
        final Friend geek = new Friend("Geek");
        
        // 第一个线程
        new Thread(new Runnable() {
            public void run() {
                lucy.bow(geek);
            }
        }).start();
        
        // 另一个线程
        new Thread(new Runnable() {
            public void run() {
                geek.bow(lucy);
            }
        }).start();
    }
}

上述代码,两个好友Lucy 和 Geek,礼貌性相互鞠躬。如果定下一个严格的礼仪规则:当你向好友鞠躬时,如果对方没有回礼,那么你就必须一直保持着鞠躬的姿势。不幸的是,恰好Lucy和Geek同时地向对方鞠躬,这将导致这俩好友彼此之间都一直保护着鞠躬姿势并相互等待着。这就发生了“死锁”。

Starvation and Livelock

Starvation and Livelock问题比死锁问题的出现频率更低。但对于并发程序来讲,我们仍然可能遭遇到。

1)Starvation

Starvation描述了一种情景,一个线程不能够正常地访问共享资源,导致程序不能够继续执行。这种问题的发生,是因为共享资源被另一个贪婪的线程长时间占有而导致的。

2)Livelock

一个线程通常扮演着响应另一个线程的角色。如果另一个线程又扮演着响应再一个的线程,这就可能导致livelock问题。和死锁一样,livelock同样阻止了程序的继续执行。然而这种livelock并不是“阻塞”,而只是纯粹地忙于线程之间的相互响应。

五、Guarded Block 守护块

线程通常要协调它们的操作,最普遍的协调风格就是Guarded Block。这个block从轮询一个条件开始,这个条件在block开始执行前必须为真。为了正确地执行这个过程,有一系列的步骤需要遵守。

1)我们来分析一下如下的这段代码:

public void guardedJoy() {
    // 单纯地使用 while循环来监视代码块,这很浪费处理器的时间,因此不推荐这样做
    while(joy) {
        // while true do something
    }
    System.out.println("joy has been achieved.");
}

上面这段代码,我们对joy变量进行监控,当其为true时,才能去做指定的任务。这里使用了while循环来监视joy条件的变化,事实上这是很浪费处理器的时间,因为它需要多次的运行与监视,而导致了程序等待。

2)我们现在使用 Object.wait() 方法来对其优化,这是一种更加高效的解决方案。

public synchronized void guardedJoy() {
    while(joy) {
        try {
            wait();
        } catch (InterruptedException e) {
            // catch exception
        }
    }
    System.out.println("joy and efficiency have been achieved.");
}

当 wait() 被调用时,当前线程就会释放掉它对某个对象的内部锁,并延迟执行。在不久后,另一个线程将可以获取到同一块内部锁,并调用Object.notifyAll() 通知所有等待这块内部锁的线程有重要事情发生了。

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

不久后,当第二个线程释放掉这块内部锁之后,第一个线程将重新获取到这块内部锁,并当Object.wait()方法return后重新开始工作。

3)下面我们使用 Object.wait() / Object.notifyAll() 来实现一个“守护块”。

public class Drop {
    private String msg;
    private boolean empty = true;
    public synchronized String take() {
        while(empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        empty = true;
        notifyAll();
        return msg;
    }
    public synchronized void put(String msg) {
        while(empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        empty = false;
        this.msg = msg;
        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[] msgs = {"Mares eat oats", "Does eat oats", "Little lambs eat ivy"};
        Random random = new Random();
        for (int i=0; i<msgs.length; i++) {
            drop.put(msgs[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 msg = drop.take(); !msg.equals("DONE"); msg=drop.take()) {
            System.out.format("msg received: %s%n", msg);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}

开始测试上述代码:

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

六、不可变的对象

一个不可变的对象,它的状态不能被改变。使用不可变对象作为一个健全的策略用于创建简单的可依赖的代码,是广泛被接受的。可变对象在并发性应用程序中,是非常有用的,因为他们的状态是不能改变的,它们不能被线程损坏。

相关文章

网友评论

      本文标题:[Java Tutorials] 06 | Java Concu

      本文链接:https://www.haomeiwen.com/subject/zymrlqtx.html