Java 进程和线程

进程和线程

在并发编程中,有两个基本的执行单元:进程和线程。在Java编程语言中,通常并发编程主要与线程有关。但是进程也很重要。

计算机系统通常具有许多活动的进程和线程。即使在只有一个执行核心,因此在任何给定时刻只有一个线程实际执行的系统中,也是如此。通过称为时间分片的OS功能,单个内核的处理时间在进程和线程之间共享

计算机系统具有多个处理器或具有多个执行核心的处理器正变得越来越普遍。这极大地增强了系统同时执行进程和线程的能力-但即使在没有多个处理器或执行核心的简单系统上,并发也是可能的。

进程(Processes)

进程具有独立的执行环境。进程通常具有一套完整的私有基本运行时资源;特别是,每个进程都有自己的存储空间。

大多数系统都支持进程之间可以互相通信(IPC)资源,例如管道、套接字。

Java虚拟机的大多数实现都是作为单个进程运行的。Java应用程序可以使用ProcessBuilder对象创建其他进程 。

线程数

线程有时称为轻量级进程。进程和线程都提供执行环境,但是创建新线程所需的资源少于创建新进程的资源

线程存在于一个进程中-每个进程至少有一个,线程共享进程的资源。

多线程执行是Java平台的基本功能。每个应用程序至少有一个线程,或者,如果算上执行诸如内存管理和信号处理之类的“系统”线程,则至少有几个。但是从应用程序程序员的角度来看,您仅从一个线程(称为*主线程)开始*。该线程具有创建其他线程的能力。

线程对象

每个线程都与该类的一个实例相关联 Thread。使用Thread对象创建并发应用程序有两种基本策略。

  • 要直接控制线程的创建和管理,只需Thread在应用程序每次需要启动异步任务时实例化即可。
  • 要从应用程序的其余部分抽象线程管理,请将应用程序的任务传递给executor

定义和启动线程

创建Thread实例的应用程序必须提供将在该线程中运行的代码。

有两种方法可以做到这一点:

  1. 提供一个Runnable 对象,Runnable接口定义了一个方法run,旨在包含在线程中执行的代码。像HelloRunnable示例一样,将Runnable对象传递给Thread构造函数。

    public class HelloRunnable implements Runnable {
    
        public void run() {
            System.out.println("Hello from a thread!");
        }
    
        public static void main(String args[]) {
            (new Thread(new HelloRunnable())).start();
        }
    
    }
    
  2. 子类线程。Thread类本身实现了Runnable,尽管其run方法不执行任何操作。应用程序可以子类化Thread,提供自己的运行实现,如HelloThread示例中所示:

    public class HelloThread extends Thread {
    
        public void run() {
            System.out.println("Hello from a thread!");
        }
    
        public static void main(String args[]) {
            (new HelloThread()).start();
        }
    
    }
    

注意,两个示例都调用Thread.start来启动新线程。

您应该使用以下哪些方式?第一个使用Runnable(接口)对象的习惯用法更为通用,因为Runnable对象可以继承Thread以外的其他类。第二种方式在简单的应用程序中更易于使用,但由于您的任务类必须是Thread(类)的后代这一事实而受到限制。

暂停执行Sleep

Thread.sleep使当前线程在指定时间段内暂停执行。这是使处理器时间可用于应用程序的其他线程或计算机系统上可能正在运行的其他应用程序的有效方法。如后面的示例所示,sleep方法也可以用于调速,并像稍后部分中的SimpleThreads示例一样,等待另一个线程被认为具有时间要求。

提供了两种过载的Sleep版本:一种将睡眠时间指定为毫秒,另一种将睡眠时间指定为纳秒。但是,由于这些Sleep时间受到底层操作系统提供的功能的限制,因此不能保证精确的Sleep(睡眠)时间。而且,Sleep期可以通过中断来终止,这将在下一部分中介绍。无论如何,您不能假定调用sleep将在指定的时间段内精确地挂起线程。

SleepMessages示例使用sleep以四秒钟的间隔打印消息:

public class SleepMessages {
    public static void main(String args[])
        throws InterruptedException {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };

        for (int i = 0;
             i < importantInfo.length;
             i++) {
            //Pause for 4 seconds
            Thread.sleep(4000);
            //Print a message
            System.out.println(importantInfo[i]);
        }
    }
}

注意main声明它抛出了InterruptedException。这是一个异常,当Sleep处于活动状态时,另一个线程中断当前线程会引发Sleep。由于此应用程序尚未定义引起中断的另一个线程,因此它不会费心捕捉InterruptedException。

中断(Interrupts)

中断表明线程应该停止正在执行的操作并执行其他操作。完全由程序员决定线程如何响应中断,但是终止线程是很常见的。线程通过在Thread对象上调用要被中断的线程来发送中断。为了使中断机制正常工作,被中断的线程必须支持自己的中断。

