并发
Java平台支持并发编程。通过Java类库,支持基本的并发。自5.0版本,Java平台支持高级并发API。这里介绍Java平台的基本并发和java.util.concurrent
包中的高级并发。
进程和线程
并发编程中有两个基本执行单元:进程和线程。在Java并发编程中,更多指的是线程。
进程
一个进程包含一个执行环境。一个进程通常有一组完整的、私有的运行时资源。其中重要的是:每个进程都有自己的内存空间。
进程也经常等价于程序或应用。当用户看到的单个应用其实往往有多个进程。为方便进程间通信,操作系统往往支持进程间通信(IPC,Inter Process Communication),比如:管道和Socket。进程间通信不仅仅是同一个系统内的进程进行通信,也包括不同系统间的进程通信。
很多Java虚拟机的实现以单进程运行。但Java应用也可以通过ProcessBuilder类构建额外进程。
线程
线程有时被称为轻量级的进程。进程和线程都提供了一个执行环境,但是创建新线程所需资源比创建新进程要少。
线程存在于进程之内 – 每个进程至少有一个线程。线程共享进程资源,包括内存,打开的文件。这个共享资源让创建线程更高效,但同时引发 “通信”(同步) 问题。
多线程是Java平台的基本特性。每个应用至少有一个线程(或者多个,如果算上内存管理,信号处理等线程)。从开发者的角度看,每个应用至少有一个线程,叫主线程
。主线程
可以创建其他线程。
线程对象
每个线程都是Thread
类的实例,使用Thread
的两种方法如下:
- 每次需要其他线程时,直接实例化
Thread
类 - 将并发任务提交到
executor
定义和启动线程
要创建多线程应用,首先得提供待并发任务的代码。有两种方式提供待并发任务的代码:
- 实现一个
Runnable
实现。Runnable
接口只有一个run
方法,实现这个方法,并在构造Thread
实例时,传入Runnable
实现即可。如下所示
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();
}
}
- 生成
Thread
的子类。Thread
类也是Runnable
的实现。直接继承Thread
,修改其中run
方法即可。如下所示:
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
后,调用Thread.start
方法即可,如上面例子所示。
此外,Thread
类中定义了多个用于管理线程的方法。有的方法是本线程调用,用于获取线程信息,修改线程状态。有的方法是其他线程调用,用于管理线程。
用sleep暂停执行
Thread.sleep
会暂停当前线程指定时间。这样可以释放CPU。
有两个重载的sleep
版本,一个以指定毫秒时间进行休眠,一个指定纳秒时间进行休眠。但是,这些时间并不是完全准确的,它受操作系统影响。休眠中的线程,可以通过中断唤醒。
如下的例子中,使用sleep
每4秒打印一次消息:
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
就是Thread.sleep
抛出的。它会在其他线程中断它的时候,抛出这个异常。这里没有处理这个异常,是因为没有其他线程会中断它。
中断
中断一个线程,即告诉它出现了一个新情况。至于如何响应这个情况,由被中断的线程决定。但最常见的是,中断进程的休眠。
通过在被中断线程的Thread
对象上调用interrupt
函数来中断该线程。要想中断信号被正确处理,被中断线程有代码来检测中断信号。
处理中断
正确处理中断的方式跟线程执行的任务有关。
- 如果一个线程频繁执行抛出
InterruptedException
的方法,那么它只要在捕获到该异常时,进行中断处理即可。一般这样就能够及时的处理响应了。比如上面循环发送消息的例子,可以这样响应中断(它的中断处理就是退出线程)
for (int i = 0; i < importantInfo.length; i++) {
// Pause for 4 seconds
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// 在这里退出线程。
// 因为抛出 InterruptedException 异常的方法被频繁执行,所以中断消息能被及时处理。
return;
}
// Print a message
System.out.println(importantInfo[i]);
}
- 如果一个线程不会频繁不调用或根本不调用抛出
InterruptedException
异常方法,那么它要周期性的调用Thread.interrupted
方法来监听是否是否有中断信号。如下例子所示:
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
// 这里只是简单的返回方法,常见处理也有抛出异常的,即 new InterruptedException()
return;
}
}
中断状态标记
中断机制通过一个叫中断状态
的标记实现。当在线程对象上调用Thread.interrupt
方法时,会设置这个标记。当被中断线程使用静态方法Thread.interrupted
时,会清除标记。还有一个非静态isInterrupted
方法可以用来查询另一个线程的中断状态
标记,且不会清除它。
按惯例,任何抛出InterruptedException
异常退出的线程都会清除中断状态
标记。
Join
Join
方法允许一个线程等待另一个线程完成。
比如:t
是一个Thread
对象,且它正在执行。若当前线程调用t.join()
,会阻塞当前线程的执行,直到t
线程结束。
join
有一个重载方法可以指定等待指定的时间。但是跟sleep
类似,这个时间以来操作系统,因此不是精准的。
跟sleep
一样,join
也会抛出InterruptedException
异常。
简单示例
下面的简单示例展示本节涉及到的概念。简单示例有两个线程组成。
主线程创建子线程,然后等待完成(指定时间内),但若子线程在指定时间内没有完成,则中断它。
子线程循环打印消息,若被中断则打印退出日志,然后退出。具体如
public class SimpleThreads {
// Display a message, preceded by
// the name of the current thread
static void threadMessage(String message) {
String threadName =
Thread.currentThread().getName();
System.out.format("%s: %s%n",
threadName,
message);
}
private static class MessageLoop
implements Runnable {
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
try {
for (int i = 0;
i < importantInfo.length;
i++) {
// Pause for 4 seconds
Thread.sleep(4000);
// Print a message
threadMessage(importantInfo[i]);
}
} catch (InterruptedException e) {
threadMessage("I wasn't done!");
}
}
}
public static void main(String args[])
throws InterruptedException {
// Delay, in milliseconds before
// we interrupt MessageLoop
// thread (default one hour).
long patience = 1000 * 60 * 60;
// If command line argument
// present, gives patience
// in seconds.
if (args.length > 0) {
try {
patience = Long.parseLong(args[0]) * 1000;
} catch (NumberFormatException e) {
System.err.println("Argument 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");
// loop until MessageLoop
// thread exits
while (t.isAlive()) {
threadMessage("Still waiting...");
// Wait maximum of 1 second
// for MessageLoop thread
// to finish.
t.join(1000);
if (((System.currentTimeMillis() - startTime) > patience)
&& t.isAlive()) {
threadMessage("Tired of waiting!");
t.interrupt();
// Shouldn't be long now
// -- wait indefinitely
t.join();
}
}
threadMessage("Finally!");
}
}
同步工具
线程通信的主要方式是:共享字段或共享对象字段。这种通信方式很高效,但有如下两个问题:
- 线程交错
- 内存一致性错误
同步工具用来防止这两类问题
但是同步工具会引入线程竞争,这会降低线程执行速度(甚至停止线程执行)。饥饿和活锁是两种常见的线程竞争,它们将在线程活跃度
一节介绍。
本节主要阐述如下问题: - 线程交错:即多线程访问共享数据容易引发的问题之一
- 内存一致性错误:即多线程访问共享数据容易引发的问题之二
- Synchronized方法:可以防止线程交错和内存一致性错误的简单同步手段
- 内置锁和同步工具:基于内置锁的更常用的同步手段
- 原子访问:一些方法,指示如何构建不能被分割的操作
线程交错
考虑下面这个简单的计数器
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
这个计数器很简单,仅仅是通过increment
方法+1
通过decrement
方法-1
。
但是,当这个对象被多个线程使用时,就可能出现线程交错。
线程交错:操作共享数据的操作,分成多个步骤时,不同线程的多个步骤相互交错。
比如,即使上面这个这个简单的c++
操作,一般会分为三个步骤:
- 将
c
的值存到缓存 - 给缓存
+1
- 将缓存存回
c
当这个c++
操作发生在多个线程时,不同线程的步骤交错在一起,即发生了线程交错。
比如:有线程A执行increment
操作时,线程B在执行decrement
操作(decrement
的c--
同样要分解成类似c++
的三个步骤)。此时这个6个步骤有C6,6 = 720
中排列方式,即交错方式。其中有的交错方式得到正确结果,有的交错方式得到错误结果。
比如:下面这种交错方式就没有得到正确结果(假设c
初始为0,则结果期望是0,但这里是1) - 线程A:将
c
存到缓存 - 线程B:将
c
存到缓存 - 线程A:将缓存+1,
缓存 = 1
- 线程B:将缓存-1,
缓存 = -1
- 线程A:将缓存存回
c
,c = 1
- 线程B:将缓存存回
c
,c=-1
这两个操作执行完,结果跟预期不符。这就是并发引起的线程交错导致的错误。
内存一致性错误
内存一致性错误发生在:不同线程,对共享数据有不同的视图时。比如,下面例子:
假设有一个共享字段int counter = 0
。这个字段被A,B两个线程引用,其中A线程执行counter++
而B线程执行System.out.println(counter)
。假设A执行完马上到B。这个时候,预期B的输出为1,但其实可能是0。因为A线成对counter
的修改不一定会被B线程看到(除非开发人员特别操作)。这就是两个线程对共享数据有不同视图。
内存一致性错误的原因比较复杂,这里不涉及。只需知道避免内存一致性错误的手段即可 – 建立语句前发生
关系。前发生
关系确保:被某语句写的内存,写结果能被其他语句看到。
有很多方法可以建立前发生关系,其中同步工具可以建立前发生关系。我们已经见过的前发生关系,如下:
- 调用
Thread.start
时,所有和这条语句有前发生关系的语句,都跟新线程中的所有语句有前发生关系。 - 调动
Thread.join
时,被终止线程的所有语句跟Thread.join
后面的所有语句存在前发生关系。
同步方法
java语言提供两种基本的同步方案,Synchronized方法
和Synchronized语句
。细节将在下一节阐述。这里先看Synchronized方法
的简单示例。
要让一个方法变成同步方法,只需在方法前添加Synchronized
关键字,如下所示:
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
跟上面的简单计数器不同,这个SynchronizedCounter
有如下效果:
- 首先,多个线程调用
Synchronized方法
不会发生交错。因为同一个对象的Synchronized方法
同一时刻只有一个线程在执行,其他线程会被阻塞。 - 其次,当
Synchronized方法
退出时,自动跟该对象的其他Synchronized方法
建立前发生
关系。
Synchronized方法
提供简单方式来避免线程交错和内存一致性错误:即针对共享对象状态的读写都通过Synchronized方法
来完成。但同步方法会降低线程的活跃度。
内置锁与SynchronizedX同步工具
SynchronizedX
底层通过内置锁(也叫监控器锁)实现。内置锁实现了如下两功能,才避免并发的两大错误:
- 排他性的对象状态访问,即一次只有一个线程能访问对象
- 建立
前发生
关系
每个对象都有内置锁。按惯例,当一个线程需要排他和一致的访问共享对象状态时,它需要:在访问前获取对象的内置锁,在访问结束后释放对象的内置锁。
当一个线程拥有某对象的内置锁时,其他请求该内置锁的线程将会阻塞。
当一个线程释放某对象的内置锁时,这个释放语句和其后对同一个内置锁的请求语句将构成前发生
关系
内置锁与Synchronized方法
当线程调用一个Synchronized方法
时,它会自动请求该对象的内置锁,并在方法结束或者异常退出后释放内置锁。
注意Synchronized静态方法
静态方法属于Class
对象,因此它会请求对象Class
实例的内置锁,而不是对象本身的内置锁。
Synchronized语句
Synchronized语句
是java提供的另一种创建同步代码的方法。它跟Synchronized方法
的差别在于:需要指明提供内置锁的对象。如下所示:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
上面的lastName
和 nameCount
要求同步访问。若是使用Synchronized方法
,则nameList.add(nanme)
就需要放在一个单独的非同步的方法中。(因为它不需要同步)
Synchronized语句
通过降低同步代码的粒度,从而提高了并发性。比如下面的例子,假如字段c1
和c2
是共享字段,但两者不会同时使用。即更新c1
的同时更新c2
是没有什么问题的。如果使用Synchronized方法
,那么更新c1
时就不能更新c2
。导致可并发性下降,而下面使用Synchronized语句
则是更好的方式:
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++;
}
}
}
可重入同步工具
前面提到,线程不能请求其他线程已拥有的锁(会被阻塞)。但是有时线程需要再次请求自己已拥有的锁。当一个锁可以被已拥有线程多次请求时(而不会阻塞自己),这种锁叫做可重入锁。这种锁用在同步代码直接或间接请求同一个锁的其他同步代码的场景下。
原子访问
原子操作即:不会被分割的操作。
比如之前的c++
操作会被分割成三步,所以它不是原子操作。由此可知,即使很简单的操作也会被分割。但是下面这两个操作已知是原子的:
- 读写引用类型或基本类型变量(除了
long
和double
) - 读写所有
volatile
变量
原子操作不能被分割,所以它们不担心线程交错。但是依旧有可能发生内存不一致错误。但读写volatile
变量连内存不一致错误也能避免,因为对volatile
变量的写和后续的读,有一个前发生
关系。
使用原子操作相比其他同步工具更高效,但是要注意内存不一致错误。
java.util.concurrent
包提供很多原子方法。将在下面的高级并发对象中阐述它们。
线程活跃度
并发应用线程执行速度叫活跃度。这部分描述关于线程活跃度的三个问题:死锁,饥饿,活锁。
死锁
死锁描述这样一个场景:两个或多个线程等待对方持有的锁,而相互阻塞。
假如存在这样一个国家,这里的人向朋友鞠躬时需要等待朋友鞠躬回来,这个鞠躬行为才能结束(即bow
方法中,需要等待调用对方bowBack
方法)。当bow
和bowBack
都是同步方法时,若两个人同时bow
则会陷入死锁。如下所示:
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();
}
}
当两者同时bow
时,会陷入死锁,不会出来。
饥饿和活锁
相较于死锁,饥饿和活锁比较少见。但同样需要仔细考量。
饥饿
饥饿描述这样的场景:线程很难拿到共享资源,导致自身进行阻塞。饥饿的原因是,共享资源长期被其他线程占用。比如:某对象有一个Synchronized方法
该方法执行时间长,且频繁被调用;从而导致该对象的其他共享资源很难访问到。
活锁
活锁发生在:一个线程对另外一个线程的行为做出反应时。跟死锁类似,活锁一样使得线程不能继续执行,这是没有被阻塞,而是一直忙于恢复正常工作。比如:载波侦听与冲突检测,若处理不当,就容易发生活锁。当一个线程准备往总线上发送数据时,发现其他线程正在使用总线,这时,两者都会停止并等待指定时间,若时间固定,则下次发送,依旧冲突。
保护块
线程间经常需要协调它们的行为。最常用的协调方法就是:保护块。保护块即:线程推动进度前,需要轮询某条件,直到条件为true
。
比如,下面的例子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
方法,让本线程停止。调用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!");
}
从上面的方法可以知道Object.wait
方法同样抛出InterruptedException
,表明它可以被中断。
注意上面的方法是Synchronized方法
,之所以是Synchronized方法
是因为调用某对象的wait
方法,需要先获得该对象的内置锁。当调用对象的wait
之后,线程便释放对象的内置锁并等到。然后当其他线程在该对象上调用notifyAll
时(同样要求先获取内置锁),这唤醒在该锁上等待的线程。如下所示:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
一般情况下,等notifyAll
线程释放锁后,等待线程就会获取锁,然后继续执行。
可以使用保护块创建一个生产者和消费者应用。它们的功能如下:
- 生产者,往共享对象存放数据,当存放区满时阻塞自己
- 消费者,从共享独享获取数据,当存放区空时阻塞自己
在这个例子中,数据是一些列的文本消息,共享对象的结构如下:
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();
}
}
生产者线程产生一些列消息,当产生“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");
}
}
消费者取出数据,并打印,当收到“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) {}
}
}
}
最后启动这两个线程即可
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();
}
}
不可变对象
当一个对象构造完成之后,其状态不能被改变,这个对象就叫不可变对象。
不可变对象再并发应用中非常有用。因为不可变,所以不会发生线程交错和内存不一致错误(因为只能读)。
下面一节展示如何从一个可变对象衍生出不可变对象。并给出使用不可变对象的规则。
一个同步的类的示例
比如有下面这样一个表示颜色的类,它保存三种主色,并可以给颜色命名。
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;
}
}
这个类虽然很好的使用的Synchronized方法
但多线程访问它是,还是要小心。比如下面这个场景:
SynchronizedRGB color =
new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB(); //Statement 1
String myColorName = color.getName(); //Statement 2
当在上面的Statement1
和Statement2
之间有线程调用了color.set
,那么颜色的名字和值就对应不上了。因此Statement1
和Statement2
需要这样写:
synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
}
这样写后,不会发生不一致问题,但这完全依赖于开发人员。然而如果SynchronizedRGB
是不可变对象,则会降低对开发人员的要求。
定义不可变对象的规则
下面列出了创建不可变对象的简单规则。
- 不要提供
setter
方法 - 所有的字段都是
private final
- 禁止派生子类。比如
final类
或则私有构造函数并用工厂方法创建对象 - 若字段中引用其他可变对象,则通过这些方法禁止可变对象变化:1.不提供setter方法,通过构造函数初始化。2.获取对象时,不直接返回对象引用,而是返回一个副本的引用。(避免外部通过引用修改对象)
将这些规则用在上面的SynchronizedRGB
类中有: - 上面类中的
set
和invert
方法都能改变对象状态。 - 所有字段都是
private
但还差final
- 类不是
final
- 只有一个字段引用对象,但String对象本来就是不可变对象
如此,新的不可变类如下:
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);
}
}
高级并发对象
之前的API都是java平台诞生之初就携带的同步工具。这些API只能用于基本的并发任务。
本节会介绍Java5.0引进的高级并发特性。大部分这些特性都是在java.util.concurrent
包中实现。这其中包含了一些新的并发结构的集合类。
本节的具体内容如下:
- 锁对象
Executors
- 一个发布和管理线程的接口- 并发集合 - 用于管理大量并发数据
- 原子变量 - 最小化的同步工具,可以避免内存不一致错误
ThreadLocalRandom
- 并发随机数
锁对象
之前提到对象内置锁可重入锁简单,但是有一定的局限性。在java.util.concurrent.locks
包中有更加好用的锁。这个包提供了一个Lock
接口
Lock
跟内置锁类似,某时刻它仅能被一个线程拥有;借助Condition类
同样支持保护块机制(即wait/notify
)。
Lock
和内置锁最大的差异在于:尝试获取锁和锁中断。Lock
接口提供如下个方法:
tryLock
- 获取锁失败时直接返回(或指定时间内返回)lockInterruptibly
- 当其他线程在锁上触发中断时,返回
我们使用Lock
处理之前的鞠躬的死锁问题。要求鞠躬之前先获取两者的锁。如下所示:
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 {
// 当不能同时获取两个锁时,释放已经获得的锁
// 这样就不会死锁,之前死锁是因为第二个"tryLock"不会返回
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
前面的例子中,并发代码都关联到一个Thread
对象。这只合适规模较小的应用,规模大的应用需要将线程的创建,管理和线程的具体代码分离开。Executors
就提供了这样的服务。下面从三个方面介绍Executors
:
Executor接口
- 派生了2个Executor
子接口- 线程池 - 最常见的
Executor
实现 Fork/Join
- 递归并发接口
Executor接口
java.util.concurrent
中定义了Executor
及其两个派生接口,如下:
Executor
- 这个接口用来发布并发任务ExecutorService
- 这个子接口(Executor
的)用来管理线程和线程池的生命周期ScheduledExecutorService
- 这个子接口(ExecutorService
的)提供指定时间/周期时间执行任务
Executor接口
Executor
接口提供了一个execute
方法,用来代替之前Thread
的start
方法。具体过程如下:
比如r
是一个Runnable
实例
以前的运行方式:
(new Thread(r)).start();
Executor
的运行方式
e.execute(r);
以前的运行方式是,创建一个线程并立即运行它。现在Executor
可能也类似,但更可能的是使用一条已有的线程执行它,或者将它放在等待队列中,等有空时在运行它。
ExecutorService接口
相较于Executor
,ExecutorService
多了一个submit
方法。submit
方法除了可以提交Runnable
对象,还可提交Callable
对象。此外,ExecutorService
还提供用于管理线程池关闭的方法。为了支持线程立即关闭,线程任务应该监听和处理中断。
ScheduleExecutorService接口
相较于ExecutorService
,ScheduleExecutorService
多提供了关于定时/定期执行任务的方法:
schedule
方法 - 用于指定延迟后,执行任务scheduleAtFixedRate
- 用于重复执行任务scheduleWithFixedDelay
- 用于重复执行任务
线程池
java.util.concurrent
包中Executor
的实现是线程池。这些线程跟并发代码Runnable
和Callable
是分开的。
使用线程池可以最小化线程创建开销。因为线程对象占用大量内存,创建和删除大量线程需要很大的内存管理开销。
一个常用的线程池是Fixed Thread Pool
,这种线程池有指定数量的线程,若线程在执行过程中不小心死掉了,它会自动创建一个来代替死掉的线程。任务通过内部的队列来提交给线程池。
固定线程池最重要的优势是服务降级。比如一个网站给每个请求创建一个线程,当请求数量大增,创建线程数量超出了机器能处理的量后,所有请求都会崩溃。但是如果使用固定线程池,线程数量有限,多余的请求会在队列中等待,这样机器虽然不能很快响应,但是不会崩溃。
创建固定线程最简单的方法是通过java.util.concurrent.Executors
的newFixedThreadPool
工厂方法。除了这个方法,该类还提供如下创建其他线程池的方法:
newCachedThreadPool
- 创建可扩展线程池。这种线程池适合处理大量存活期短的线程。newSingleThreadExecutor
- 这种线程池一次执行一个任务。- 还有很多关于创建
ScheduledExecutorService
线程池的工厂方法
如果上述工厂方法创建的线程池不能满足你的要求,可以自己构造java.util.concurrent.ThreadPoolExecutor
和java.util.concurrent.ScheduledThreadPoolExecutor
,它们给出了很多额外的选项。
Fork/Join递归并发接口
跟线程池类似,Fork/Join
框架是ExecutorService
的另一个实现。它用于并发处理那些可以进一步分解成更小任务的任务。
跟其他ExecutorService
实现类似,Fork/Join
框架将任务分发给线程池中的线程。它和一般线程池的差别在于work-stealing
算法(偷工时算法)-- 即空闲线程可以从繁忙线程获取任务。
Fork/Join
框架的实现类叫ForkJoinPool
,它继承了AbstractExecutorService
。ForkJoinPool
实现了work-stealing
算法,并处理ForkJoinTask
任务。
基本用法
使用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
在上面这段代码中,使用RecursiveTask
或RecursiveAction
封装子任务,等它们的实现类准备好后,就将它传递给ForkJoinPool.invoke()
方法即可。
示例
假设你想模糊化一张照片。照片用数组表示,每个元素是一个像素的色值。模糊化的过程即:每个色值跟周围的色值进行平均。然后将结果存到输出图片数组的对应位置。由于图片的数组很大,因此可以使用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.
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;
}
}
实现了RecursiveAction
的模糊化方法后,还要实现其中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
就算实现好了。之后就可以提交任务,具体如下:
- 创建任务
ForkBulr fb = new ForkBlur(src, 0, src.length, dst);
- 创建线程池
ForkJoinPool pool = new ForkJoinPool();
- 运行任务
pool.invoke(fb);
标准实现
Java8SE中有写标准库也使用可Fokr/Join
框架,比如:java.util.Arrays
的parallelSort()
方法。java.util.streams
包中也有大量应用。
并发集合
java.util.concurrent
包还包含了一些列的集合类。这些类可以通过集合接口进行如下分类:
BlockingQueue
- 阻塞队列,当给满队列放数据,或从空队列取数据时,都会阻塞线程。ConcurrentMap
接口 - 他是java.util.Map
接口的子接口。它的操作都是原子操作。它的常见实现是ConcurrentHashMap
,是HashMap
的并发版本ConcurrentNavigableMap
- 它是ConcurrentMap
的子接口。它只是对元素排序。最常见的实现是ConcurrentSkipListMap
,是TreeMap
的并发版本
所有这写集合的写操作和读操作之间有前发生
关系,因此可以避免内存不一致问题。
原子变量
java.util.concurrent.atomic
包定义了很多类。这些类支持在变量上执行原子操作。所有这些类的get/set
方法,就像在volatile
变量上进行读写一样。即set
和后续get
存在前发生
关系。
下面通过修改之前的Counter
类,来看如何使用java.util.concurrent.atomic
包。
之前的计数器类
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
之前用SynchronizedX
修改的,不会发生线程交错的计数器类
class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
使用原子变量的,更轻量的同步计数器。它可以避免不必要线程阻塞,提高线程活跃度。
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();
}
}
并发随机数
在JDK7中,java.util.concurrent
包中还有一个方便的类ThreadLocalRandom
,它可以在并发场景下产生随机数。
在并发场景下,相较于Math.random()
,ThreadLocalRandom
产生更少的冲突并有更好的性能。
它的使用方法如下:
int r = ThreadLocalRandom.current().nextInt(4,77);