计算机的使用者一直觉得他们的计算机能够同时作不少事情。他们认为当其余的应用程序在下载文件,管理打印队列或者缓冲音频的时候他们能够继续在文字处理程序上工做。甚至对于单个应用程序,他们任然期待它能在在同一时间作不少事情。举个例子,一个流媒体播放程序必须能同时完成如下工做:从网络上读取数字音频,解压缩数字音频,管理播放和更新程序显示。甚至文字处理器也应该能在忙于从新格式化文本和刷新显示的状况下同时响应键盘和鼠标事件。这样的软件就被称为并发软件。
经过Java语言和Java类库对于基础并发的支持,Java平台具备彻底(from the ground up )支持并发编程的能力。从JDK5.0起,Java平台还引入了高级并发APIs。这个课程不只涵盖了Java平台基础并发内容,还对高级并发APIs有必定的阐述。
进程和线程
(本部分
原文连接,
译文连接,译者:bjsuo,校对:郑旭东)
在并发编程中,有两个基本的执行单元:进程和线程。在java语言中,并发编程最关心的是线程,然而,进程也是很是重要的。
即便在只有单一的执行核心的计算机系统中,也有许多活动的进程和线程。所以,在任何给定的时刻,只有一个线程在实际执行。处理器的处理时间是经过操做系统的时间片在进程和线程中共享的。
如今具备多处理器或有多个执行内核的多处理器的计算机系统愈来愈广泛,这大大加强了系统并发执行的进程和线程的吞吐量–但在不没有多个处理器或执行内核的简单的系统中,并发任然是可能的。
进程
进程具备一个独立的执行环境。一般状况下,进程拥有一个完整的、私有的基本运行资源集合。特别地,每一个进程都有本身的内存空间。
进程每每被看做是程序或应用的代名词,然而,用户看到的一个单独的应用程序实际上多是一组相互协做的进程集合。为了便于进程之间的通讯,大多数操做系统都支持进程间通讯(IPC),如pipes 和sockets。IPC不只支持同一系统上的通讯,也支持不一样的系统。
Java虚拟机的大多数实现是单进程的。Java应用可使用的ProcessBuilder对象建立额外的进程,多进程应用超出了本课的范围。
线程
线程有时也被称为轻量级的进程。进程和线程都提供了一个执行环境,但建立一个新的线程比建立一个新的进程须要的资源要少。
线程是在进程中存在的 — 每一个进程最少有一个线程。线程共享进程的资源,包括内存和打开的文件。这样提升了效率,但潜在的问题就是线程间的通讯。
多线程的执行是Java平台的一个基本特征。每一个应用都至少有一个线程 – 或几个,若是算上“系统”线程的话,好比内存管理和信号处理等。可是从程序员的角度来看,启动的只有一个线程,叫主线程。这个线程有能力建立额外的线程,咱们将在下一节演示。
线程对象
同步
(本部分
原文连接,
译文连接,译者:蘑菇街-小宝,
Greenster,
李任 校对:丁一,郑旭东,
李任)
线程间的通讯主要是经过共享域和引用相同的对象。这种通讯方式很是高效,不过可能会引起两种错误:线程干扰和内存一致性错误。防止这些错误发生的方法是同步。
不过,同步会引发线程竞争,当两个或多个线程试图同时访问相同的资源,随之就致使Java运行时环境执行其中一个或多个线程比原先慢不少,甚至执行被挂起,这就出现了线程竞争。线程饥饿和活锁都属于线程竞争的范畴。关于线程竞争的更多信息可参考活跃度一节。
本节内容包括如下这些主题:
- 线程干扰讨论了当多个线程访问共享数据时错误是怎么发生的。
- 内存一致性错误讨论了不一致的共享内存视图致使的错误。
- 同步方法讨论了 一种能有效防止线程干扰和内存一致性错误的常见作法。
- 内部锁和同步讨论了更通用的同步方法,以及同步是如何基于内部锁实现的。
- 原子访问讨论了不能被其余线程干扰的操做的整体思路。
1. 线程干扰
下面这个简单的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对象被多个线程引用,线程间的干扰可能会使结果同咱们预期的不一致。
当两个运行在不一样的线程中却做用在相同的数据上的操做交替执行时,就发生了线程干扰。这意味着这两个操做都由多个步骤组成,而步骤间的顺序产生了重叠。
Counter类实例的操做会交替执行,这看起来彷佛不太可能,由于c上的这两个操做都是单一而简单的语句。然而,即便一个简单的语句也会被虚拟机转换成多个步骤。咱们不去深究虚拟机内部的详细执行步骤——理解c++这个单一的语句会被分解成3个步骤就足够了:
- 获取当前c的值;
- 对获取到的值加1;
- 把递增后的值写回到c;
语句c–也能够按一样的方式分解,除了第二步的操做是递减而不是递增。
假设线程A调用increment()的同时线程B调用decrement().若是c的初始值为0,线程A和B之间的交替执行顺序多是下面这样:
- 线程A:获取c;
- 线程B:获取c;
- 线程A:对获取的值加1,结果为1;
- 线程B:对获取的值减1,结果为-1;
- 线程A:结果写回到c,c如今是1;
- 线程B:结果写回到c,c如今是-1;
线程A的结果由于被线程B覆盖而丢失了。这个交替执行的结果只是其中一种可能性。在不一样的环境下,多是线程B的结果丢失了,也多是不会出任何问题。因为结果是不可预知的,因此线程干扰的bug很难检测和修复。
2. 内存一致性错误
当不一样的线程对相同的数据产生不一致的视图时会发生内存一致性错误。内存一致性错误的缘由比较复杂,也超出了本教程的范围。不过幸运的是,一个程序员并不须要对这些缘由有详细的了解。所须要的是避免它们的策略。
避免内存一致性错误的关键是理解happens-before关系。这种关系只是确保一个特定语句的写内存操做对另一个特定的语句可见。要说明这个问题,请参考下面的例子。假设定义和初始化了一个简单int字段:
这个counter字段被A,B两个线程共享。假设线程A对counter执行递增:
而后,很快的,线程B输出counter:
- System.out.println(counter);
若是这两个语句已经在同一个线程中被执行过,那么输出的值应该是“1”。不过若是这两个语句在不一样的线程中分开执行,那输出的值极可能是“0”,由于没法保证线程A对counter的改动对线程B是可见的——除非咱们在这两个语句之间已经创建了happens-before关系。
有许多操做会创建happens-before关系。其中一个是同步,咱们将在下面的章节中看到。
咱们已经见过两个创建happens-before关系的操做。
当一条语句调用Thread.start方法时,和该语句有happens-before关系的每一条语句,跟新线程执行的每一条语句一样有happens-before关系。建立新线程以前的代码的执行结果对线新线程是可见的。
当一个线程终止而且当致使另外一个线程中Thread.join返回时,被终止的线程执行的全部语句和在join返回成功以后的全部语句间有happens-before关系。线程中代码的执行结果对执行join操做的线程是可见的。
要查看创建happens-before关系的操做列表,请参阅java.util.concurrent包的
摘要页面。
3. 同步方法
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类的实例,那么让这些方法成为同步方法有两个做用:
首先,相同对象上的同步方法的两次调用,它们要交替执行是不可能的。 当一个线程正在执行对象的同步方法时,全部其余调用该对象同步方法的线程会被阻塞(挂起执行),直到第一个线程处理完该对象。
其次,当一个同步方法退出时,它会自动跟该对象同步方法的任意后续调用创建起一种happens-before关系。这确保对象状态的改变对全部线程是可见的。
注意构造方法不能是同步的——构造方法加synchronized关键字会报语法错误。同步的构造方法没有意义,由于当这个对象被建立的时候,只有建立对象的线程能访问它。
警告:当建立的对象会被多个线程共享时必须很是当心,对象的引用不要过早“暴露”出去。好比,假设你要维护一个叫instances的List,它包含类的每个实例对象。你可能会尝试在构造方法中加这样一行:
不过其余线程就可以在对象构造完成以前使用instances访问对象。
同步(synchronized)方法使用一种简单的策略来防止线程干扰和内存一致性错误:若是一个对象对多个线程可见,对象域上的全部读写操做都是经过synchronized方法来完成的。(一个重要的例外:final域,在对象被建立后不可修改,能被非synchronized方法安全的读取)。synchronized同步策略颇有效,不过会引发活跃度问题,咱们将在本节后面看到。
4. 内部锁与同步
同步机制的创建是基于其内部一个叫内部锁或者监视锁的实体。(在Java API规范中一般被称为监视器。)内部锁在同步机制中起到两方面的做用:对一个对象的排他性访问;创建一种happens-before关系,而这种关系正是可见性问题的关键所在。
每一个对象都有一个与之关联的内部锁。一般当一个线程须要排他性的访问一个对象的域时,首先须要请求该对象的内部锁,当访问结束时释放内部锁。在线程得到内部锁到释放内部锁的这段时间里,咱们说线程拥有这个内部锁。那么当一个线程拥有一个内部锁时,其余线程将没法得到该内部锁。其余线程若是去尝试得到该内部锁,则会被阻塞。
当线程释放一个内部锁时,该操做和对该锁的后续请求间将创建happens-before关系。
5. 同步方法中的锁
当线程调用一个同步方法时,它会自动请求该方法所在对象的内部锁。当方法返回结束时则自动释放该内部锁,即便退出是因为发生了未捕获的异常,内部锁也会被释放。
你可能会问调用一个静态的同步方法会如何,因为静态方法是和类(而不是对象)相关的,因此线程会请求类对象(Class Object)的内部锁。所以用来控制类的静态域访问的锁不一样于控制对象访问的锁。
6. 同步块
另一种同步的方法是使用同步块。和同步方法不一样,同步块必须指定所请求的是哪一个对象的内部锁:
- public void addName(String name) {
- synchronized(this) {
- lastName = name;
- nameCount++;
- }
- nameList.add(name);
- }
在上面的例子中,addName方法须要使lastName和nameCount的更改保持同步,并且要避免同步调用该对象的其余方法。(在同步代码中调用其余方法会产生
Liveness一节所描述的问题。)若是不使用同步块,那么必需要定义一个额外的非同步方法,而这个方法仅仅是用来调用nameList.add。
使用同步块对于更细粒度的同步颇有帮助。例如类MsLunch有两个实例域c1和c2,他们并不会同时使用(译者注:即c1和c2是彼此无关的两个域),全部对这两个域的更新都须要同步,可是彻底不须要防止c1的修改和c2的修改相互之间干扰(这样作只会产生没必要要的阻塞而下降了并发性)。这种状况下没必要使用同步方法,可使用和this对象相关的锁。这里咱们建立了两个“锁”对象(译者注:起到加锁效果的普通对象lock1和lock2)。
- 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++;
- }
- }
- }
使用这种方法时要特别当心,须要十分肯定c1和c2是彼此无关的域。
7. 可重入同步
还记得吗,一个线程不能得到其余线程所拥有的锁。可是它能够得到本身已经拥有的锁。容许一个线程屡次得到同一个锁实现了可重入同步。这里描述了一种同步代码的场景,直接的或间接地,调用了一个也拥有同步代码的方法,且两边的代码使用的是同一把锁。若是没有这种可重入的同步机制,同步代码则须要采起许多额外的预防措施以防止线程阻塞本身。
8. 原子访问
在编程过程当中,原子操做是指全部操做都同时发生。原子操做不能被中途打断:要么全作,要么不作。原子操做在完成前不会有看得见的反作用。
咱们发现像c++这样的增量表达式,并无描述原子操做。即便是很是简单的表达式也可以定义成能被分解为其余操做的复杂操做。然而,有些操做你能够定义为原子的:
- 对引用变量和大部分基本类型变量(除long和double以外)的读写是原子的。
- 对全部声明为volatile的变量(包括long和double变量)的读写是原子的。
原子操做不会交错,因而能够放心使用,没必要担忧线程干扰。然而,这并不能彻底消除原子操做上的同步,由于内存一致性错误仍可能发生。使用volatile变量能够下降内存一致性错误的风险,由于对volatile变量的任意写操做,对于后续在该变量上的读操做创建了happens-before关系。这意味着volatile变量的修改对于其余线程老是可见的。更重要的是,这同时也意味着当一个线程读取一个volatile变量时,它不只能看到该变量最新的修改,并且也能看到导致该改变发生的代码的副效应。
使用简单的原子变量访问比经过同步代码来访问更高效,可是须要程序员更加谨慎以免内存一致性错误。至于这额外的付出是否值得,得看应用的大小和复杂度。
java.util.concurrent包中的一些类提供了一些不依赖同步机制的原子方法。咱们将在高级并发对象这一节中讨论它们。
活跃度
(本部分
原文地址,
译文地址,译者:
李任,郑旭东 校对:
蘑菇街-小宝)
一个并发应用程序能及时执行的能力称为活跃性。本节将介绍最多见的活跃性问题:死锁(deadlock),以及另外两个活跃性问题:饥饿(starvation)和活锁(livelock)。
1. 死锁
死锁描述了这样一种情景,两个或多个线程永久阻塞,互相等待对方释放资源。下面是一个例子。
Alphone和Gaston是朋友,都很讲究礼节。礼节有一个严格的规矩,当你向一个朋友鞠躬时,你必须保持鞠躬的姿式,直到你的朋友有机会回鞠给你。不幸的是,这个规矩没有算上两个朋友相互同时鞠躬的可能。
下面的应用例子,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();
- }
- }
当DeadLock运行后,两个线程极有可能阻塞,当它们尝试调用bowBack方法时。没有哪一个阻塞会结束,由于每一个线程都在等待另外一个线程退出bow方法。
2. 饥饿和活锁
饥饿和活锁并不如死锁通常广泛,但它仍然是每一个并发程序设计者可能会遇到的问题。
饥饿
饥饿是指当一个线程不能正常的访问共享资源而且不能正常执行的状况。这一般在共享资源被其余“贪心”的线程长期时发生。举个例子,假设一个对象提供了一个同步方法,这个方法一般须要执行很长一段时间才返回。若是一个线程常常调用这个方法,那么其余须要同步的访问这个对象的线程就常常会被阻塞。
活锁
一个线程一般会有会响应其余线程的活动。若是其余线程也会响应另外一个线程的活动,那么就有可能发生活锁。同死锁同样,发生活锁的线程没法继续执行。然而线程并无阻塞——他们在忙于响应对方没法恢复工做。这就至关于两个在走廊相遇的人:Alphonse向他本身的左边靠想让Gaston过去,而Gaston向他的右边靠想让Alphonse过去。可见他们阻塞了对方。Alphonse向他的右边靠,而Gaston向他的左边靠,他们仍是阻塞了对方。
保护块(Guarded Blocks)
(本部分
原文链接,
译文链接,译者:Greester,校对:郑旭东)
多线程之间常常须要协同工做,最多见的方式是使用Guarded Blocks,它循环检查一个条件(一般初始值为true),直到条件发生变化才跳出循环继续执行。在使用Guarded Blocks时有如下几个步骤须要注意:
假设guardedJoy()方法必需要等待另外一线程为共享变量joy设值才能继续执行。那么理论上能够用一个简单的条件循环来实现,但在等待过程当中guardedJoy方法不停的检查循环条件其实是一种资源浪费。
- public void guardedJoy() {
-
-
- while(!joy) {}
- System.out.println("Joy has been achieved!");
- }
更加高效的方法是调用Object.wait将当前线程挂起,直到有另外一线程发起事件通知(尽管通知的事件不必定是当前线程等待的事件)。
- public synchronized void guardedJoy() {
-
-
- while(!joy) {
- try {
- wait();
- } catch (InterruptedException e) {}
- }
- System.out.println("Joy and efficiency have been achieved!");
- }
注意:必定要在循环里面调用wait方法,不要想固然的认为线程唤醒后循环条件必定发生了改变。
和其余能够暂停线程执行的方法同样,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 {
-
-
- private String message;
-
-
-
-
- private boolean empty = true;
-
- public synchronized String take() {
-
-
- while (empty) {
- try {
- wait();
- } catch (InterruptedException e) {}
- }
-
- empty = true;
-
-
- notifyAll();
- return message;
- }
-
- public synchronized void put(String message) {
-
-
- while (!empty) {
- try {
- wait();
- } catch (InterruptedException e) {}
- }
-
- empty = false;
-
- this.message = message;
-
-
- 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。
不可变对象
(本部分
原文连接,
译文连接,译者:Greenster,校对:郑旭东)
一个对象若是在建立后不能被修改,那么就称为不可变对象。在并发编程中,一种被广泛承认的原则就是:尽量的使用不可变对象来建立简单、可靠的代码。
在并发编程中,不可变对象特别有用。因为建立后不能被修改,因此不会出现因为线程干扰产生的错误或是内存一致性错误。
可是程序员们一般并不热衷于使用不可变对象,由于他们担忧每次建立新对象的开销。实际上这种开销经常被过度高估,并且使用不可变对象所带来的一些效率提高也抵消了这种开销。例如:使用不可变对象下降了垃圾回收所产生的额外开销,也减小了用来确保使用可变对象不出现并发错误的一些额外代码。
接下来看一个可变对象的类,而后转化为一个不可变对象的类。经过这个例子说明转化的原则以及使用不可变对象的好处。
一个同步类的例子
SynchronizedRGB是表示颜色的类,每个对象表明一种颜色,使用三个整形数表示颜色的三基色,字符串表示颜色名称。
- public class SynchronizedRGB {
-
-
- 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();
- String myColorName = color.getName();
若是有另一个线程在Statement 1以后、Statement 2以前调用了color.set方法,那么myColorInt的值和myColorName的值就会不匹配。为了不出现这样的结果,必需要像下面这样把这两条语句绑定到一块执行:
- synchronized (color) {
- int myColorInt = color.getRGB();
- String myColorName = color.getName();
- }
这种不一致的问题只可能发生在可变对象上。
定义不可变对象的策略
如下的一些规则是建立不可变对象的简单策略。并不是全部不可变类都彻底遵照这些规则,不过这不是编写这些类的程序员们粗枝大叶形成的,极可能的是他们有充分的理由确保这些对象在建立后不会被修改。但这须要很是复杂细致的分析,并不适用于初学者。
- 不要提供setter方法。(包括修改字段的方法和修改字段引用对象的方法)
- 将类的全部字段定义为final、private的。
- 不容许子类重写方法。简单的办法是将类声明为final,更好的方法是将构造函数声明为私有的,经过工厂方法建立对象。
- 若是类的字段是对可变对象的引用,不容许修改被引用对象。
·不提供修改可变对象的方法。
·不共享可变对象的引用。当一个引用被当作参数传递给构造函数,而这个引用指向的是一个外部的可变对象时,必定不要保存这个引用。若是必需要保存,那么建立可变对象的拷贝,而后保存拷贝对象的引用。一样若是须要返回内部的可变对象时,不要返回可变对象自己,而是返回其拷贝。
将这一策略应用到SynchronizedRGB有如下几步:
- SynchronizedRGB类有两个setter方法。第一个set方法只是简单的为字段设值(译者注:删掉便可),第二个invert方法修改成建立一个新对象,而不是在原有对象上修改。
- 全部的字段都已是私有的,加上final便可。
- 将类声明为final的
- 只有一个字段是对象引用,而且被引用的对象也是不可变对象。
通过以上这些修改后,咱们获得了
ImmutableRGB:
- final public class ImmutableRGB {
-
-
- 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对于很是基本的任务来讲已经足够,可是对于更高级的任务就须要更高级的API。特别是针对充分利用了当今多处理器和多核系统的大规模并发应用程序。 本节,咱们将着眼于Java 5.0新增的一些高级并发特征。大多数特征已经在新的java.util.concurrent包中实现。Java集合框架中也定义了新的并发数据结构。
- 锁对象提供了能够简化许多并发应用的锁的惯用法。
- Executors为加载和管理线程定义了高级API。Executors的实现由java.util.concurrent包提供,提供了适合大规模应用的线程池管理。
- 并发集合简化了大型数据集合管理,且极大的减小了同步的需求。
- 原子变量有减少同步粒度和避免内存一致性错误的特征。
- 并发随机数(JDK7)提供了高效的多线程生成伪随机数的方法。
1. 锁对象
同步代码依赖于一种简单的可重入锁。这种锁使用简单,但也有诸多限制。
java.util.concurrent.locks包提供了更复杂的锁。咱们不会详细考察这个包,但会重点关注其最基本的接口,锁。 锁对象做用很是相似同步代码使用的隐式锁。如同隐式锁,每次只有一个线程能够得到锁对象。经过关联
Condition对象,锁对象也支持wait/notify机制。 锁对象之于隐式锁最大的优点在于,它们有能力收回得到锁的尝试。若是当前锁对象不可用,或者锁请求超时(若是超时时间已指定),tryLock方法会收回获取锁的请求。若是在锁获取前,另外一个线程发送了一个中断,lockInterruptibly方法也会收回获取锁的请求。 让咱们使用锁对象来解决咱们在
活跃度中见到的死锁问题。Alphonse和Gaston已经把本身训练成能注意到朋友什么时候要鞠躬。咱们经过要求Friend对象在双方鞠躬前必须先得到锁来模拟此次改善。下面是改善后模型的源代码,Safelock。为了展现其用途普遍,咱们假设Alphonse和Gaston对于他们新发现的稳定鞠躬的能力是如此入迷,以致于他们没法不相互鞠躬。
- 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();
- }
- }
2. 执行器(Executors)
在以前全部的例子中,Thread对象表示的线程和Runnable对象表示的线程所执行的任务之间是紧耦合的。这对于小型应用程序来讲没问题,但对于大规模并发应用来讲,合理的作法是将线程的建立与管理和程序的其余部分分离开。封装这些功能的对象就是执行器,接下来的部分将讲详细描述执行器。
3. Executor接口
java.util.concurrent中包括三个Executor接口:
- Executor,一个运行新任务的简单接口。
- ExecutorService,扩展了Executor接口。添加了一些用来管理执行器生命周期和任务生命周期的方法。
- ScheduledExecutorService,扩展了ExecutorService。支持Future和按期执行任务。
一般来讲,指向Executor对象的变量应被声明为以上三种接口之一,而不是具体的实现类。
Executor接口
Executor接口只有一个execute方法,用来替代一般建立(启动)线程的方法。例如:r是一个Runnable对象,e是一个Executor对象。可使用
来代替
但execute方法没有定义具体的实现方式。对于不一样的Executor实现,execute方法多是建立一个新线程并当即启动,但更有多是使用已有的工做线程运行r,或者将r放入到队列中等待可用的工做线程。(咱们将在线程池一节中描述工做线程。)
ExecutorService接口
ExecutorService接口在提供了execute方法的同时,新加了更加通用的submit方法。submit方法除了和execute方法同样能够接受Runnable对象做为参数,还能够接受Callable对象做为参数。使用Callable对象能够能使任务返还执行的结果。经过submit方法返回的Future对象能够读取Callable任务的执行结果,或是管理Callable任务和Runnable任务的状态。 ExecutorService也提供了批量运行Callable任务的方法。最后,ExecutorService还提供了一些关闭执行器的方法。若是须要支持即时关闭,执行器所执行的任务须要正确处理中断。
ScheduledExecutorService接口
ScheduledExecutorService扩展ExecutorService接口并添加了schedule方法。调用schedule方法能够在指定的延时后执行一个Runnable或者Callable任务。ScheduledExecutorService接口还定义了按照指定时间间隔按期执行任务的scheduleAtFixedRate方法和scheduleWithFixedDelay方法。
4. 线程池
在java.util.concurrent包中多数的执行器实现都使用了由工做线程组成的线程池,工做线程独立于所它所执行的Runnable任务和Callable任务,而且经常使用来执行多个任务。 使用工做线程可使建立线程的开销最小化。
在大规模并发应用中,建立大量的Thread对象会占用占用大量系统内存,分配和回收这些对象会产生很大的开销。一种最多见的线程池是固定大小的线程池。这种线程池始终有必定数量的线程在运行,若是一个线程因为某种缘由终止运行了,线程池会自动建立一个新的线程来代替它。须要执行的任务经过一个内部队列提交给线程,当没有更多的工做线程能够用来执行任务时,队列保存额外的任务。 使用固定大小的线程池一个很重要的好处是能够实现优雅退化。例如一个Web服务器,每个HTTP请求都是由一个单独的线程来处理的,若是为每个HTTP都建立一个新线程,那么当系统的开销超出其能力时,会忽然地对全部请求都中止响应。若是限制Web服务器能够建立的线程数量,那么它就没必要当即处理全部收到的请求,而是在有能力处理请求时才处理。 建立一个使用线程池的执行器最简单的方法是调用
java.util.concurrent.Executors的
newFixedThreadPool方法。Executors类还提供了下列一下方法:
若是上面的方法都不知足须要,能够尝试
java.util.concurrent.ThreadPoolExecutor或者
java.util.concurrent.ScheduledThreadPoolExecutor。
5. Fork/Joint
fork/join框架是ExecutorService接口的一种具体实现,目的是为了帮助你更好地利用多处理器带来的好处。它是为那些可以被递归地拆解成子任务的工做类型量身设计的。其目的在于可以使用全部可用的运算能力来提高你的应用的性能。 相似于ExecutorService接口的其余实现,fork/join框架会将任务分发给线程池中的工做线程。fork/join框架的独特之处在与它使用工做窃取(work-stealing)算法。完成本身的工做而处于空闲的工做线程可以从其余仍然处于忙碌(busy)状态的工做线程处窃取等待执行的任务。 fork/join框架的核心是
ForkJoinPool类,它是对AbstractExecutorService类的扩展。ForkJoinPool实现了工做偷取算法,并能够执行
ForkJoinTask任务。
基本使用方法
使用fork/join框架的第一步是编写执行一部分工做的代码。你的代码结构看起来应该与下面所示的伪代码相似:
- if (当前这个任务工做量足够小)
- 直接完成这个任务
- else
- 将这个任务或这部分工做分解成两个部分
- 分别触发(invoke)这两个子任务的执行,并等待结果
你须要将这段代码包裹在一个ForkJoinTask的子类中。不过,一般状况下会使用一种更为具体的的类型,或者是
RecursiveTask(会返回一个结果),或者是
RecursiveAction。 当你的ForkJoinTask子类准备好了,建立一个表明全部须要完成工做的对象,而后将其做为参数传递给一个ForkJoinPool实例的invoke()方法便可。
要清晰,先模糊
想要了解fork/join框架的基本工做原理,接下来的这个例子会有所帮助。假设你想要模糊一张图片。原始的source图片由一个整数的数组表示,每一个整数表示一个像素点的颜色数值。与source图片相同,模糊以后的destination图片也由一个整数数组表示。 对图片的模糊操做是经过对source数组中的每个像素点进行处理完成的。处理的过程是这样的:将每一个像素点的色值取出,与周围像素的色值(红、黄、蓝三个组成部分)放在一块儿取平均值,获得的结果被放入destination数组。由于一张图片会由一个很大的数组来表示,这个流程会花费一段较长的时间。若是使用fork/join框架来实现这个模糊算法,你就可以借助多处理器系统的并行处理能力。下面是上述算法结合fork/join框架的一种简单实现:
- public class ForkBlur extends RecursiveAction {
- private int[] mSource;
- private int mStart;
- private int mLength;
- private int[] mDestination;
-
- 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++) {
-
- 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;
- }
-
-
- 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) 建立一个表示全部须要完成工做的任务。
- ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
(2) 建立将要用来执行任务的ForkJoinPool。
- ForkJoinPool pool = new ForkJoinPool();
(3) 执行任务。
想要浏览完成的源代码,请查看
ForkBlur,其中还包含一些建立destination图片文件的额外代码。
标准实现
除了可以使用fork/join框架来实现可以在多处理系统中被并行执行的定制化算法(如前文中的ForkBlur.java例子),在Java SE中一些比较经常使用的功能点也已经使用fork/join框架来实现了。在Java SE 8中,java.util.Arrays类的一系列parallelSort()方法就使用了fork/join来实现。这些方法与sort()系列方法很相似,可是经过使用fork/join框架,借助了并发来完成相关工做。在多处理器系统中,对大数组的并行排序会比串行排序更快。这些方法到底是如何运用fork/join框架并不在本教程的讨论范围内。想要了解更多的信息,请参见Java API文档。 其余采用了fork/join框架的方法还包括java.util.streams包中的一些方法,此包是做为Java SE 8发行版中
Project Lambda的一部分。想要了解更多信息,请参见
Lambda Expressions一节。
6. 并发集合
java.util.concurrent包囊括了Java集合框架的一些附加类。它们也最容易按照集合类所提供的接口来进行分类:
7. 原子变量
java.util.concurrent.atomic包定义了对单一变量进行原子操做的类。全部的类都提供了get和set方法,可使用它们像读写volatile变量同样读写原子类。就是说,同一变量上的一个set操做对于任意后续的get操做存在happens-before关系。原子的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;
- }
- }
对于这个简单的类,同步是一种可接受的解决方案。可是对于更复杂的类,咱们可能想要避免没必要要同步所带来的活跃度影响。将int替换为AtomicInteger容许咱们在不进行同步的状况下阻止线程干扰,如
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();
- }
8. 并发随机数
在JDK7中,java.util.concurrent包含了一个至关便利的类,ThreadLocalRandom,当应用程序指望在多个线程或ForkJoinTasks中使用随机数时。
美源星
对于并发访问,使用TheadLocalRandom代替Math.random()能够减小竞争,从而得到更好的性能。
你只需调用ThreadLocalRandom.current(), 而后调用它的其中一个方法去获取一个随机数便可。下面是一个例子:
- int r = ThreadLocalRandom.current().nextInt(4,77);
blog.chinaunix.net/uid-30065054-id-5753320.htmlhtml
blog.itpub.net/30065054/viewspace-2126308java
www.niuche.com/article/1139447.htmlc++
www.niuche.com/article/1139449.html程序员
mingjia.cngold.org/expert/1304176/news/c789018.htm算法
mingjia.cngold.org/expert/1304176/news/c789000.htmexpress
mingjia.cngold.org/expert/1304176/news/c788983.htm编程
mingjia.cngold.org/expert/1304176/news/c788929.htmapi
mingjia.cngold.org/expert/1304176/news/c788947.htm数组
mingjia.cngold.org/expert/1304176/news/c788918.htm安全
mingjia.cngold.org/expert/1304176/news/c788897.htm
mingjia.cngold.org/expert/1304176/news/c788875.htm
mingjia.cngold.org/expert/1304176/news/c788862.htm
mingjia.cngold.org/expert/1304176/news/c788835.htm
www.wangchao.net.cn/hi/detail_227849.html
mingjia.cngold.org/expert/1304176/news/c789238.htm
www.wangchao.net.cn/hi/detail_228106.html
www.wangchao.net.cn/hi/detail_228108.html
site.leshou.com/s/33270717.html
site.leshou.com/s/33270662.html
site.leshou.com/s/33270579.html
site.leshou.com/s/33270536.html
site.leshou.com/s/33270498.html
site.leshou.com/s/33270383.html
site.leshou.com/s/33270315.html
www.365128.com/user/myx2/22.html
www.365128.com/user/myx2/25.html
www.365128.com/user/myx2/26.html
www.365128.com/user/myx2/27.html
www.365128.com/user/myx2/28.html