支持中断(Supporting Interruption)

线程如何支持自己的中断?这取决于它当前正在做什么。如果线程经常调用引发InterruptedException的方法,则在捕获该异常后,它仅从run方法返回。例如,假设SleepMessages示例中的中央消息循环位于线程的Runnable对象的run方法中。然后可以对其进行如下修改以支持中断:

for (int i = 0; i < importantInfo.length; i++) {
    // Pause for 4 seconds
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        // We've been interrupted: no more messages.
        return;
    }
    // Print a message
    System.out.println(importantInfo[i]);
}

许多引发InterruptedException的方法(例如Sleep)旨在取消其当前操作,并在收到中断时立即返回。如果线程长时间运行而没有调用引发InterruptedException的方法怎么办?然后,它必须定期调用Thread.interrupted,如果收到中断,则返回true。例如:

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        // We've been interrupted: no more crunching.
        return;
    }
}

在这个简单的示例中,代码仅测试中断,如果已接收到该线程,则退出线程。在更复杂的应用程序中,抛出InterruptedException可能更有意义:

中断状态标志

中断机制是使用内部标志(称为中断状态)实现的。调用Thread.interrupt设置此标志。当线程通过调用静态方法Thread.interrupted检查中断时,将清除中断状态。一个线程用于查询另一线程的中断状态的非静态isInterrupted方法不会更改中断状态标志。

照惯例,任何通过抛出InterruptedException退出的方法都会清除中断状态。但是,总是有可能通过另一个调用中断的线程立即再次设置中断状态。

Joins

join方法允许一个线程等待另一个线程的完成。如果t是当前正在执行线程的Thread对象,

t(Thread对象).join();

导致当前线程暂停执行,直到t的线程终止。join的重载方法使程序员可以指定等待时间。但是,与Sleep一样,join的运行时间也取决于操作系统,因此,您不应假定join会完全按照您指定的时间等待。

像Sleep一样,join通过退出InterruptedException来响应中断。

中断异常作用于:wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int) 这些方法,将清除中断状态并抛出InterruptedException异常

同步(Synchronization)

线程主要通过共享对字段和对象引用字段所引用的访问来进行通信。这种通信形式非常高效,但可能导致两种错误:线程干扰内存一致性错误。防止这些错误所需的工具是同步

但是,同步会引入线程争用,当两个或多个线程尝试同时访问同一资源并使Java运行时更慢地执行一个或多个线程,甚至挂起它们的执行时,就会发生线程争用。 饥饿和活锁是线程争用的形式。

Thread Interference

Counter考虑一个称为Counter的简单类:

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

计数器(Counter)被设计为使每次increment调用都将对c加1,而每次decrement调用将从c中减去1。但是,如果从多个线程引用了Counter对象,则线程之间的干扰可能会阻止此事件按预期发生。

当在不同线程中运行但作用于相同数据的两个操作交错时,就会发生干扰。这意味着这两个操作由多个步骤组成,并且步骤顺序重叠。

由于对c的两个操作都是单个简单的语句,因此对Counter实例的操作似乎无法进行交织。但是,即使是简单的语句也可以由虚拟机转换为多个步骤。我们不会检查虚拟机采取的具体步骤-足以知道单个表达式c ++可以分解为三个步骤:

  1. 检索c的当前值。
  2. 将检索到的值增加1。
  3. 将增加的值存储回c中。

表达式c--可以用相同的方式分解,除了第二步是递减而不是递增。

假设线程A大约在线程B调用减量的同时调用增量。如果c的初始值为0,则它们的交错动作可能遵循以下顺序:

  1. 线程A:检索c。
  2. 线程B:检索c。
  3. 线程A:增加检索值;结果是1。
  4. 线程B:递减检索值;结果是-1。
  5. 线程A:将结果存储在c中; c现在是1。
  6. 线程B:将结果存储在c中; c现在为-1。

线程A的结果丢失,被线程B覆盖。这种特定的交织只是一种可能性。在不同的情况下,可能是线程B的结果丢失了,或者根本没有错误。由于它们是不可预测的,因此可能很难检测和修复线程干扰错误。

Memory Consistency Errors内存一致性错误

当不同的线程对应为相同数据的视图不一致时,将发生内存一致性错误。内存一致性错误的原因很复杂,超出了本教程的范围。幸运的是,程序员不需要详细了解这些原因。所需要的只是避免它们的策略。

避免内存一致性错误的关键是了解事前发生的关系。这种关系只是对一个特定语句的内存写操作对另一特定语句可见的保证。要看到这一点,请考虑以下示例。假设定义并初始化了一个简单的int字段:

int counter = 0;

计数器字段在两个线程A和B之间共享。假设线程A递增计数器:

counter++;

