##4.1 并行程序设计模式 ###4.1.1 Future模式 在传统的单线程环境下,调用函数是同步的,也就是说它必须等到服务程序返回结果后,才能进行其他处理。而在Future模式下,调用方法改为异步,而原先等待返回的时间段,在主调用函数中,则可用于处理其他事务。 Future模式的代码实现
:
public class Main {
public static void main(String [] args) {
Client client = new Client();
// 这里会立即返回,因为得到的是FutureData而不是RealData
Data data = client.request("name");
System.out.println("请求完毕");
try {
// 这里可以用一个sleep代替对其他业务逻辑的处理
// 在处理这些业务逻辑的过程中,RealData被创建,从而充分利用了等待时间
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
// 使用真实的数据
System.out.println("真实数据 = " + data.getResult());
}
}
public interface Data {
public String getResult();
}
public class FutureData implements Data {
// FutureData是RealData的包装
protected RealData realData = null;
protected boolean isReady = false;
public synchronized void setRealData(RealData realData) {
if (isReady) {
return;
}
this.isReady = true;
this.realData = realData;
// RealData已经被注入,通知getResult()
notifyAll();
}
public synchronized String getResult() {
try {
while (!isReady) {
// 一直等待,直到RealData被注入
wait();
}
} catch (Exception e) {
e.printStackTrace();
}
// 由RealData实现
return realData.result;
}
}
public class RealData implements Data {
protected final String result;
public RealData(String para) {
// RealData的构造可能很慢,需要用户等待很久,这里使用sleep模拟
StringBuffer sb = new StringBuffer();
try {
for (int i = 0; i < 10; i++) {
sb.append(para);
Thread.sleep(100);
}
} catch (Exception e) {
e.printStackTrace();
}
this.result = sb.toString();
}
public String getResult() {
return result;
}
}
public class Client {
public Data request(final String queryStr) {
final FutureData future = new FutureData();
// RealData的构建很慢,所以在单独的线程中进行
new Thread() {
public void run() {
RealData realData = new RealData(queryStr);
future.setRealData(realData);
}
}.start();
// FutureData会被立即返回
return future;
}
}
Future模式是如此的常用,以至于在JDK的并发包中,就已经内置了一种Future模式的实现。JDK内置的Future模式功能强大,除了基本的功能外,它还可以取消Future任务,或者设定Future任务的超时时间。 Callable接口
是一个用户自定义的实现。在应用程序中,通过实现Callable接口的call()方法,指定FutureTask的实际工作内容和返回对象。 Future接口
提供的线程控制功能有:
boolean cancel(boolean mayInterruptRunning); // 取消任务
boolean isCancelled(); // 是否已经取消
boolean isDone(); // 是否已完成
V get() throws InterruptedException, ExecutionException; // 取得返回对象
V get(long timeout, TimeUnit unit); // 取得返回对象,可以设置超时时间
public class RealData implements Callable<String> {
protected String para;
public RealData(String para) {
this.para = para;
}
@Override
public String call() throws Exception {
StringBuffer sb = new StringBuffer();
try {
for (int i = 0; i < 10; i++) {
sb.append(para);
Thread.sleep(100);
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
}
}
public class Main {
public static void main(String [] args) throws ExecutionException, InterruptedException {
// 构造FutureTask
FutureTask<String> future = new FutureTask<>(new RealData("a"));
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(future);
System.out.println("请求完毕");
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("数据 = " + future.get());
}
}
笔者认为,Future模式的核心优势在于去除了主函数中的等待时间,并使得原本需要等待的时间段可以用于处理其他的业务逻辑,从而充分利用计算机资源。
###4.1.2 Master-Worker模式 Master-Worker模式是常用的并行模式之一。它的核心思想,系统是由两类进程协作工作:Master进程和Worker进程。Master进程负责接收和分配任务,Worker进程负责处理子任务。当各个Worker进程将子任务处理完成后,将结果返回给Master进程,由Master进程做归纳和汇总,从而得到系统的最终结果。
Master-Worker模式的好处,它能够将一个大任务分解成若干个小任务,并行执行,从而提高系统的吞吐量。而对于系统请求者Client来说,任务一旦提交,Master进程会分配任务并立即返回,并不会等待系统全部处理完成后再返回,其处理过程是异步的。因此Client不会出现等待现象。
public class Master {
// 任务队列
protected Queue<Object> workQueue = new ConcurrentLinkedDeque<>();
// Worker进程队列
protected Map<String, Thread> threadMap = new HashMap<>();
// 子任务处理结果集
protected Map<String, Object> resultMap = new ConcurrentHashMap<>();
/**
* 是否所有的子任务都结束了
* @return
*/
public boolean isComplete() {
for (Map.Entry<String, Thread> entry : threadMap.entrySet()) {
if (entry.getValue().getState() != Thread.State.TERMINATED) {
return false;
}
}
return true;
}
/**
* Master的构造,需要一个Worker进程逻辑,和需要的Worker进程数量
*/
public Master(Worker worker, int countWorker) {
worker.setWorkQueue(workQueue);
worker.setResultMap(resultMap);
for (int j = 0; j < countWorker; j++) {
threadMap.put(Integer.toString(j), new Thread(worker, Integer.toString(j)));
}
}
/**
* 提交一个任务
*/
public void submit(Object job) {
workQueue.add(job);
}
/**
* 返回子任务结果集
*/
public Map<String, Object> getResultMap() {
return resultMap;
}
/**
* 开始运行所有的Worker进程,进行处理
*/
public void execute() {
for (Map.Entry<String, Thread> entry : threadMap.entrySet()) {
entry.getValue().start();
}
}
}
public class Worker implements Runnable {
// 任务队列,用于取得子任务
protected Queue<Object> workQueue;
// 子任务处理结果集
protected Map<String, Object> resultMap;
public void setWorkQueue(Queue<Object> workQueue) {
this.workQueue = workQueue;
}
public void setResultMap(Map<String, Object> resultMap) {
this.resultMap = resultMap;
}
// 子任务处理的逻辑,在子类中实现具体逻辑
public Object handle(Object input) {
return input;
}
@Override
public void run() {
while (true) {
// 获取子任务
Object input = workQueue.poll();
if (input == null) break;
// 处理子任务
Object result = handle(input);
// 将处理结果写入结果集
resultMap.put(Integer.toString(input.hashCode()), result);
}
}
}
public class PlusWorker extends Worker {
@Override
public Object handle(Object input) {
Integer i = (Integer) input;
return i * i * i;
}
}
public class Main {
public static void main(String [] args) {
// 固定使用5个Worker,并指定Worker
Master m = new Master(new PlusWorker(), 5);
for (int i = 0; i < 100; i++) {
// 提交100个子任务
m.submit(i);
}
// 开始计算
m.execute();
int re = 0;
// 最终计算结果保存在此
Map<String, Object> resultMap = m.getResultMap();
while (resultMap.size() > 0 || !m.isComplete()) {
// 不需要等待所有Worker都执行完,即可开始计算最终结果
Set<String> keys = resultMap.keySet();
String key = null;
for (String k : keys) {
key = k;
break;
}
Integer i = null;
if (key != null)
i = (Integer) resultMap.get(key);
if (i != null)
// 最终结果
re += i;
if (key != null)
// 移除已经被计算过的项
resultMap.remove(key);
}
System.out.println(re);
}
}
###4.1.3 Guarded Suspension模式 Guarded Suspension意为保护暂停,其核心思想是仅当服务进程准备好时,才提供服务。该模式可以确保系统仅在有能力处理某个任务时,才处理该任务。当系统没有能力处理任务时,它将暂存任务信息,等待系统空闲时处理。
public class Request {
private String name;
public Request(String name) {
this.name = name;
}
public String getName() {
return name;
}
public String toString() {
return "[ " + name + " ]";
}
}
public class RequestQueue {
private LinkedList queue = new LinkedList();
public synchronized Request getRequest() {
try {
while (queue.size() == 0) {
// 等待直到有新的Request加入
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回Request队列中的第一个请求,并移除
return (Request) queue.remove();
}
public synchronized void addRequest(Request request) {
// 加入新的Request请求
queue.add(request);
// 通知getRequest()方法
notifyAll();
}
}
public class ServerThread extends Thread {
private RequestQueue requestQueue;
public ServerThread(RequestQueue requestQueue, String name) {
super(name);
this.requestQueue = requestQueue;
}
@Override
public void run() {
try {
while (true) {
// 得到请求
final Request request = requestQueue.getRequest();
// 模拟处理请求耗时
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " handles " + request.getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ClientThread extends Thread {
private RequestQueue requestQueue;
public ClientThread(RequestQueue requestQueue, String name) {
super(name);
this.requestQueue = requestQueue;
}
public void run() {
try {
for (int i = 0; i < 10; i++) {
Request request = new Request("RequestID : " + i + " Thread_Name : " + Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + " requests " + request);
requestQueue.addRequest(request);
Thread.sleep(10);
System.out.println("ClientThread Name is : " + Thread.currentThread().getName());
}
System.out.println(Thread.currentThread().getName() + " request end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String [] args) {
RequestQueue requestQueue = new RequestQueue();
for(int i = 0; i < 10; i++) {
new ServerThread(requestQueue, "ServerThread" + i).start();
}
for(int i = 0; i < 10; i++) {
new ClientThread(requestQueue, "ClientThread" + i).start();
}
}
}
在实际软件开发过程中,往往需要依靠多种模式,多种结构相组合,以达到预期的软件功能和性能要求。
Guarded Suspension模式可以在一定程度上缓解系统的压力,它可以将系统的负载在时间轴上均匀的分配,使用该模式后,可以有效降低系统的瞬时负载,对提高系统的抗压力和稳定性很有帮助。 ###4.1.4 不变模式 在并发软件开发过程中,同步操作似乎是必不可少的。当多线程对同一个对象进行读写操作时,为了保证对象数据的一致性和正确性,有必要对对象进行同步。而同步操作对系统性能有相当的损耗。为了仅可能地去除这些同步操作,提高并行程序性能,可以使用一种不可变的对象,依靠对象的不变性,可以确保其在没有同步操作的多线程环境中依然保持内部状态的一致性和正确性。这就是不变模式
。
不变模式天生就是多线程友好的,它的核心思想是,一个对象一旦被创建,则它的内部状态将永远不会改变。所以,没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。
不变模式和只读属性是有一定区别的。不变模式比只读属性具有更强的一致性和不变性。对只读属性的对象而言,对象本身不能被其他线程修改,但是对象的自身状态却可能自行修改。
public final class Product { // 确保无子类
// 私有属性,不会被其他对象获取
private final String no;
// final保证属性不会被2次赋值
private final String name;
private final double price;
public Product(String no, String name, double price) {
// 在创建对象时,必须指定数据,因为创建之后,无法进行修改
this.no = no;
this.name = name;
this.price = price;
}
public String getNo() {
return no;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
在JDK中,不变模式的应用非常广泛。其中,最为典型的就是java.lang.String类。此外,所有的元数据类包装类,都是使用不变模式实现的
。
不变模式通过回避问题而不是解决问题的态度来处理多线程并发访问控制。不变对象是不需要进行同步操作的。由于并发同步会对性能产生不良的影响,因此,在需求允许的情况下,不变模式可以提高系统的并发性能和并发量。 ###4.1.5 生产者-消费者模式 生产者-消费者模式是一个经典的多线程设计模式,它为多线程间的协作提供了良好的解决方案。在生产者-消费者模式中,通常有两类线程,即若干个生产者线程和若干个消费者线程。生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务。生产者和消费者通过共享内存缓冲区进行通信。共享内存缓冲区的主要功能是数据在多线程间的共享。此外,通过该缓冲区,可以缓解生产者和消费者间的性能差。
public class PCData {
private final int intData;
public PCData(int d) {
this.intData = d;
}
public PCData(String d) {
intData = Integer.valueOf(d);
}
public int getData() {
return intData;
}
@Override
public String toString() {
return "data : " + intData;
}
}
public class Producer implements Runnable {
private volatile boolean isRunning = true;
// 内存缓冲区
private BlockingQueue<PCData> queue;
// 总数 原子操作
private static AtomicInteger count = new AtomicInteger();
private static final int SLEEPTIME = 1000;
public Producer(BlockingQueue<PCData> queue) {
this.queue = queue;
}
@Override
public void run() {
PCData data = null;
Random r = new Random();
System.out.println("start producer id = " + Thread.currentThread().getId());
try {
while (isRunning) {
Thread.sleep(r.nextInt(SLEEPTIME));
data = new PCData(count.incrementAndGet());
System.out.println(data + " is put into queue");
// 提交数据到缓存
if (!queue.offer(data, 2, TimeUnit.SECONDS)) {
System.err.println("failed to put data : " + data);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
public void stop() {
isRunning = false;
}
}
public class Consumer implements Runnable {
// 内存缓冲区
private BlockingQueue<PCData> queue;
private static final int SLEEPTIME = 1000;
public Consumer(BlockingQueue<PCData> queue) {
this.queue = queue;
}
@Override
public void run() {
Random r = new Random();
System.out.println("start producer id = " + Thread.currentThread().getId());
try {
while (true) {
PCData data = queue.take();
if (null != data) {
int re = data.getData() * data.getData();
System.out.println(MessageFormat.format("{0} * {1} = {2}", data.getData(), data.getData(), re));
Thread.sleep(r.nextInt(SLEEPTIME));
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
public class Main {
public static void main(String [] args) {
BlockingQueue<PCData> queue = new LinkedBlockingDeque<>();
Producer producer1 = new Producer(queue);
Producer producer2 = new Producer(queue);
Producer producer3 = new Producer(queue);
Consumer consumer1 = new Consumer(queue);
Consumer consumer2 = new Consumer(queue);
Consumer consumer3 = new Consumer(queue);
ExecutorService service = Executors.newCachedThreadPool();
service.execute(producer1);
service.execute(producer2);
service.execute(producer3);
service.execute(consumer1);
service.execute(consumer2);
service.execute(consumer3);
Thread.sleep(10 * 1000);
producer1.stop();
producer2.stop();
producer3.stop();
Thread.sleep(3 * 1000);
service.shutdown();
}
}
生产者-消费者模式能够很好地对生产者线程和消费者线程进行解耦,优化了系统整体结构。同时,由于缓冲区的作用,允许生产者线程和消费者线程存在执行的性能差异,从一定程度上缓解了性能瓶颈对系统性能的影响。 ##4.2 JDK多任务执行框架 ###4.2.1 无限制线程的缺陷 多线程的软件设计方法确实可以最大限度地发挥现代多核处理器的计算能力,提高生产系统的吞吐量和性能,但是,若不加控制和管理地随意使用线程,对系统的性能反而会产生不利的影响。
new Thread(new Runnable() {
@Override
public void run() {
// do sth.
}
}).start();
以上代码创建了一个线程,并在run()方法结束后,自动回收该线程。
虽然与进程相比,线程是一种轻量级的工具,但其创建和关闭依然需要花费时间,如果为每一个小任务都创建一个线程,很有可能出现创建和销毁线程所占用的时间大于该线程真实工作所消耗的时间,反而会得不偿失。
其次,线程本身也是有要占用内存空间的,大量的线程会抢占宝贵的内存资源,如果处理不当,可能会导致OOM异常。即便没有,大量的线程回收也会给GC带来很大的压力,延长GC的停顿时间。
在实际生产过程中,线程的数量必须得到控制。盲目地大量创建线程对系统性能是有伤害的。 ###4.2.2 简单的线程池实现 线程池的基本功能就是进行线程的复用。
public class PThread extends Thread {
// 线程池
private ThreadPool pool;
// 任务
private Runnable target;
private boolean isShutDown = false;
private boolean isIdle = false;
// 构造函数
public PThread(Runnable target, String name, ThreadPool pool) {
super(name);
this.pool = pool;
this.target = target;
}
public Runnable getTarget() {
return target;
}
public boolean isIdle() {
return isIdle;
}
@Override
public void run() {
try {
// 只要没有关闭,则一只不结束该线程
while (!isShutDown) {
isIdle = false;
if (target != null) {
// 运行任务
target.run();
}
// 任务结束了,到闲置状态
isIdle = true;
// 该任务结束后,不关闭线程,而是放入线程池空闲队列
pool.repool();
synchronized (this) {
// 线程空闲,等待新的任务到来
wait();
}
isIdle = false;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized setTarget(Runnable target) {
this.target = target;
// 设置了任务之后,通知run方法,开始执行这个任务
notifyAll();
}
public synchronized void shutDown() {
isShutDown = true;
notifyAll();
}
}
public class ThreadPool {
private static ThreadPool instance = null;
// 空闲的线程队列
private List<PThread> idleThreads;
// 已有的线程总数
private int threadCounter;
private boolean isShutDown = false;
private ThreadPool() {
this.idleThreads = new Vector<>(5);
threadCounter = 0;
}
public int getCreatedThreadsCount() {
return threadCounter;
}
// 取得线程池的实例
public synchronized static ThreadPool getInstance() {
if (instance == null) {
instance = new ThreadPool();
}
return instance;
}
// 将线程放入池中
protected synchronized void repool(PThread thread) {
if (!isShutDown) {
idleThreads.add(thread);
} else {
thread.shutDown();
}
}
// 停止池中所有线程
public synchronized void shutDown() {
isShutDown = true;
for (int i = 0; i < idleThreads.size(); i++) {
PThread thread = idleThreads.get(i);
thread.shutDown();
}
}
// 执行任务
public synchronized void start(Runnable target) {
PThread thread = null;
// 如果有空闲线程,则直接使用
if (idleThreads.size() > 0) {
int lastIndex = idleThreads.size() - 1;
thread = idleThreads.get(lastIndex);
idleThreads.remove(lastIndex);
// 立即执行这个任务
thread.setTarget(target);
} else { // 没有空闲线程,则创建新线程
threadCounter ++;
// 创建新线程
thread = new PThread(target, "PThread #" + threadCounter, this);
// 启动这个线程
thread.start();
}
}
}
public class MyTask extends Runnable {
protected String name;
public MyTask() {
}
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
try {
// 使用sleep方法代替一个具体功能的执行
Thread.sleep(100);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String [] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new MyTask("Test No Thread Pool " + i)).start();
}
for (int i = 0; i < 1000; i++) {
ThreadPool.getInstance().start(new MyTask("Test Thread Pool " + i));
}
}
}
使用线程池后,线程的创建和关闭通常由线程池维护。线程通常不会因为执行完一次任务而被关闭,线程池中的线程会被多个任务重复使用。线程池可以减少线程频繁调度的开销。 ###4.2.3 Executor框架 为了能更好的控制多线程,JDK提供了一套Executor框架,帮助开发人员有效地进行线程控制。其核心成员,如下: ThreadPoolExecutor
:线程池,实现了Executor接口,因此通过这个接口,任何Runnable的对象都可以被其调度执行; Executors
:线程池工厂,可以取得一个特定功能的线程池,如下:
// 返回一个固定线程数量的线程池,该线程池中的线程数量始终不变。当一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
public static ExecutorService newFixedThreadPool(int nThreads);
// 返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,等待线程空闲,按先入先出的顺序执行队列中的任务。
public static ExecutorService newSingleThreadExecutor();
// 返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
public static ExecutorService newCachedThreadPool();
// 返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。
public static ScheduledExecutorService newSingleThreadScheduledExecutor();
// 返回一个指定线程数量的ScheduledExecutorService对象。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
###4.2.4 自定义线程池 无论是newFixedThreadPool()方法,newSingleThreadExecutor()还是newCachedThreadPool()方法,其内部实现均使用了ThreadPoolExecutor:
public ThreadPoolExecutor(int corePoolSize, // 指定线程池中的线程数量
int maximumPoolSize, // 指定线程池中的最大线程数量
long keepAliveTime, // 当线程池中的线程数量超过corePoolSize时,多余的空闲线程的存活时间。即,超过corePoolSize的空闲线程,在多长时间内被销毁。
TimeUnit unit, // keepAliveTime的时间单位
BlockingQueue<Runnable> workQueue, // 任务队列,被提交但尚未被执行的任务。
ThreadFactory threadFactory, // 线程工厂,用于创建线程,一般用默认的即可。
RejectedExecutionHandler handler // 拒绝策略。当任务太多来不及处理,如何拒绝任务。
)
参数workQueue指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。根据队列功能分类,在ThreadPoolExecutor的构造函数中可以使用以下几种BlockingQueue: 直接提交的队列
:该功能由SynchronousQueue对象提供。SynchronousQueue是一个特殊的BlockingQueue。其没有容量,每一个插入操作都要等待一个相应的删除操作,反之,每一个删除操作都要等待对应的插入操作。SynchronousQueue不保存任务,它总是将任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程,如果线程数量已经达到最大值,则执行拒绝策略。因此,使用SynchronousQueue队列,通常要设置很大的maximumPoolSize值,否则很容易执行异常策略。
有界的任务队列
:该功能由ArrayBlockingQueue对象提供。ArrayBlockingQueue的构造函数必须带一个容量参数,表示该队列的最大容量。当使用有界的任务队列时,若有新的任务需要执行,如果线程池的实际线程数小于corePoolSize,则会优先创建新的线程,若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务。若大于maximumPoolSize,则执行拒绝策略。可见,有界队列仅当任务队列装满时,才可能将线程数提升到corePoolSize以上,换言之,除非系统非常繁忙,否则确保核心线程数维持在corePoolSize。
无界的任务队列
:该功能由LinkedBlockingQueue对象提供。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,线程池会生成新的线程执行任务,但当系统的线程数达到corePoolSize后,就不会继续增加。若后续仍有新的任务加入,而又没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。
优先的任务队列
:该功能由PriorityBlockingQueue对象提供。指带有执行优先级的任务队列。可以控制任务的执行先后顺序,是一个特殊的无界队列。无论是有界队列ArrayBlockingQueue,还是未指定大小的无界队列LinkedBlockingQueue都是按照先进先出算法处理任务的。而PriorityBlockingQueue则可以根据任务自身的优先级顺序先后执行,在确保系统性能的同时,也能有很好的质量保证(总是确保高优先级的任务先执行)。
使用优先队列的好处是在系统繁忙时,可以忽略任务的提交先后次序,总是让优先级更高的任务先执行。使用优先队列时,任务线程必须实现Comparable接口,优先队列则会根据该接口对任务进行排序。
ThreadPoolExecutor的最后一个参数指定了拒绝策略,即当任务数量超过系统实际承载能力时,该如何处理。JDK内置提供了4种拒绝策略: AbortPolicy策略
:该策略会直接抛出异常,阻止系统正常工作。 CallerRunsPolicy策略
:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。 DiscardOldestPolicy策略
:该策略将会丢弃最老的一个任务,也就是即将被执行的一个任务,并尝试再次提交当前任务。 DiscardPolicy策略
:该策略默默地丢弃无法处理的任务,不予任何处理。
以上内置策略均实现了RejectedExecutionHandler接口,若以上策略仍无法满足实际应用需要,完全可以自己扩展RejectedExecutionHandler接口。
public interface RejectedExecutionHandler {
// r : 请求执行的任务 executor : 当前的线程池
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
###4.2.5 优化线程池大小 线程池的大小对系统性能有一定的影响。过大或者过小的线程数量都无法发挥最优的系统性能,但是线程池大小的确定也不需要做的非常精确,因为只要避免极大或极小两种情况,线程池的大小对系统的性能并不会影响太大。一般来说,确定线程池的大小需要考虑CPU数量,内存大小,JDBC连接等因素。
Nthreads = Ncpu * Ucpu * (1 + W/C);
Ncpu:CPU的数量;
Ucpu:目标CPU的使用率,0<=Ucpu<=1;
W/C:等待时间与计算时间的比率;
###4.2.6 扩展ThreadPoolExecutor ThreadPoolExecutor是一个可以扩展的线程池,它提供了beforeExecute(),afterExecute()和terminated()3个接口对线程池进行控制。 以beforeExecute(),afterExecute()为例,在ThreadPoolExecutor.Worker.runTask()方法内部提供了这样的实现,如下:
boolean ran = false;
beforeExecute(thread, task); // 运行前
try {
task.run(); // 运行任务
ran = true;
afterExecute(task, null); // 运行结束后
++completedTasks;
} catch (RuntimeException ex) {
if (!ran) {
afterExecute(task, ex); // 运行结束后
}
throw ex;
}
ThreadPoolExecutor.Worker是ThreadPoolExecutor的内部类,它是一个实现了Runnable接口的类。ThreadPoolExecutor线程池中的工作线程也是Worker实例。Worker.runTask()方法会被线程池以多线程模式异步调用,即Worker.runTask()会同时被多个线程访问。因此其beforeExecute(),afterExecute()接口也将同时被多线程访问。
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("beforeExecute MyThread Name : " + t.getName());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("afterExecute TID : " + Thread.currentThread().getId());
System.out.println("afterExecute PoolSize : " + this.getPoolSize());
}
}
通过扩展线程池,开发者可以获取线程池调度的内部细节,对并行程序故障排查很有帮助。 ##4.3 JDK并发数据结构 ###4.3.1 并发List Vector或者CopyOnWriteArrayList是两个线程安全的List实现,ArrayList不是线程安全的。应该尽量避免ArrayList在多线程环境中使用。如果因为某些原因必须使用的,则需要使用:
// 进行线程安全封装
Collections.synchronizedList(List<T> list);
CopyOnWriteArrayList实现机制
:即当进行写操作时,复制该对象;若进行读操作时,则直接返回结果,操作过程中不进行同步。其很好的利用了对象不变性,在没有对对象进行写操作前,由于对象未发生改变,因此不需要加锁。而在试图改变对象时,总是先获取对象的一个副本,然后对副本进行修改,最后将副本写回。其核心思想是减少锁竞争,从而提高在高并发时的读取性能,但是它却在一定程度上牺牲了写的性能。
Vector使用了synchronized同步关键字,所有的get()操作都必须先取得对象锁才能进行。在高并发的情况下,大量的锁竞争会拖累系统性能。
在读多写少的高并发环境下,使用CopyOnWriteArrayList可以提高系统的性能。但是,在写多读少的场合,CopyOnWriteArrayList的性能不如Vector。 ###4.3.2 并发Set 和List相似,并发Set也有一个CopyOnWriteArraySet,它实现了Set接口,并且是线程安全的。它的内部实现完全依赖于CopyOnWriteArrayList,因此它的特性和CopyOnWriteArrayList完全一致,适用于读多写少的高并发场合。在需要并发写的场合,则可以使用Collections的方法:
// 进行线程安全封装
Collections.synchronizedSet(Set<T> set);
###4.3.3 并发Map 在多线程环境中使用Map,一般也可以使用Collections的synchronizedMap()方法得到一个线程安全的Map。但在高并发的情况下,这个Map的性能表现不是最优的。JDK中提供了一个专用于高并发的Map实现ConcurrentHashMap。
ConcurrentHashMap是专门为线程并发而设计的HashMap。它的get()操作是无锁的,它的put()操作的锁粒度又小于同步的HashMap。因此它的整体性能优于同步的HashMap。
###4.3.4 并发Queue 在并发队列上,JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列。不论哪种实现,都继承自Queue接口。
ConcurrentLinkedQueue
是一个适用于高并发场景下的队列。它通过无锁的方式,实现了高并发状态下的高性能。通常,ConcurrentLinkedQueue的性能要高于BlockingQueue。
BlockingQueue
与ConcurrentLinkedQueue的使用场景不同,BlockingQueue的主要功能并不是在于提升高并发时的队列性能,而在于简化多线程间的数据共享。
BlockingQueue
的典型使用场景是在生产者-消费者模式中,生产者总是将产品放入BlockingQueue队列中,而消费者从队列中取出产品消费,从而实现数据共享。
BlockingQueue
提供一种读写阻塞等待的机制,即如果消费者速度较快,则BlockingQueue可能被清空,此时,消费线程再试图从BlockingQueue读取数据时就会被阻塞。反之,如果生产线程较快,则BlockingQueue可能会被装满,此时,生产线程再试图向BlockingQueue队列中装入数据时,便会被阻塞等待。
BlockingQueue的核心方法如下: offer(E object)
:将object加到BlockingQueue里,如果BlockingQueue有足够空间,则返回true,否则返回false(该方法不阻塞当前执行方法的线程); offer(E object, long timeout, TimeUnit unit)
:可以设定等待的时间,如果在指定的时间内,还不能往队列中加入,则返回false; put(E object)
:把Object加到BlockingQueue里,如果BlockingQueue没有足够空间,则调用此方法的线程被阻塞直到BlockingQueue里面有空闲空间时再继续; poll(long time)
:取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到返回null; poll(long timeout, TimeUnit unit)
:从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。超时后依然没有取得数据则返回失败; take()
:取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态,直到BlockingQueue有新的数据被加入; drainTo()
:一次性从BlockingQueue获取所有可用的对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据的效率,不需要多次分批加锁或释放锁;
ArrayBlockingQueue
:它是一种基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,用于缓存队列中的数据对象。此外,ArrayBlockingQueue内部还保存着两个整形变量,分别标示着队列的头部和尾部在数组中的位置。在创建ArrayBlockingQueue时,还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
LinkedBlockingQueue
:它是一种基于链表的阻塞队列实现,与ArrayListBlockingQueue类似,内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值,默认不限制大小),才会阻塞生产者队列,直到消费者从队列中消费掉一个数据,生产者线程才会被唤醒。 ###4.3.5 并发Deque 在JDK 1.6中,提供了一种双端队列(Double-Ended Queue),简称Deque。Deque允许在队列的头部或者尾部进行出队和入队操作。与Queue相比,它们具有更加复杂的功能。 LinkedList,ArrayDeque和LinkedBlockingDeque
它们都实现了双端队列Deque接口,如下: LinkedList
使用链表实现了双端队列。 ArrayDeque
使用数组实现双端队列,通常情况下,由于ArrayDeque基于数组实现,拥有高效的随机访问性能,因此ArrayDeque具有更好的遍历性能。但是当队列的大小变化较大时,ArrayDeque
需要重新分配内存,并进行数组复制,在这种情况下,基于链表的LinkedList没有内存调整和数组复制的负担,性能表现比较好。但无论是LinkedList或是ArrayDeque,它们都不是线程安全的。 LinkedBlockingDeque
:是一个线程安全的双端队列实现。可以说,它已经是最为复杂的一个队列实现。在内部实现中,LinkedBlockingDeque使用链表结构。在每一个队列节点都维护一个前驱节点和一个后驱节点。LinkedBlockingDeque没有进行读写锁的分离,因此同一时间只能有一个线程对其进行操作。因此,在高并发应用中,它的性能要远远低于LinkedBlockingQueue,更要低于ConcurrentLinkedQueue。 ##4.4 并发控制方法 ###4.4.1 Java内存模型与volatile 在Java中,每一个线程有一块工作内存区,其中存放着被所有线程共享的主内存中的变量的值的拷贝。当线程执行时,它在自己的工作内存中操作这些变量。为了存取一个共享的变量,一个线程通常先获取锁定并清除它的工作内存区,这保证该共享变量从所有线程的共享内存区正确地装入到线程的工作内存区,当线程解锁时保证该工作内存区中变量的值写回到共享内存中。
一个线程可以执行的操作有:使用(use),赋值(assign),装载(load),存储(store),锁定(lock),解锁(unlock)。而主内存可以执行的操作有:读(read),写(write),锁定(lock),解锁(unlock),每一个操作都是原子的。
当一个线程使用某一个变量时,不论程序是否正确地使用线程同步操作,它获取的值一定是由它本身或者其他线程存储到变量中的值。例如,如果两个线程把不同的值或者对象引用存储到同一个共享变量中,那么该变量的值要么是这个线程的,要么是那个线程的,共享变量的值不会由两个线程的引用值组合而成(除long,double外)。
double和long类型变量的非原子处理
:如果一个double或者long变量没有声明为volatile,则变量在主内存进行read或write操作时,主内存把它当作两个32位的read或write操作进行处理,这两个操作在时间上是分开的,可能会有其他操作介于它们之间。如果这种情况发生,则两个并发的线程对共享的非volatile类型的double或long变量赋不同的值,那么随后对该变量的使用而获取的值可能不等于任何一个线程所赋的值,而可能是依赖于具体应用的两个线程所赋的值的混合。因此,在32位的系统中,必须对double或long进行同步。
一个变量是Java程序可以存取的一个地址,它不仅包括基本类型变量,引用类型变量,而且还包括数组类型变量。保存在主内存区的变量可以被所有线程共享,但一个线程存取另一个线程的参数或局部变量是不可能的。所以开发人员不必担心局部变量的线程安全问题。
使用(use),赋值(assign),锁定(lock)和解锁(unlock)操作都是线程的执行引擎和线程的工作内存的原子操作;但主内存和线程的工作内存间的数据传送并不是满足原子性,即当数据从主内存复制到工作内存时,必须出现两个动作:(1)由主内存执行的读(read)操作;(2)由工作内存执行的相应的load操作。当数据从工作内存拷贝到主内存时,也出现两个操作:(1)由工作内存执行的存储(store)操作;(2)由主内存执行的相应的写(write)操作。由于主内存和工作内存间传送数据需要一定的时间,而且每次所消耗的时间可能是不同的。因此从另一个线程的角度来看,一个线程对变量的操作顺序可能是不同的。比如:某一线程内的代码是先给变量a赋值,再给变量b赋值,在另一个线程中,可能先在主内存中看见变量b的更新,再看见变量a的更新。当然,在一个线程中对同一个变量的操作次序,一定和该线程中的实际次序相吻合。
以上,各个操作的含义如下: 线程的use操作
:把一个变量在线程工作内存中的拷贝内容传送给线程执行引擎; 线程的assign操作
:把一个变量从线程执行引擎传送到变量的线程工作内存; 主内存的read操作
:把一个变量的主内存拷贝的内容送输到线程的工作内存,以便load操作使用; 线程的load操作
:把read操作从主内存中得到的值放入变量的线程工作内存中; 线程的store操作
:把一个变量的线程工作内存拷贝内容传送到主内存中,以便write操作使用; 主内存的write操作
:把store操作从线程工作内存中得到的值放入主内存中的变量拷贝中; 主内存的lock操作
:使线程获得一个独占锁; 主内存的unlock操作
:使线程释放一个独占锁;
这样,线程和变量的相互作用由use,assign,load和store操作的序列组成。主内存为线程的每个load操作执行read操作,为线程的每个store操作执行write操作。线程锁定和解锁由lock和unlock操作完成。
线程的每个load操作有唯一一个主内存的read操作和它相匹配,这个load操作跟在read操作的后面;线程的每个store操作有唯一一个主内存的write操作和它相匹配,这个write操作跟在store操作后面。
由于每个线程都有自己的工作内存区,因此当一个线程改变自己的工作内存中的数据时,对其他线程来说,可能是不见的。为此,可以使用volatile关键字迫使所有的线程均读写主内存中的对应变量,从而使得volatile变量在多线程间可见。
声明为volatile的变量可以做以下保证
: (1)其他线程对变量的修改,可以即时反应在当前线程中; (2)确保当前线程对volatile变量的修改,能即时写回共享主内存中,并被其他线程所见; (3)使用volatile声明的变量,编译器会保证其有序性;
public class MyThread extends Thread {
// 确保stop变量在多线程中可见
private volatile boolean stop = false;
// 在其他线程中调用,停止本线程
public void stopMe() {
stop = true;
}
// 在其他线程中改变stop的值
@Override
public void run() {
int i = 0;
while (!stop) {
i++;
}
System.out.println("Stop Thread");
}
}
/**
* 如果使用-client模式运行这段代码,无论是否使用volatile,其运行效果都是一样
* 的,MyThread总是会发现stop状态的修改(会发现修改,但不是即时的)。
* -server模式下有这样明显的区别是因为在该模式下,JVM会对代码做一些优化,
* 使得优化后的代码不再去读取未曾发生改变的,且未标记为volatile的stop变量,
* 使得在-server模式下,该变量的修改线程间不可见。
* @throws Exception
*/
public static void testVolatile() throws Exception {
MyThread t = new MyThread();
t.start();
Thread.sleep(1000);
t.stopMe();
Thread.sleep(1000);
}
###4.4.2 同步关键字synchronized 同步关键字synchronized使用简洁,代码可维护性好。在JDK 6中,性能也比早期的JDK有很大的改进。如果可以满足程序要求,应该首先考虑这种同步方式。
首先,关键字synchronized一个最为常用的用法是锁定一个this对象
的方法。
public synchronized void method() {}
其次,使用synchronized还可以构造同步块,与同步方法相比,同步块更为精确地控制同步代码范围,缩小同步块。一个小的同步代码非常有利于锁的快进快出,从而使系统拥有更高的吞吐量。
public void method(SomeObject so) {
synchronized (so){
// some code here
}
}
此外,synchronized方法还可以用于static函数,相当于将锁加到当前Class对象上。
public synchronized static void method() {}
虽然synchronized可以保证对象或者代码段的线程安全,但是仅使用synchronized还不足以控制拥有复杂逻辑的线程交互。为了实现多线程间的交互,还需要使用Object对象的wait()和notify()方法。
函数wait()可以让线程等待当前对象上的通知(notify()被调用),在wait()过程中,线程会释放对象锁。它的典型用法如下:
synchronized (obj) {
while(<等待条件>) {
obj.wait();
... // 收到通知后,继续执行
}
}
首先,在使用wait()方法前,需要获得对象锁,以上代码片段就事先获得了obj的独占锁。其次wait()方法需要在一个循环中使用,指明跳出循环的条件。在wait()方法执行时,当前线程会释放obj的独占锁,供其他线程使用。
当等待在obj上的线程收到一个obj.notify()时,它就能重新获得obj的独占锁,并继续运行。方法notify()将唤醒一个等待在当前对象上的线程。如果当前有多个线程等待,那么notify()方法将会随机选择其中一个。
两个线程死锁代码
:
synchronized (obj) {
while(<等待条件>) {
obj.wait();
... // 收到通知后,继续执行
obj1.notify();
}
}
synchronized (obj1) {
while(<等待条件>) {
obj1.wait();
... // 收到通知后,继续执行
obj.notify();
}
}
下面代码实现了一个阻塞队列。该队列有两个方法,分别是pop()和put()。方法pop()从队列中获取第一个数据,并返回,如果队列为空,则等待一个有效的对象;方法put()将一个对象保存到队列中,并通知一个在等待中的pop()方法。
public class BlockQueue {
private List list = new ArrayList();
public synchronized Object pop() throws InterruptedException {
while (list.size() == 0) { // 如果队列为空,则等待
this.wait();
}
if (list.size() > 0) {
return list.remove(0); // 队列不为空,则返回第一个对象
} else {
return null;
}
}
public synchronized void put(Object o) {
list.add(o); // 增加对象到队列中
this.notify(); // 通知一个pop()方法,可以取得数据
}
}
与wait()方法类似,Object对象还提供了以下操作: void wait(long timeout)
: 在当前对象上等待,最大等待时间不超过timeout毫秒。 void wait(long timeout, int nanos)
:在当前对象上等待,最大等待时间不超过timeout毫秒,nanos纳秒。
与notify()方法类似,Object对象还提供了notifyAll()方法,与notify()方法不同,notifyAll()将会唤醒所有等待在当前对象上的线程。
为了有效地控制线程间的协作,需要配合使用synchronized以及notify()和wait()等方法。 ###4.4.3 ReentrantLock重入锁 ReentrantLock称为重入锁。它比内部锁synchronized拥有更强大的功能,它可中断,可定时。
ReentrantLock还提供了公平和非公平两种锁。公平锁
可以保证在锁的等待队列中的各个线程是公平的,因此不会存在插队情况,对锁的获取总是先进先出,而非公平的ReentrantLock
不做这个保证,申请锁的线程可能插队,后申请锁的线程有可能先拿到锁。公平锁的实现代价比非公平锁大,因此从性能上分析,非公平锁的性能要好的多。因此,若无特殊的需要,应该优先选择非公平锁,而synchronized提供锁也不是绝对公平的。
通过以下构造函数可以指定锁是否公平:
public ReentrantLock(boolean fair)
使用ReentrantLock时,还要时刻牢记,一定要在程序最后释放锁
。一般释放锁的代码要写在finally里。否则,如果程序出现异常,ReentrantLock锁就永远无法释放了。相比synchronized,JVM虚拟机总是会在最后自动释放synchronized锁。
ReentrantLock锁提供了以下重要的方法: lock()
:获得锁,如果锁已经被占用,则等待。 lockInterruptibly()
:获得锁,但优先响应中断。与lock()的不同之处在于,lockInterruptibly()在锁等待的过程中可以响应中断事件
。 tryLock()
:尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。 tryLock(long time, TimeUnit unit)
:在给定时间内尝试获得锁。 unlock()
:释放锁。
public class ReentrantLockThread extends Thread {
private static final Lock lock = new ReentrantLock();
public ReentrantLockThread(String name) {
super(name);
}
@Override
public void run() {
try {
while (true) {
// 在给定时间内尝试获得锁
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
// if (lock.tryLock()) 尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
// lock.lock(); 获得锁,如果锁已经被占用,则等待。
// lock.lockInterruptibly(); 获得锁,但优先响应中断。
try {
System.out.println("locked " + Thread.currentThread().getName());
Thread.sleep(1000);
} finally {
lock.unlock();
System.out.println("unlocked " + Thread.currentThread().getName());
}
break;
} else {
System.out.println("unable to lock " + Thread.currentThread().getName());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + " is Interrupted");
}
}
}
public class ReentrantLockTest {
public static void main(String [] args) throws InterruptedException {
Thread first = new ReentrantLockThread("FirstThread");
Thread second = new ReentrantLockThread("SecondThread");
first.start();
second.start();
Thread.sleep(600);
// 中断
second.interrupt();
}
}
###4.4.4 ReadWriteLock读写锁 ReadWriteLock是JDK 5中提供的读写分离锁。读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写锁允许多个线程同时读,由于考虑到数据完整性,写写操作和读写操作间依然是需要相互等待和持有锁的。如果在系统中,读操作次数远远大于写操作,则读写锁就可以发挥最大的功效,提升系统的性能。
public class ReadWriteLockTest {
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
public static void main(String [] args) throws InterruptedException {
Thread read = new Thread(new Runnable() {
@Override
public void run() {
try {
readLock.lock();
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
});
Thread write = new Thread(new Runnable() {
@Override
public void run() {
try {
writeLock.lock();
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
});
read.start();
write.start();
}
}
在读多写少的场合,使用读写锁可以分离读操作和写操作,使所有读操作间真正并行,因此,能够有效提高系统的并发能力。 ###4.4.5 Condition对象 线程间的协调工作光有锁是不够的,在业务层,可能会有复杂的线程间协作逻辑。Condition对象就可以用于协调多线程间的复杂协作。 Condition是与锁相关联的。通过Lock接口的Condition newCondition()方法可以生成一个与锁绑定的Condition实例。Condition对象和锁的关系,就如同Object.wait(),Object.notify()两个函数以及synchronized关键字一样,它们都可以配合使用已完成对多线程协作的控制。
Condition接口提供的基本方法
: public void await() throws InterruptedException
:会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。或者当前线程被中断时,也能跳出等待。 public void awaitUninterruptibly()
:与await()方法基本相同,但是它并不会在等待过程中响应中断。 public void singal()
:唤醒一个在等待中的线程,相对的singalAll()方法会唤醒所有在等待中的线程。
以ArrayBlockingQueue为例,在它的put()方法实现如下:
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 对put()方法做同步
try {
while (count == items.length) // 如果当前队列已满
notFull.await(); // 等待队列有足够的空间
insert(e); // 当notFull被通知时,说明有足够的空间
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal(); // 通知需要take()的线程,队列已有数据
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 对take()方法做同步
try {
while (count == 0) // 如果队列为空
notEmpty.await(); // 则消费队列要等待一个非空的信号
return extract();
} finally {
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
E x = this.<E>cast(items[takeIndex]);
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
notFull.signal(); // 通知put()线程队列已有空闲空间
return x;
}
###4.4.6 Semaphore信号量 信号量为多线程协作提供了更为强大的控制方法。广义上说,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReetrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程同时访问某一个资源。
public Semaphore(int permits)
public Semaphore(int permits, boolean fair) // 第二个参数可以指定是否公平
在构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。当每个线程每次只申请一个许可时,这就相当于指定了同时有多少个线程可以访问某一个资源。 public void acquire()
:尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断。 public void acquireUninterruptibly()
:与acquire()方法类似,但是不响应中断。 public boolean tryAcquire()
:尝试获得一个许可,如果成功返回true,失败返回false,它不会进行等待,立即返回。 public boolean tryAcquire(long timeout, TimeUnit unit)
:与tryAcquire()类似,增加了等待超时时间。 public void release()
:用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。 ###4.4.7 ThreadLocal线程局部变量 ThreadLocal是一种多线程间并发访问变量的解决方案。与synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此它不是一种数据共享的解决方案。
从性能上来说,认为ThreadLocal并不具有绝对的优势,在并发量不是很高时,也许加锁的性能会更好。但作为一套与锁完全无关的线程解决方案,在高并发或者锁竞争激烈的场合,使用ThreadLocal可以在一定程度上减少锁竞争。
值得注意的是,不同线程间的对象副本,并不是由ThreadLocal创建的,而必须在线程内创建,并保证不同线程间实例均不相同。若不同线程间使用了同一个对象实例,即使把它放到ThreadLocal中保护起来,也无法保证其线程安全性。为理解这一点,可以进一步查看ThreadLocal.set()方法的实现:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 这里取得线程的ThreadLocals成员变量
if (map != null) // ThreadLocals是一个Map,存放线程的本地变量
map.set(this, value); // 将value设置到map中,get()方法也是从这里取值
else
createMap(t, value);
}
可以看到,ThreadLocal先取得当前线程的ThreadLocalMap,并将值设置到ThreadLocalMap中(每个线程都有自己独立的ThreadLocalMap)。在整个过程中并不生成value的副本,因此,应该避免将同一个实例设置到不同线程的ThreadLocal中,否则其线程安全性无法保证。 ##4.5 “锁”的性能和优化 “锁”是最常用的同步方法之一。在高并发的环境下,激烈的锁竞争会导致程序的性能下降。 ###4.5.1 线程的开销 在多核时代,使用多线程可以明显地提高系统的性能。但事实上,使用多线程的方式会额外增加系统的开销。
对于单任务或者单线程的应用而言,其主要资源消耗都花在任务本身。它既不需要维护并行数据结构间的一致性状态,也不需要为线程的切换和调度花费时间;但对于多线程应用来说,系统除了处理功能需求外,还需要额外维护多线程环境的特有信息,如:线程本身的元数据,线程的调度,线程上下文的切换等。
事实上,在单核CPU上,采用并行算法的效率一般要低于原始的串行算法的。其根本原因也在此。因此,并行计算之所以能提高系统的性能,并不是因为它“少干活”了,而是因为并行计算可以更合理的进行任务调度。因此,合理的并发,才能将多核CPU的性能发挥到极致。
###4.5.2 避免死锁 死锁问题是多线程特有的问题,它可以认为是线程间切换消耗系统性能的一种极端情况。在死锁时,线程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是系统任务永远无法执行完成。死锁问题是在多线程开发中,应该坚决避免和杜绝的问题。 ###4.5.3 减小锁持有时间 对于使用锁进行并发控制的应用程序而言,在锁竞争过程中,单个线程对锁的持有时间与系统性能有着直接的关系。如果线程持有锁的时间很长,那么相对地,锁的竞争程度也就越激烈。因此,在程序开发过程中,应该尽可能地减少某个锁的占有时间,以减少线程间互斥的可能。
注意
:减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。 ###4.5.4 减小锁粒度 减小锁粒度也是一种削弱多线程锁竞争的一种有效手段,这种技术典型的使用场景就是ConcurrentHashMap类的实现。对一个普通的集合对象的多线程同步来说,最常使用的方式就是对get()和add()方法进行同步。每当对集合进行add()操作或者get()操作时,总是获得集合对象的锁。因此,事实上没有两个线程可以做到真正的并发,任何线程在执行这些同步方法时,总要等待前一个线程执行完毕。在高并发时,激励的锁竞争会影响系统的吞吐量。
作为JDK并发包中重要的成员ConcurrentHashMap类,很好地使用了拆分锁对象的方式提高ConcurrentHashMap的吞吐量。ConcurrentHashMap将整个HashMap分成若干个段(Segment),每个段都是一个子HashMap。
如果需要在ConcurrentHashMap中增加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashCode得到该表项应该被存放到哪个段中,然后对该段加锁,并完成put()操作。在多线程环境中,如果多个线程同时进行put()操作,只要被加入的表项不存放在同一个字段中,则线程间便可以做到真正的并行。
默认情况下,ConcurrentHashMap拥有16个段,因此,如果够幸运的话,ConcurrentHashMap可以同时接受16个线程同时插入(如果都插入不同的段中),从而大大提高其吞吐量。
但是,减少锁粒度会引入一个新的问题
,即:当系统需要取得全局锁时,其消耗的资源会比较多。仍然以ConcurrentHashMap类为例,虽然其put()方法很好地分离了锁,但是当试图访问ConcurrentHashMap全局信息时,就需要同时取得所有段的锁方能顺利实施。比如ConcurrentHashMap的size()方法,它将返回ConcurrentHashMap的有效表项的数量,即ConcurrentHashMap的全部有效表项之和。要获得这个信息需要取得所有子段的锁,因此,其size()方法的部分代码如下:
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
###4.5.5 读写分离锁来替换独占锁 使用读写分离锁来替代独占锁是减小锁粒度的一种特殊情况。如果说上小节中提到的减小锁粒度是通过分割数据结构实现的,那么读写锁则是对系统功能点的分割。
在读多写少的场合,读写锁对系统性能是很有好处的。因为如果系统在读写数据时均只使用独占锁,那么读写操作间,读读操作间,写写操作间,均不能做到真正的并发,并且需要相互等待。而读操作本身不会影响数据的完整性和一致性,因此,理论上讲,在大部分情况下,应该可以允许多线程同时读。
使用读锁在读读操作时不需要相互等待,读锁之间是相容的,对象可以同时持有多个读锁,因此,可以提升多线程读数据的性能。但是,对象对写锁的占用是独占式的,即,只有在对象没有锁的情况下,才能获得对象的写锁,在写锁释放前,也无法在此对象上附加任何锁。 ###4.5.6 锁分离 读写锁思想的延伸就是锁分离。读写锁根据读写操作功能上的不同,进行了有效的锁分离。依据应用程序的功能特点,使用类似的分离思想,也可以对独占锁进行分离。一个典型的案例就是LinkedBlockingQueue的实现。
在LinkedBlockingQueue的实现中,take()函数和put()函数分别实现了从队列中取得数据和往队列中增加数据的功能。虽然两个函数都对当前队列进行了修改操作,但由于LinkedBlockingQueue是基于链表的,因此,两个操作分别作用于队列的前端和尾端(此处与ArrayBlockingQueue不同
),从理论上讲,两者并不冲突。
如果使用独占锁,则要求在两个操作进行时获取当前队列的独占锁,那么take()和put()操作就不可能真正的并发,在运行时,它们会彼此等待对方释放锁资源。在这种情况下,锁竞争会相对比较激烈,从而影响程序在高并发时的性能。
因此,在JDK的实现中,并没有采用这样的方式,取而代之的是两把不同的锁分离了take()和put操作。
// 与LinkedBlockingQueue,ArrayBlockingQueue只有一个lock,不区分takeLock和putLock,LinkedBlockingQueue的锁更细。
// 原因:LinkedBlockingQueue的take(),put()两个操作分别作用于队列的前端和尾端,而ArrayBlockingQueue确不一定。
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
###4.5.7 重入锁(ReentrantLock)和内部锁(synchronized) 内部锁和重入锁有功能上的重复,所有使用内部锁是实现的功能,使用重入锁都可以实现。从使用上看,内部锁使用简单,因此得到了广泛的使用,重入锁使用略微复杂,必须在finally代码块中,显示释放重入锁,而内部锁可以自动释放。
从性能上看,在高并发量的情况下,内部锁的性能略逊于重入锁,但是,在JDK6中,JVM对内部锁实现了很多优化,并且有理由相信,在将来的JDK版本中,内部锁的性能会越来越好。
从功能上看,重入锁有着更为强大的功能,比如提供了锁等待时间(boolean tryLock(long time, TimeUnit unit)),支持锁中断(LockInterruptibly())和快速锁轮询(tryLock())。这些技术有助于避免死锁的产生,从而提高系统的稳定性。
同时,重入锁还提供了一套Condition机制。通过Condition,重入锁可以进行复杂的线程控制功能,而类似的功能,内部锁需要通过Object的wait()和notify()方法实现。 ###4.5.8 锁粗化(Lock Coarsening) 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早地获得资源执行任务。但是,凡是都有一个度,如果对同一个锁不停地进行请求,同步和释放,其本身也会消耗系统的宝贵资源,反而不利于性能优化。
为此,JVM在遇到一连串连续的对同一个锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这个操作叫作锁的粗化。
public void demoMethod() {
synchronized(lock) {
// do sth
}
// 做其他不需要同步的工作,但能很快执行完毕
synchronized(lock) {
// do sth
}
}
//
public void demoMethod() {
synchronized(lock) {
// do sth
// 做其他不需要同步的工作,但能很快执行完毕
}
}
###4.5.9 自旋锁(Spinning Lock) 在前文中已经提到,线程的状态和上下文切换是要消耗系统资源的。在多线程并发时,频繁的挂起和恢复线程的操作会给系统带来极大的压力。特别是当访问共享资源仅需花费很小一段CPU时间时,锁的等待可能只需要很短的时间,这段时间可能要比将线程挂起并恢复的时间还要短,因此,为了这段时间去做重量级的线程切换是不值得的。
为此,JVM引入了自旋锁。自旋锁可以使线程在没有取得锁时,不被挂起,而转而去执行一个空循环(即所谓的自旋),在若干个空循环后,线程如果获得了锁,则继续执行。若线程依然不能获得锁,才会被挂起。
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,是具有一定的积极意义,但对于锁竞争激烈,锁占用时间长的并发程序,自旋锁在自旋等待后,往往依然无法获得对应的锁,不仅白白浪费了CPU时间,最终还是免不了执行被挂起的操作,反而浪费了系统资源。
JVM虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁的等待次数。
###4.5.10 锁消除(Lock Eliminate) 锁消除是JVM在即时编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。
在Java软件开发过程中,开发人员必然会使用一些JDK的内置API,比如StringBuffer,Vector等。这些常用的工具类可能会被大面地使用。虽然这些工具类本身可能有对应的非同步版本,但是开发人员也很有可能在完全没有多线程竞争的场合使用它们。
在这种情况下,这些工具类内部的同步方法就是不必要的。JVM虚拟机可以在运行时,基于逃逸分析技术,捕获到这些不可能存在竞争却有申请锁的代码段,并消除这些不必要的锁,从而提高系统性能。
逃逸分析和锁消除分别可以使用JVM参数-XX:+DoEscapeAnalysis和-XX:+EliminateLocks开启(锁消除必须工作在-server模式下)。
对锁的请求和释放是要消耗系统资源的。使用锁消除技术可以去掉那些不可能存在多线程访问的锁请求,从而提高系统性能。 ###4.5.11 锁偏向(Biased Lock) 锁偏向是JDK 1.6提出的一种锁优化方式。其核心思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是说,若某一锁被线程获取后,便进入偏向模式。当线程再次请求这个锁时,无需再进行相关的同步操作,从而节省了操作时间。如果在此期间有其他线程进行了锁请求,则锁退出偏向模式。
在JVM中使用-XX:+UseBiasedLocking可以设置启用偏向锁。
偏向锁在锁竞争激烈的场合没有优化效果,因为大量的竞争会导致持有锁的线程不停地切换,锁也很难一直保持在偏向模式,此时,使用锁偏向不仅得不到性能的优化,反而有锁系统性能。因此,在激烈竞争的场合,使用-XX:-UseBiasedLocking参数禁用锁偏向反而能提升系统吞吐量。 ##4.6 无锁的并行计算 为了确保程序和数据的线程安全,使用“锁”是最直观的一种方式。但是,在高并发时,对“锁”的激烈竞争可能会成为系统瓶颈。为此,开发人员可以使用一种称为非阻塞同步的方法
。这种方法不需要使用“锁”(因此称之为“无锁”),但是依然能确保数据和程序在高并发环境下保持多线程间的一致性。 ###4.6.1 非阻塞的同步/无锁 基于锁的同步方式,也是一种阻塞的线程间同步方式,无论是使用信号量,重入锁或者内部锁,都会受到核心资源的限制,不同线程在锁竞争时,总不能避免相互等待,从而阻塞当前线程。为了避免这个问题,非阻塞同步的方式就被提出,最简单的一种非阻塞同步以ThreadLocal为代表,每个线程拥有各自独立的变量副本,因此在并行计算时,无需相互等待。
一种更为重要的,基于比较并交换(Compare And Swap)CAS算法的无锁并发控制方法。
与锁的实现相比,无锁算法的设计和实现都要复杂的多,但由于非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方法要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
CAS算法的过程
:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不等时,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,仅有一个线程会胜出,并成功更新,其他均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,JVM便可以使用这个指令来实现并发操作和并发数据结构。 ###4.6.2 原子操作 在JDK的java.util.concurrent.atomic包下,有一组使用无锁算法实现的原子操作类,主要有AtomicInteger
,AtomicIntegerArray
,AtomicLong
,AtomicLongArray
和AtomicRefrence
等。它们分别封装了对整数,整数数组,长整型,长整型数组和普通对象的多线程安全操作。
以getAndSet()方法为例,看一下CAS算法是如何工作的:
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
在CAS算法中,首先是一个无穷循环,在这里,这个无穷循环用于多线程间的冲突处理,即当前线程受其他线程影响而更新失败时,会不停地尝试,直到成功。在整个过程中,无需加锁,无需等待。无锁的操作实际上将多线程并发的冲突处理由应用层自行解决,这不仅提升了系统性能,还增加了系统的灵活性。但相对地,算法及编码的复杂度也明显地增加了。
java.util.concurrent.atomic
包中的原子类是基于无锁算法实现的,它们的性能要远远优于普通的有锁操作。 ###4.6.3 Amino框架介绍 ###4.6.4 Amino集合 ###4.6.5 Amino树 ###4.6.6 Amino图 ###4.6.7 Amino简单调度模式 ##4.7 协程 ###4.7.1 协程的概念 ###4.7.2 Kilim框架简介 ###4.7.3 Task及其状态 ###4.7.4 Fiber及其状态 ###4.7.5 Kilim开发环境配置 ###4.7.6 Kilim之Hello World ###4.7.7 多任务通信 ###4.7.8 Kilim实例及性能评估