Java并发(多线程) :本文描述了如何用Java做并发编程。它涵盖了parallel programming, immutability,threads, the executor framework (thread pools)、future、callables CompletableFuture和fork - join框架等相关概念。
Table of Contents
1.Concurrency
1.1.What is concurrency?
1.2.Process vs. threads
2.Improvements and issues with concurrency
2.1.Limits of concurrency gains
2.2.Concurrency issues
3.Concurrency in Java
3.1.Processes and Threads
3.2.Locks and thread synchronization
3.3.Volatile
4.The Java memory model
4.1.Overview
4.2.Atomic operation
4.3.Memory updates in synchronized code
5.Immutability and Defensive Copies
5.1.Immutability
5.2.Defensive Copies
6.Threads in Java
7.Threads pools with the Executor Framework
8.Futures and Callables
8.1.Futures and Callables
8.2.Drawbacks with Futures and Callbacks
9.CompletableFuture
10.Nonblocking algorithms
11.Fork-Join in Java 7
12.Deadlock
13.About this website
14.Links and Literature
14.1.Concurrency Resources
14.2.vogella GmbH training and consulting support
1. Concurrency(并发性)
1) What isconcurrency?(什么是并发)
并发性是指能够同时运行多个程序或者程序的几个部分;如果可以异步或并发执行一个耗时的任务能够大大提高程序的吞吐量和交互性。现代计算机一般都有多个CPU或一个CPU有多个核心,因此能够利用多核技术执行超大的应用程序。
2) Process vs.threads(进程VS线程)
进程是一个可以单独运行并且独立于其他进程,它不能与其他进程直接共享数据;进程资源(内存、CPU时间)都通过操作系统直接分配。一个线程是一个所谓的轻量级的进程。它有自己的调用堆栈,但是可以访问同一进程的其他线程所共享的数据,每个线程都有自己的内存缓存;一个线程能够读取存储在自己内存缓存中的共享数据并且能够重复读取共享数据。
一个Java应用程序在默认情况下运行在一个进程,在Java应用程序中利用多个线程来实现并行处理或异步行为。
2. Improvements and issues with concurrency(改善并发性问题)
在Java应用程序中利用多个线程来实现并行处理或异步行为,因为这些任务可以分为子任务,这些子任务可以并行执行,所以并发性可以更快地执行某些任务,当然并行运行时受到并发执行的Task的限制。
理论上可行性效率可以通过Amdahl计算规则来计算:
如果F表示程序不能并发运行的百分比,N是进程的数量,则最大的性能效率= 1/ (F+ ((1-F)/n)).
线程有自己的调用堆栈,同时也可以访问共享数据。因此有两个基本问题:可见性和访问控制问题。
可见性问题:如果线程A读取了共享数据后,线程B修改了共享数据,然而线程A读取的仍然是修改前的数据。
访问问题:多个线程访问并且同时修改了共享数据。
可见性和访问问题会导致如下问题:
失去活性:程序由于同时争夺数据而且相互等待,没有任何反应,如死锁。
安全问题:程序创建了错误的数据。
Java应用程序在默认情况有一个线程,并且运行在自己的进程中,Java通过Thread支持线程作为Java语言的一部分,Java应用程序可以通过Thread类来创建新的线程。
Java1.5在java.util.concurrent包中改进了对并发性的支持。
Java提供了locks来保护代码的某些部分被多个线程同时执行,最简单的方法锁定一个方法或Java类定义同步方法或者使用synchronized 关键字
synchronized关键字在Java中确保以下问题:
a) 在同一时刻仅仅只有一个线程执行代码块;
b) 每个线程进入一个同步的代码块会影响同一个锁保护中之前的所有修改;
c) Synchronization对于多个线程之间的互斥访问和可靠的线程通信是必要的;
d) 可以使用Synchronization关键字来定义一个方法,这可以保证同一时刻仅仅只有一个线程执行这个方法,另一个线程只有等该方法执行完毕后才可以调用该方法。
public synchronized void critial() {
// some thread critical stuff
// here
}
你还可以使用synchronized关键字来保护方法内部的代码块。这个代码块用关键字lock保护,可以是一个字符串或者对象;由同一个锁保护的代码同一时刻只能由一个线程执行。
举例说明,下面的数据结构将确保仅仅只有一个线程可以访问内部的add()和next()方法。
package de.vogella.pagerank.crawler;
import java.util.ArrayList;
import java.util.List;
public class CrawledSites {
private ListcrawledSites = new ArrayList ();
private ListlinkedSites = new ArrayList ();
public void add(String site) {
synchronized (this) {
if (!crawledSites.contains(site)) {
linkedSites.add(site);
}
}
}
/**
* Get next site to crawl. Can return null (if nothing to crawl)
*/
public String next() {
if (linkedSites.size() == 0) {
return null;
}
synchronized (this) {
// Need to check again if size has changed
if (linkedSites.size() > 0) {
String s = linkedSites.get(0);
linkedSites.remove(0);
crawledSites.add(s);
return s;
}
return null;
}
}
}
如果声明一个变量用关键字volatile修饰可以保证任何线程读取该字段时会访问最近被修改后的值,volatile修饰的变量不会执行任何互斥锁。
在Java 5的写访问中,volatile修饰的变量也将更改由同一个线程修改的非volatile变量。这也可以用来更新引用变量的值,如volatile修饰的变量person,在这种情况下,你必须使用一个临时变量person,并且用setter初始化变量,然后分配临时变量给最终的变量,这将使该变量的地址和值变化对于其他线程可见。
Java内存模型描述了线程中内存和应用程序的主内存之间的通信;它定义了通过该线程转到其他线程时内存如何改变的规则。Java内存模型还在一个线程从主内存刷新内存时定义了相应的解决方案,其还描述了操作的原子性和有序性。
所谓原子操作,就是"不可中断的一个或一系列操作" 。在多线程的情况下,如果确认一个操作是原子操作则可以避免仅仅为保护这个操作而加上性能开销昂贵的锁,甚至我们可以借助于原子操作来实现互斥锁。
假设i定义为int类型。则i++(增量)在Java操作中不是一个原子操作,同时这也适用于其他数值类型,如long等等)。
i++操作首先读取存储的i值(原子操作),然后i+1(原子操作);但i的读和写操作已经改变了其值。
Java1.5之后添加了原子变量,如AtomicInteger或AtomicLong提供的 getAndDecrement (),getAndIncrement()和getAndSet()方法都是原子方法。
Java内存模型保证每个线程进入一个同步的代码时块将会查看被同一个锁保护中以前所有的修改,。
避免并发问题最简单的方法就是线程之间共享不可变的数据(不能修改的数据)。
不可变的类:
a) 所有的字段用final修饰;
b) 该类定义为final类型;
c) 仅仅在构造函数中定义引用;
d) 引用可变数据对象的任何字段:private,没有设置setter方法,没有直接返回给调用者的对象,在类的内部改变了对类的外部没有影响。
不可变类也许包含一些管理状态的可变数据,但从外面来看这个类的属性没有改变。
对于所有的可变字段来说,如数组在构建阶段从外面传递给该类,这个类需要一个元素保护性副本来确保没有外部的其他对象来修改数据。
你必须在调用代码中保护类。假定调用代码会在某种程度上出乎你的意料地改变你的数据。事实上确实如此,但你不希望在类的外部改变你的数据。
为了保护类的数据不被修改,你应该复制所接收的数据,并且返回副本数据给调用代码。
下面的示例创建一个列表的副本(ArrayList)和仅仅返回列表的副本。此种方式下该类的客户端不能从列表中删除元素。
package de.vogella.performance.defensivecopy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MyDataStructure {
List
public void add(String s) {
list.add(s);
}
/**
*Makes a defensive copy of the List and return it
*This way cannot modify the list itself
*
*@return List
*/
public List
return Collections.unmodifiableList(list);
}
}
并发的基本方法就是java.lang.Threads类.A线程执行java.lang.Runnable中的对象。
Runnable是定义了run()方法的接口。该方法被线程对象调用,并且包含了该做的工作。因此Runnable是一个执行的task,线程就是执行task的worker。
下面展示了一个task(Runnable),其计算给定范围的数字的总和。创建新的java project为de.vogella.concurrency.threads ,部分代码如下:
package de.vogella.concurrency.threads;
/**
*MyRunnable will count the sum of the number from 1 to the parameter
*countUntil and then write the result to the console.
*
*MyRunnable is the task which will be performed
*
*@author Lars Vogel
*
*/
public class MyRunnable implements Runnable {
private final long countUntil;
MyRunnable(long countUntil) {
this.countUntil = countUntil;
}
@Override
public void run() {
long sum = 0;
for (long i = 1; i < countUntil; i++) {
sum += i;
}
System.out.println(sum);
}
}
下面的例子演示了使用线程和Runnable类的用法。
package de.vogella.concurrency.threads;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
// We will store the threads so that wecan check if they are done
List
// We will create 500 threads
for (int i = 0; i < 500; i++) {
Runnable task = newMyRunnable(10000000L + i);
Thread worker = new Thread(task);
// We can set the name of the thread
worker.setName(String.valueOf(i));
// Start the thread, never call methodrun() direct
worker.start();
// Remember the thread for later usage
threads.add(worker);
}
int running = 0;
do {
running = 0;
for (Thread thread : threads) {
if (thread.isAlive()) {
running++;
}
}
System.out.println("We have " + running + "running threads. ");
} while (running > 0);
}
}
直接使用线程类具有以下缺点:
a) 创建一个新的线程会降低程序性能;
b) 过多的线程会导致性能下降,因为CPU需要在这些线程之间切换;
c) 不太好控制线程的数量,因此你可能会由于创建太多的线程而遇到内存不足错误。
java.util.concurrent包相对于直接使用了线程来说提供了并发性更好地支持,这个包在下面会有说明。
7.
Threads pools with the Executor Framework(线程池与执行程序框架)线程池管理一个worker thread线程池,该线程池包含一个工作队列,其持有等待执行的任务。线程池可以被描述为Runnable对象的集合(work队列)和运行线程的连接。这些线程持续运行,检查work并且查询新的work。如果有新的work要做,那么就执行这个work(Runnable)。线程类本身提供了一个方法,如execute(Runnable r)来添加新的Runnable对象到work队列中。
Executor framework提供了示例实现java.util.concurrent.Executor接口,例如Executors.newFixedThreadPool(intn)将创建n个worker线程。ExecutorService增加了生命周期方法到Executor中,其允许关闭Executor,并等待终止线程。
Tips:如果你想使用一个线程池,其中一个线程执行几个runnable可以使用Executors.newSingleThreadExecutor()方法来
创建Runnable:
package de.vogella.concurrency.threadpools;
/**
*MyRunnable will count the sum of the number from 1 to the parameter
*countUntil and then write the result to the console.
*
*MyRunnable is the task which will be performed
*
*@author Lars Vogel
*
*/
public class MyRunnable implements Runnable {
private final long countUntil;
MyRunnable(long countUntil) {
this.countUntil = countUntil;
}
@Override
public void run() {
long sum = 0;
for (long i = 1; i < countUntil; i++) {
sum += i;
}
System.out.println(sum);
}
}
现在可以用executor框架执行runnables:
package de.vogella.concurrency.threadpools;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
private static final int NTHREDS = 10;
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(NTHREDS);
for (int i = 0; i < 500; i++) {
Runnable worker = newMyRunnable(10000000L + i);
executor.execute(worker);
}
// This will make the executor accept nonew threads
// and finish all existing threads inthe queue
executor.shutdown();
// Wait until all threads are finish
executor.awaitTermination();
System.out.println("Finished all threads");
}
}
在此种情况下,线程应该返回某个值(result-bearing线程),然后可以使用java . util . concurrent类。
Executor框架提出使用Runnable对象来工作,但Runnable不能向调用者返回一个结果。如果希望线程返回计算结果可以使用java.util.concurrent.Callable。Callable对象允许返回计算后的值。Callable对象使用了泛型来定义返回的类型的对象。如果提交一个Callable对象到Executor,框架会返回一个java.util.concurrent.Future类型的对象。Futrue提供方法允许客户端监控task被不同的线程执行的的进度。因此Futrue对象可以用来检查Callable的状态和结果。
在Executor中,你可以使用此方法提交callable并且获得一个future。为了获取futrue结果可以使用get()方法。
package de.vogella.concurrency.callables;
import java.util.concurrent.Callable;
public class MyCallable implements Callable
@Override
public Long call() throws Exception {
long sum = 0;
for (long i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
package de.vogella.concurrency.callables;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableFutures {
private static final int NTHREDS = 10;
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(NTHREDS);
List
for (int i = 0; i < 20000; i++) {
Callable
Future
list.add(submit);
}
long sum = 0;
System.out.println(list.size());
// now retrieve the result
for (Future
try {
sum += future.get();
}catch (InterruptedException e) {
e.printStackTrace();
}catch (ExecutionException e) {
e.printStackTrace();
}
}
System.out.println(sum);
executor.shutdown();
}
}
Java5.0支持原子操作,因此可以设计非阻塞算法,如无须同步,但都是基于低级原子硬件原语,如比较和交换(CAS),如果某个变量有个确定的值并且此值将执行这个操作则需要比较和交换操作检查。
例如下面创建了一个不断增加的非阻塞计数器:
package de.vogella.concurrency.nonblocking.counter;
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger value = new AtomicInteger();
public int getValue(){
return value.get();
}
public int increment(){
return value.incrementAndGet();
}
// Alternative implementation asincrement but just make the
// implementation explicit
public int incrementLongVersion(){
int oldValue = value.get();
while (!value.compareAndSet(oldValue,oldValue+1)){
oldValue = value.get();
}
return oldValue+1;
}
}
package de.vogella.concurrency.nonblocking.counter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test {
private static final int NTHREDS = 10;
public static void main(String[] args) {
final Counter counter = new Counter();
List
ExecutorService executor = Executors.newFixedThreadPool(NTHREDS);
for (int i = 0; i < 500; i++) {
Callable
@Override
public Integer call() throws Exception {
int number = counter.increment();
System.out.println(number);
return number ;
}
};
Future
list.add(submit);
}
// This will make the executor accept nonew threads
// and finish all existing threads inthe queue
executor.shutdown();
// Wait until all threads are finish
while (!executor.isTerminated()) {
}
Set
for (Future
try {
set.add(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
if (list.size()!=set.size()){
throw new RuntimeException("Double-entries!!!");
}
}
}
其中最重要的方法 incrementAndGet()使用了CAS操作。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
JDK本身对于每个开发人员来说为了提高性能会使用越来越多的使用非阻塞算法,然而开发正确的非阻塞算法并不是一件容易的事情。
Java 7为密集型计算任务引入了新的并行机制:fork - join框架。fork - join框架允许你分配一个特定任务到多个worker上,然后等待运行结果。
创建一个非阻塞算法:
package algorithm;
import java.util.Random;
/**
*
*This class defines a long list of integers which defines the problem we will
*later try to solve
*
*/
public class Problem {
private final int[] list = new int[2000000];
public Problem() {
Random generator = new Random(19580427);
for (int i = 0; i < list.length; i++) {
list[i] = generator.nextInt(500000);
}
}
public int[] getList() {
return list;
}
}
定义Solver类如下:
API定义其他的类,如RecursiveAction,AsyncAction.
package algorithm;
import java.util.Arrays;
import jsr166y.forkjoin.RecursiveAction;
public class Solver extends RecursiveAction {
private int[] list;
public long result;
public Solver(int[] array) {
this.list = array;
}
@Override
protected void compute() {
if (list.length == 1) {
result = list[0];
} else {
int midpoint = list.length / 2;
int[] l1 = Arrays.copyOfRange(list, 0,midpoint);
int[] l2 = Arrays.copyOfRange(list, midpoint,list.length);
Solver s1 = new Solver(l1);
Solver s2 = new Solver(l2);
forkJoin(s1, s2);
result = s1.result + s2.result;
}
}
}
测试用例:
package testing;
import jsr166y.forkjoin.ForkJoinExecutor;
import jsr166y.forkjoin.ForkJoinPool;
import algorithm.Problem;
import algorithm.Solver;
public class Test {
public static void main(String[] args) {
Problem test = new Problem();
// check the number of available processors
int nThreads =Runtime.getRuntime().availableProcessors();
System.out.println(nThreads);
Solver mfj = newSolver(test.getList());
ForkJoinExecutor pool = new ForkJoinPool(nThreads);
pool.invoke(mfj);
long result = mfj.getResult();
System.out.println("Done. Result: " + result);
long sum = 0;
// check if the result was ok
for (int i = 0; i < test.getList().length; i++) {
sum += test.getList()[i];
}
System.out.println("Done. Result: " + sum);
}
}
一个并发应用程序可能会发生死锁。如果所有进程正在等待同一组中另一个进程正在使用的资源,那么这组进程可能会发生死锁。
例如线程A等待对象B占有的对象 Z,线程B等待进程A持有的对象Y,那么这两个进程会发生死锁无法继续推进。
这好比一个交通拥塞:小车(线程)要求占有一条确切的道路(资源),但该道路被另一个小车占有(死锁)。