然后,不久之后,线程B打印出计数器:

System.out.println(counter);

如果两个语句已在同一线程中执行,则可以安全地假定打印出的值为“ 1”。但是,如果两个语句在单独的线程中执行,则打印出的值很可能是“ 0”,因为不能保证线程A对计数器的更改将对线程B可见-除非程序员已在两者之间建立事前发生的关系这两个陈述。

有几种动作可以创建事前发生的关系。其中之一是同步,正如我们将在以下各节中看到的。

我们已经看到了两个创建先于关系的动作。

  • 当一条语句调用Thread.start时,与该语句具有事前发生关系的每个语句也与新线程执行的每个语句都具有事前发生关系。导致创建新线程的代码效果对新线程可见。

  • 当一个线程终止并导致另一个线程中的Thread.join返回时,终止线程执行的所有语句与成功连接之后的所有语句都具有事前发生关系。现在,执行Join的线程可以看到线程中代码的效果。

Synchronized Methods同步方法

Java编程语言提供了两个基本的同步习惯用法:同步方法和同步语句。

介绍同步方法,要使方法同步,只需将synchronized关键字添加到其声明中:

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

如果count是SynchronizedCounter的一个实例,则使这些方法同步具有两个作用:

  • 首先,不可能对同一对象的两次同步方法调用进行交织。当一个线程正在执行对象的同步方法时,所有其他调用同一对象块的同步方法的线程(挂起执行),直到第一个线程对该对象完成。
  • 其次,当同步方法退出时,它将自动与之前对同一对象的同步方法的任何调用建立先发生关系。这保证了对象状态的更改对所有线程都是可见的。

请注意,构造函数无法同步-在构造函数中使用synced关键字是语法错误。同步构造函数没有任何意义,因为只有创建对象的线程才可以在构造对象时对其进行访问。

警告:构造将在线程之间共享的对象时,请务必注意,对该对象的引用不会过早“泄漏”。例如,假设您要维护一个包含实例的List,其中包含类的每个实例。您可能会想将以下行添加到构造函数中:instance.add(this) 但是,其他线程可以在对象的构造完成之前使用实例来访问对象。

同步方法提供了一种防止线程干扰和内存一致性错误的简单策略:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都将通过同步方法完成。(一个重要的例外:一旦构造了对象,就可以通过非同步方法安全地读取final字段(一旦构造了对象,则无法修改)。这种策略是有效的,但是会带来活动性方面的问题。

Intrinsic Locks and Synchronization(固有锁和同步)

同步是围绕称为内部锁定或监视器锁定的内部实体构建的。(API规范通常将此实体简称为“监视器”。)内在锁在同步的两个方面都起作用:强制对对象状态的独占访问并建立对可见性至关重要的事前关联。

每个对象都有一个与之关联的固有锁。按照约定,需要对对象的字段进行独占且一致的访问的线程必须在访问对象之前先获取对象的固有锁,然后在完成对它们的锁定后释放固有锁。据称,线程在获取锁和释放锁之间拥有内部锁。只要一个线程拥有一个内在锁,其他任何线程都无法获得相同的锁。另一个线程在尝试获取锁时将阻塞。

当线程释放固有锁时,该动作与任何随后的相同锁获取之间将建立事前发生的关系。

锁定同步方法

当线程调用同步方法时,它将自动获取该方法对象的内在锁,并在方法返回时释放该内在锁。即使返回是由未捕获的异常引起的,也会发生锁定释放。

您可能想知道当调用静态同步方法时会发生什么,因为静态方法与类而不是对象相关联。在这种情况下,线程获取与该类关联的Class对象的固有锁定。因此,通过与该类的任何实例的锁不同的锁来控制对类的静态字段的访问。

同步语句

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

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

在此示例中,addName方法需要将对lastName和nameCount的更改同步,但还需要避免同步其他对象的方法的调用。如果没有同步语句,则仅出于调用nameList.add的唯一目的,就必须有一个单独的非同步方法。

同步语句对于通过细粒度同步提高并发性也很有用。例如,假设类MsLunch有两个实例字段c1和c2,它们从未一起使用。这些字段的所有更新都必须同步,但是没有理由阻止c1更新与c2更新交织—这样做会通过创建不必要的阻塞来减少并发性。代替使用同步方法或以其他方式使用与此关联的锁,我们仅创建两个对象来提供锁。

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++;
        }
    }
}

请格外小心地使用此习语。您必须绝对确保插入受影响字段的访问确实是安全的。

重入同步

回想一下,一个线程无法获取另一个线程拥有的锁。但是线程可以获取它已经拥有的锁。允许一个线程多次获取相同的锁将启用重入同步。这描述了一种情况,其中同步代码直接或间接调用一个也包含同步代码的方法,并且两组代码使用相同的锁。如果没有可重入同步,则同步代码将不得不采取许多其他预防措施,以避免线程导致自身阻塞。

原子访问(Atomic Access)

在编程中,原子动作是一次有效地同时发生的动作。原子动作不能停在中间:它要么完全发生,要么根本不发生。直到动作完成,原子动作的副作用才可见。

我们已经看到,增量表达式(例如c ++)并未描述原子动作。即使是非常简单的表达式也可以定义可以分解为其他动作的复杂动作。但是,您可以指定一些原子操作:

  • 对于参考变量和大多数原始变量(除long和double以外的所有类型),读写都是原子的。
  • 对于声明为volatile的所有变量(包括long和double变量),读写都是原子的。

原子动作不能交错,因此可以使用它们而不必担心线程干扰。但是,这并不能消除所有同步原子操作的需要,因为仍然可能发生内存一致性错误。使用volatile变量可降低内存一致性错误的风险,因为对volatile变量的任何写操作都会与该变量的后续读取建立先发生后关系。而且,这还意味着,当线程读取一个volatile变量时,它不仅会看到对volatile的最新更改,还会看到导致更改的代码的副作用。

使用简单的原子变量访问比通过同步代码访问这些变量更有效,但是程序员需要格外小心以避免内存一致性错误。是否值得付出额外的努力取决于应用程序的大小和复杂性。

java.util.concurrent包中的某些类提供了不依赖同步的原子方法。

Liveness

并发应用程序及时执行的能力被称为实时性。

Deadlock(死锁)

死锁描述了一种情况,其中两个或多个线程永远被阻塞,互相等待。

阿方斯(Alphonse)和加斯顿(Gaston)是朋友,也是礼貌的忠实信徒。严格的礼节规则是当您向朋友鞠躬时,必须保持鞠躬,直到朋友有机会归还弓箭为止。不幸的是,该规则不能解决两个朋友可能同时鞠躬的可能性。该示例应用程序Deadlock对这种可能性进行了建模:

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时阻塞。两个块都不会结束,因为每个线程都在等待另一个退出。

Starvation and Livelock(饥饿与活锁)

与死锁相比,饥饿和活锁的问题要少得多,但仍然是每个并行软件设计人员都可能遇到的问题。

Starvation

饥饿描述了一种情况,即线程无法获得对共享资源的常规访问并且无法取得进展。当“贪婪”线程使共享资源长时间不可用时,就会发生这种情况。例如,假设一个对象提供了一个同步方法,该方法通常需要很长时间才能返回。如果一个线程频繁调用此方法,则也需要频繁同步访问同一对象的其他线程将经常被阻塞。

活锁

一个线程通常会响应另一个线程的操作而行动。如果另一个线程的动作也是对另一个线程的动作的响应,则可能会导致活锁。与死锁一样,活动锁定的线程无法取得进一步的进展。但是,线程没有被阻塞-它们只是太忙于彼此响应而无法恢复工作。这相当于两个人试图在走廊中互相经过:阿方斯(Alphonse)向左移动以让加斯顿(Gaston)通过,而格斯顿(Gaston)向右移动以让Alphonse通过。看到他们仍然互相阻挡,Alphone向右移动,而Gaston向左移动。他们仍然互相阻碍,所以...

Guarded Blocks

线程通常必须协调其动作。最常见的协调习惯是防护区。这样的块首先轮询一个条件,该条件必须为真,然后才能继续进行。例如,假设guardedJoy是在另一个线程设置了共享变量joy之前不能继续执行的方法。从理论上讲,这种方法可以简单地循环直到满足条件为止,但是这种循环是浪费的,因为它在等待时连续执行。

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

效率更高的防护调用Object.wait来挂起当前线程。在另一个线程发出可能已经发生某些特殊事件的通知之前,不会返回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。不要以为中断是针对您正在等待的特定条件,还是该条件仍然为真。

就像许多暂停执行的方法一样,wait可能会引发InterruptedException。

为什么此版本的guardedJoy是同步的?假设d是我们用来调用wait的对象。当线程调用d.wait时,它必须拥有d的固有锁-否则将引发错误。在同步方法中调用wait是获取内部锁的一种简单方法。

调用wait时,线程释放锁并中止执行。在将来的某个时间,另一个线程将获取相同的锁并调用Object.notifyAll,通知所有在该锁上等待的线程发生了重要的事情:

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

在第二个线程释放锁后的某个时间,第一个线程重新获取该锁,并通过从wait调用返回而恢复。

注意:还有第二种通知方法notify,它唤醒一个线程。因为notify不允许您指定唤醒的线程,所以它仅在大规模并行应用程序中有用,即,具有大量线程的程序,它们都执行类似的杂务。在这样的应用程序中,您不必担心哪个线程被唤醒。

让我们使用受保护的块来创建Producer-Consumer应用程序。这种应用程序在两个线程之间共享数据:生产者(创建数据)和消费者(使用数据)。这两个线程使用共享对象进行通信。协调是必不可少的:使用者线程不得在生产者线程传递数据之前尝试检索数据,并且如果使用者没有检索到旧数据,则生产者线程不得尝试传递新数据。

在此示例中,数据是一系列文本消息,这些消息通过类型为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类是为了演示受保护的块。为了避免重新发明轮子,请在尝试编写自己的数据共享对象之前检查Java Collections Framework中的现有数据结构。

Immutable Objects不变的对象

如果对象的状态在构造后无法更改,则认为该对象是不可变的。对不可变对象的最大依赖已被广泛认为是创建简单,可靠代码的合理策略。

不可变对象在并发应用程序中特别有用。由于它们不能更改状态,因此它们不能被线程干扰破坏或在不一致的状态下观察到。

程序员通常不愿使用不可变的对象,因为他们担心创建新对象而不是就地更新对象的成本。对象创建的影响常常被高估,并且可以被与不变对象相关联的某些效率所抵消。其中包括由于垃圾收集而减少的开销,以及消除了保护可变对象免受损坏所需的代码。

同步类示例(Synchronized Class)

SynchronizedRGB类定义表示颜色的对象。每个对象将颜色表示为三个代表原始颜色值的整数,以及一个给出颜色名称的字符串。

public class SynchronizedRGB {

    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of " + name;
    }
}

SynchronizedRGB必须小心使用,以免出现不一致状态。例如,假设一个线程执行以下代码:

SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      //Statement 1
String myColorName = color.getName(); //Statement 2

如果另一个线程在语句1之后但在语句2之前调用color.set,则myColorInt的值将与myColorName的值不匹配。为了避免这种结果,必须将两个语句绑定在一起:

synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
} 

这种不一致性仅适用于可变对象,而对于不可变版本的SynchronizedRGB而言,这不会成为问题。

定义不可变对象的策略

以下规则定义了创建不可变对象的简单策略。并非所有记录为“不可变”的类都遵循这些规则。这并不一定意味着这些类的创建者很草率–他们可能有充分的理由相信,这些类的实例在构造后不会改变。但是,此类策略需要复杂的分析,并不适合初学者。

  • 不要提供“setter”方法-修改字段或字段引用的对象的方法。

  • 使所有字段均为最终字段和私有字段。

  • 不允许子类覆盖方法。最简单的方法是将类声明为final。一种更复杂的方法是使构造函数私有,并在工厂方法中构造实例。

  • 如果实例字段包含对可变对象的引用,则不允许更改这些对象:

    • 不要提供修改可变对象的方法。
    • 不要共享对可变对象的引用。永远不要存储对传递给构造函数的外部可变对象的引用;如有必要,创建副本,并存储对副本的引用。同样,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

将此策略应用于SynchronizedRGB将导致以下步骤:

  1. 此类中有两种设置方法。set的第一个对象任意变换对象,并且在类的不可变版本中没有位置。通过使第二个对象反转,可以通过创建一个新对象而不是修改现有对象来对其进行修改。
  2. 所有字段都是private的;他们进一步获得了final资格。
  3. 该类本身被声明为final
  4. 只有一个字段引用一个对象,而该对象本身是不可变的。因此,没有必要采取措施来防止更改“包含”的可变对象的状态。

完成这些更改后,我们得到了ImmutableRGB:

final public class ImmutableRGB {

    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }


    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }

    public ImmutableRGB invert() {
        return new ImmutableRGB(255 - red,
                       255 - green,
                       255 - blue,
                       "Inverse of " + name);
    }
}

高级并发对象

到目前为止,本课程重点关注从一开始就已成为Java平台一部分的低级API。这些API足以满足非常基本的任务,但更高级的任务则需要更高级别的构建块。对于充分利用当今的多处理器和多核系统的大规模并发应用程序尤其如此。

在本节中,我们将介绍Java平台5.0版引入的一些高级并发功能。这些功能大多数都在新的java.util.concurrent包中实现。Java Collections Framework中还有新的并发数据结构。

  • Lock objects支持简化许多并发应用程序的锁定习惯用法。
  • Executors 定义了用于启动和管理线程的高级API。java.util.concurrent提供的执行程序实现提供适用于大规模应用程序的线程池管理。
  • Concurrent collections 使管理大型数据收集更加容易,并且可以大大减少同步需求。
  • Atomic variables具有可最大程度减少同步并有助于避免内存一致性错误的功能。
  • ThreadLocalRandom(在JDK 7中)提供了从多个线程高效生成伪随机数的功能。

Lock Objects

同步代码依赖于一种简单的可重入锁。这种锁易于使用,但有很多限制。java.util.concurrent.locks包支持更复杂的锁定习惯用法。我们不会详细研究此软件包,而是将重点放在其最基本的接口Lock上。

Lock对象的工作方式非常类似于同步代码使用的隐式锁。与隐式锁一样,一次只能有一个线程拥有一个Lock对象。Lock对象还通过其关联的Condition对象支持wait/notify机制。

与隐式锁相比,Lock对象的最大优点是它们能够回避获取锁的企图。如果没有立即或在超时到期之前没有可用的锁,则tryLock方法将退出(如果指定)。如果在获取锁之前另一个线程发送了中断,则lockInterruptibly方法将退出。

让我们使用Lock对象解决在Liveness中看到的死锁问题。阿方斯(Alphonse)和加斯顿(Gaston)经过自我训练,可以注意到朋友何时要鞠躬。我们通过要求Friend对象在进行弓箭操作之前必须为两个参与者都获得锁来为这种改进建模。这是改进的模型Safelock的源代码。为了证明这一习语的通用性,我们假设阿方斯和加斯顿对新发现的安全鞠躬能力非常着迷,以至于他们无法停止互相鞠躬:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;

public class Safelock {
    static class Friend {
        private final String name;
        private final Lock lock = new ReentrantLock();

        public Friend(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        public boolean impendingBow(Friend bower) {
            Boolean myLock = false;
            Boolean yourLock = false;
            try {
                myLock = lock.tryLock();
                yourLock = bower.lock.tryLock();
            } finally {
                if (! (myLock && yourLock)) {
                    if (myLock) {
                        lock.unlock();
                    }
                    if (yourLock) {
                        bower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }
            
        public void bow(Friend bower) {
            if (impendingBow(bower)) {
                try {
                    System.out.format("%s: %s has"
                        + " bowed to me!%n", 
                        this.name, bower.getName());
                    bower.bowBack(this);
                } finally {
                    lock.unlock();
                    bower.lock.unlock();
                }
            } else {
                System.out.format("%s: %s started"
                    + " to bow to me, but saw that"
                    + " I was already bowing to"
                    + " him.%n",
                    this.name, bower.getName());
            }
        }

        public void bowBack(Friend bower) {
            System.out.format("%s: %s has" +
                " bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    static class BowLoop implements Runnable {
        private Friend bower;
        private Friend bowee;

        public BowLoop(Friend bower, Friend bowee) {
            this.bower = bower;
            this.bowee = bowee;
        }
    
        public void run() {
            Random random = new Random();
            for (;;) {
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {}
                bowee.bow(bower);
            }
        }
    }
            

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

Executors

在前面的所有示例中,由新线程(由其Runnable对象定义)执行的任务与由Thread对象定义的线程本身之间存在紧密的联系。这对于小型应用程序非常有效,但是在大型应用程序中,将线程管理和创建与其余应用程序分开是有意义的。封装这些功能的对象称为执行程序。

  • Executor Interfaces接口定义了三种执行程序对象类型。
  • Thread Pools 是最常见的执行程序实现。
  • Fork/Join 是用于利用多个处理器的框架(JDK 7中的新增功能)。

Executor Interfaces

java.util.concurrent包定义了三个执行程序接口:

  • Executor,一个简单的界面,支持启动新任务。
  • ExecutorServiceExecutor的子接口,它添加了有助于管理生命周期的功能,包括单个任务和执行者本身。
  • ScheduledExecutorServiceExecutorService的子接口,支持将来和/或定期执行任务。

通常,引用执行程序对象的变量被声明为这三种接口类型之一,而不是执行程序类类型。

The Executor Interface

Executor接口提供了一种execute方法,旨在替代常用的线程创建习惯用法。如果r是Runnable对象,而e是Executor对象,则可以替换

(new Thread(r)).start();
与
e.execute(r);

但是,execute的定义不太具体。低级习惯创建一个新线程并立即启动它。取决于Executor的实现,execute可能执行相同的操作,但更可能使用现有的工作线程来运行r,或将r放入队列中以等待工作线程可用。

尽管java.util.concurrent中的执行程序实现也与基本Executor接口一起使用,但它们的设计目的是充分利用更高级的ExecutorServiceScheduledExecutorService接口。

The ExecutorService Interface

ExecutorService接口补充了使用类似但功能更广泛的Submit方法执行的功能。像execute一样,submit接受Runnable对象,但也接受Callable对象,后者允许任务返回值。Submit方法返回一个Future对象,该对象用于检索Callable返回值并管理Callable和Runnable任务的状态。

ExecutorService还提供用于提交大量Callable对象的方法。最后,ExecutorService提供了许多方法来管理执行器的关闭。为了支持立即关闭,任务应正确处理中断。

The ScheduledExecutorService Interface

ScheduledExecutorService接口使用schedule补充其父ExecutorService的方法,该schedule在指定的延迟后执行Runnable或Callable任务。另外,该接口定义了scheduleAtFixedRate和scheduleWithFixedDelay,它们以定义的间隔重复执行指定的任务。

Thread Pools(线程池)

java.util.concurrent中的大多数执行程序实现都使用线程池,该线程池由工作线程组成。这种线程与它执行的Runnable和Callable任务分开存在,通常用于执行多个任务。

使用工作线程可以最大程度地减少线程创建所带来的开销。线程对象占用大量内存,在大型应用程序中,分配和取消分配许多线程对象会产生大量内存管理开销。

线程池的一种常见类型是固定线程池。这种类型的池始终具有指定数量的正在运行的线程。如果某个线程在仍在使用时以某种方式终止,则它将自动替换为新线程。任务通过内部队列提交到池中,该内部队列在活动任务多于线程时容纳额外的任务。

固定线程池的一个重要优点是使用该线程池的应用程序可以正常降级。了理解这一点,请考虑一个Web服务器应用程序,其中每个HTTP请求都由一个单独的线程处理。如果应用程序只是为每个新的HTTP请求创建一个新线程,并且系统收到的请求超出了立即处理的请求,则当所有这些线程的开销超出系统容量时,应用程序将突然停止响应所有请求。由于可以创建的线程数受到限制,因此应用程序将不会尽快处理HTTP请求,但是会尽可能快地为系统提供服务。

创建使用固定线程池的执行程序的一种简单方法是调用java.util.concurrent.Executors中的newFixedThreadPool工厂方法。此类还提供以下工厂方法:

  • newCachedThreadPool方法创建具有可扩展线程池的执行程序。该执行程序适用于启动许多短期任务的应用程序。
  • newSingleThreadExecutor方法创建一个执行程序,一次执行一个任务。
  • 上述执行程序的ScheduledExecutorService版本是一些工厂方法。

如果上述工厂方法提供的执行程序都不满足您的需求,则构造java.util.concurrent.ThreadPoolExecutor或java.util.concurrent.ScheduledThreadPoolExecutor的实例将为您提供其他选项。

Fork/Join

fork/join框架是ExecutorService接口的实现,可帮助您利用多个处理器。它是为可以递归分解为较小部分的工作而设计的。目标是使用所有可用的处理能力来增强应用程序的性能。

与任何ExecutorService实现一样,fork / join框架将任务分配给线程池中的工作线程。fork / join框架与众不同,因为它使用工作窃取算法。工作用尽的工作线程可以从其他仍很忙的线程中窃取任务。

fork / join框架的中心是ForkJoinPool类,它是AbstractExecutorService类的扩展。ForkJoinPool实现了核心的工作窃取算法,并且可以执行ForkJoinTask进程。

Basic Use

使用fork / join框架的第一步是编写执行部分工作的代码。您的代码应类似于以下伪代码:

if (my portion of the work is small enough[我的工作量很小])
  do the work directly(直接做工作)
else
  split my work into two pieces
  invoke the two pieces and wait for the results
  (将我的工作分为两部分   调用两个片段并等待结果)

将此代码包装在ForkJoinTask子类中,通常使用其更专门的类型之一,即RecursiveTask(可以返回结果)或RecursiveAction。

在准备好ForkJoinTask子类之后,创建代表所有要完成的工作的对象,并将其传递给ForkJoinPool实例的invoke()方法。

Blurring for Clarity

为了帮助您了解fork / join框架的工作原理,请考虑以下示例。假设您要模糊图像。原始source 图像由整数数组表示,其中每个整数都包含单个像素的颜色值。模糊的destination图像还由与源大小相同的整数数组表示。

通过一次在源阵列中处理一个像素来完成模糊处理。将每个像素与其周围的像素进行平均(对红色,绿色和蓝色分量进行平均),然后将结果放置在目标数组中。由于图像是大阵列,因此此过程可能需要很长时间。通过使用fork / join框架实现算法,可以在多处理器系统上利用并发处理的优势。这是一种可能的实现:

public class ForkBlur extends RecursiveAction {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;
  
    // Processing window size; should be odd.   处理窗口大小;应该Odd
    private int mBlurWidth = 15;
  
    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    protected void computeDirectly() {
        int sidePixels = (mBlurWidth - 1) / 2;
        for (int index = mStart; index < mStart + mLength; index++) {
            // Calculate average. 计算平均值。
            float rt = 0, gt = 0, bt = 0;
            for (int mi = -sidePixels; mi <= sidePixels; mi++) {
                int mindex = Math.min(Math.max(mi + index, 0),
                                    mSource.length - 1);
                int pixel = mSource[mindex];
                rt += (float)((pixel & 0x00ff0000) >> 16)
                      / mBlurWidth;
                gt += (float)((pixel & 0x0000ff00) >>  8)
                      / mBlurWidth;
                bt += (float)((pixel & 0x000000ff) >>  0)
                      / mBlurWidth;
            }
          
            // Reassemble destination pixel. 重新组装目标像素
            int dpixel = (0xff000000     ) |
                   (((int)rt) << 16) |
                   (((int)gt) <<  8) |
                   (((int)bt) <<  0);
            mDestination[index] = dpixel;
        }
    }

现在,您可以实现抽象的compute()方法,该方法可以直接执行模糊处理,也可以将其拆分为两个较小的任务。简单的阵列长度阈值有助于确定是执行工作还是拆分工作。

protected static int sThreshold = 100000;

protected void compute() {
    if (mLength < sThreshold) {
        computeDirectly();
        return;
    }
    
    int split = mLength / 2;
    
    invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
              new ForkBlur(mSource, mStart + split, mLength - split,
                           mDestination));
}

如果先前的方法在RecursiveAction类的子类中,则设置要在ForkJoinPool中运行的任务很简单,并且涉及以下步骤:

  1. 创建一个代表所有要完成工作的任务。

    // source image pixels are in src  源图像像素在src中 
    // destination image pixels are in dst  目标图像像素在dst中
    ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
    
  2. 创建将运行任务的ForkJoinPool。

    ForkJoinPool pool = new ForkJoinPool();
    
  3. 运行任务

    pool.invoke(fb);
    

有关完整的源代码,包括一些用于创建目标图像文件的其他代码,请参见ForkBlur示例。

Standard Implementations

除了使用fork / join框架为要在多处理器系统上同时执行的任务实现自定义算法(例如上一节中的ForkBlur.java示例)之外,Java SE中还具有一些通常有用的功能,这些功能已经使用fork / join框架。Java SE 8中引入的一种此类实现由java.util.Arrays类用于其parallelSort()方法。这些方法类似于sort(),但是通过fork / join框架利用并发性。在多处理器系统上运行时,大型数组的并行排序比顺序排序要快。但是,这些方法如何精确地使用fork / join框架超出了Java教程的范围。有关此信息,请参阅Java API文档。

java.util.streams包中的方法使用fork / join框架的另一种实现,该包是计划在Java SE 8发行版中的Project Lambda的一部分。有关更多信息,请参见Lambda表达式部分。

Concurrent Collections

java.util.concurrent包包括Java Collections Framework的许多附加功能。通过提供的收集接口可以很容易地对它们进行分类:

  • BlockingQueue定义了先进先出(first-in-first-out)数据结构,当您尝试添加到完整队列或从空队列中检索时,该数据结构将阻塞或超时。
  • ConcurrentMap是java.util.Map的子接口,它定义了有用的原子操作。这些操作仅在存在键时才删除或替换键值对,或者仅在键不存在时才添加键值对。使这些操作原子化有助于避免同步。ConcurrentMap的标准通用实现是ConcurrentHashMap,它是HashMap的并发类似物。
  • ConcurrentNavigableMap是ConcurrentMap的子接口,支持近似匹配。ConcurrentNavigableMap的标准通用实现是ConcurrentSkipListMap,它是TreeMap的并发类似物。

所有这些集合都通过在将对象添加到集合的操作与访问或删除该对象的后续操作之间定义事前事前关系来帮助避免内存一致性错误(Memory Consistency Errors).

Atomic(原子) Variables

java.util.concurrent.atomic包定义了支持对单个变量进行原子操作的类。所有类都具有get和set方法,它们的工作方式类似于对volatile变量的读写。也就是说,一个set与该变量的任何后续get都具有事前发生的关系。原子的compareAndSet方法还具有这些内存一致性功能,适用于整数原子变量的简单原子算术方法也是如此。

要查看如何使用此包,让我们返回最初用于演示线程干扰的Counter类:

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

使Counter免受线程干扰的一种方法是使其方法同步,如SynchronizedCounter中所示:

class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}

对于这个简单的类,同步是可以接受的解决方案。但是对于更复杂的类,我们可能要避免不必要的同步对活动的影响。用AtomicInteger替换int字段使我们能够防止线程干扰而无需求助于同步,如AtomicCounter中那样:

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

Concurrent Random Numbers

在JDK 7中,java.util.concurrent包含一个便利类ThreadLocalRandom,用于希望使用来自多个线程或ForkJoinTasks的随机数的应用程序。

对于并发访问,使用ThreadLocalRandom代替Math.random()可以减少争用并最终提高性能。

您需要做的就是调用ThreadLocalRandom.current(),然后调用其方法之一以检索随机数。这是一个例子:

int r = ThreadLocalRandom.current() .nextInt(4, 77);

完成翻译,翻译原文地址

复制请注明出处,在世界中挣扎的灰太狼
原文地址:https://www.cnblogs.com/XingXiaoMeng/p/13020387.html