一、并发概念
1、并行和并发的区别
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并发一般解决两种问题,“速度”和“设计可管理性”。这部分事件要么看起来在并发的执行,要么在多处理环境下可以同时执行。
二、基本的线程机制
2.1 定义任务
1、实现接口Runnable接口并编写run()方法,使用方法比较简单。该任务五返回值。
public class LiftOff implements Runnable {
protected int countDown = 1000;
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff() {
System.out.println("construcate:" + Thread.currentThread().toString());
}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public void run() {
System.out.println("run:" + Thread.currentThread().toString());
}
}
2、实现Callable接口并编写call()方法。
2.1 Callable是个泛型参数化接口,它的类型参数表示的是从方法call()中返回的值。Callable能返回线程的执行结果,且能在无法正常计算时抛出异常。并且必须使用ExecutorService.submit()方法调用它,而不能使用Thread.start()方法。
ExecutorService pool = Executors.newCachedThreadPool();
Future<String> future = pool.submit(new Callable{
public void call(){
//TODO
}
});
submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。
2.2 或者利用FutureTask封装Callable再由Thread去启动(少用)
FutureTask<String> task = new FutureTask(new Callable{
public void call(){
//TODO
}
});
Thead thread = new Thread(task);
thread.start();
2.3 通过Executors.callbale(Runnable task,T result)可以执行Runnable并返回"结果",但是这个结果并不是Runnable的执行结果(Runnable的run方法是void类型),而是执行者预定义的结果,这点可以从其实现原理RunnableAdpter源码看出
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);//通过RunnableAdapter实现
}
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result; //将传入的结果的直接返回
}
}
Runnable与Callable不同点:
1. Runnable不返回任务执行结果,Callable可返回任务执行结果
2. Callable在任务无法计算结果时抛出异常,而Runnable不能
3. Runnable任务可直接由Thread的start方法或ExecutorService的submit方法去执行
2.4 Future
Future保存异步计算的结果,可以在我们执行任务时去做其他工作,并提供了以下几个方法
* cancel(boolean mayInterruptIfRunning):试图取消执行的任务,参数为true时直接中断正在执行的任务,否则直到当前任务执行完成,成功取消后返回true,否则返回false
* isCancel():判断任务是否在正常执行完前被取消的,如果是则返回true
* isDone():判断任务是否已完成
* get():等待计算结果的返回,如果计算被取消了则抛出
* get(long timeout,TimeUtil unit):设定计算结果的返回时间,如果在规定时间内没有返回计算结果则抛出TimeOutException
使用Future的好处:
1. 获取任务的结果,判断任务是否完成,中断任务
1. Future的get方法很好的替代的了Thread.join或Thread,join(long millis)
2. Future的get方法可以判断程序代码(任务)的执行是否超时,如:
* cancel(boolean mayInterruptIfRunning):试图取消执行的任务,参数为true时直接中断正在执行的任务,否则直到当前任务执行完成,成功取消后返回true,否则返回false
* isCancel():判断任务是否在正常执行完前被取消的,如果是则返回true
* isDone():判断任务是否已完成
* get():等待计算结果的返回,如果计算被取消了则抛出
* get(long timeout,TimeUtil unit):设定计算结果的返回时间,如果在规定时间内没有返回计算结果则抛出TimeOutException
使用Future的好处:
1. 获取任务的结果,判断任务是否完成,中断任务
1. Future的get方法很好的替代的了Thread.join或Thread,join(long millis)
2. Future的get方法可以判断程序代码(任务)的执行是否超时,如:
try{
future.get(60,TimeUtil.SECOND);
}catch(TimeoutException timeout){
log4j.log("任务越野,将被取消!!");
future.cancel();
}
2.2执行任务
1、Thread类
public class BaseThreads {
public static void main(String[] args) {
final Thread t = new Thread(new LiftOff());
t.start();
System.out.println("waiting for liftOff");
}
}
2、使用Executor
java SE5的java.util.concurrent包中的执行器(Executor)将自动管理Thread对象,从而简化了并发编程,推荐使用Executor来执行任务。
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(new Runnable() {
public void run() {
//TODO
}
});
executor.shutdown();
}
shutdown()方法的调用可以防止新任务被提交给这个Executor,当前线程(在本例中,即驱动main的线程)将继续运行在shutdown()被调用之前提交的所有任务,这个程序将在Executor中的所有任务完成之后尽快退出。
Executor service的类型由多种,java.util.concurrent.Executors类包含了静态工厂,可以提供所需要的大多数executor。然而如果你想来电特别的,可以直接使用ThreadPoolExecutor类。这个类允许你控制线程池操作的几乎每个方面。
CachedThreadPool,通常是个不错的选择,因为它不需要配置,并且一般情况下能够正确的工作,但是对于大负载的服务器来说,缓存的线程池就不是很好的选择。cachedThreadPool如果没有线程可用,就会新建一个线程,如果服务器负载太重以致他所有的cpu都完全被占用了,当更多的任务时,就会创建更多的线程,这样只会使情况变的更糟。所以这种情况下我们可以使用Executors.newFixedThreadPool。它为你提供一个保安固定线程数目的线程池,或者为了极大限度的控制它,可以使用ThreadPoolExecutor类。
SigleThreadExecutor就像是线程数为1的FixedThreadPool。SigleThreadExecutor和单线程的选择,以及线程池和单线程的选择要视情况而定。如下是线程池和单线程的优缺点。
线程池管理的线程的几点意义:
1、缓存线程、进行池化,可实现线程重复利用、避免重复创建和销毁所带来的性能开销。
2、当线程调度任务出现异常时,会重新创建一个线程替代掉发生异常的线程。
3、任务执行按照规定的调度规则执行。线程池通过队列形式来接收任务。再通过空闲线程来逐一取出进行任务调度。即线程池可以控制任务调度的执行顺序。
4、可制定拒绝策略。即任务队列已满时,后来任务的拒绝处理规则。
1、缓存线程、进行池化,可实现线程重复利用、避免重复创建和销毁所带来的性能开销。
2、当线程调度任务出现异常时,会重新创建一个线程替代掉发生异常的线程。
3、任务执行按照规定的调度规则执行。线程池通过队列形式来接收任务。再通过空闲线程来逐一取出进行任务调度。即线程池可以控制任务调度的执行顺序。
4、可制定拒绝策略。即任务队列已满时,后来任务的拒绝处理规则。
new Thread方式存在以下不足(线程池的优点):
1、功能单一:run方法执行任务是确定的,不能动态处理多任务。
2、无法重用:由于第1点提到的功能单一的特点。当出现动态任务时,每run方法执行结束后,线程也就结束(线程状态变为TERMINATED,线程状态可查看Thread.State这个类),结束状态的线程不能再被重新开启使用。只能占着内存。或者等待被销毁。
2、耗时较多(特定场景下):在要求重复接收外界的任务,并异步执行一些任务的场景下,需要重复创建线程来执行(因为结束状态的线程不可重用),另外如果旧的线程没被继续引用,会被销毁并回收。此场景下的耗时原因主要线程创建销毁操作过于频繁。
3、安全性低一些:线程执行过程发生异常时,线程会直接终止,变为结束状态。
但对于满足以下场景可以直接用new Thread的方式。而不需要采用ThreadPool
1、线程执行任务内容已确定:如当前线程的任务就是每隔
2、线程不需要频繁创建和销毁:如线程启动后就一直运行。
1、功能单一:run方法执行任务是确定的,不能动态处理多任务。
2、无法重用:由于第1点提到的功能单一的特点。当出现动态任务时,每run方法执行结束后,线程也就结束(线程状态变为TERMINATED,线程状态可查看Thread.State这个类),结束状态的线程不能再被重新开启使用。只能占着内存。或者等待被销毁。
2、耗时较多(特定场景下):在要求重复接收外界的任务,并异步执行一些任务的场景下,需要重复创建线程来执行(因为结束状态的线程不可重用),另外如果旧的线程没被继续引用,会被销毁并回收。此场景下的耗时原因主要线程创建销毁操作过于频繁。
3、安全性低一些:线程执行过程发生异常时,线程会直接终止,变为结束状态。
但对于满足以下场景可以直接用new Thread的方式。而不需要采用ThreadPool
1、线程执行任务内容已确定:如当前线程的任务就是每隔
2、线程不需要频繁创建和销毁:如线程启动后就一直运行。
3、内部实现比较简单,不需要线程池的管理功能(队列,异常处理等),可以用new Thread()。
2.3 线程其他机制
1、线程休眠
sleep()的调用可以抛出InterruptedException异常,它在run()中必须被捕获,因为异常不能跨线程传播回main()。
TimeUnit.MILLISECONDS.sleep(100);
Thread.sleep(100);
2、线程优先级
Thread.currentThread().setPriority(5);//设置优先级
3、线程让步
Thread.yield()。给cpu建议其他线程可以运行。
4、后台线程
所谓后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有非后台线程结束时,程序也就红纸了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
4.1 创建一个后台线程
Thread daemon = new Thread(new Runable(){ public void run(){//TODO}});
daemon.setDaemon(true);//must call before start()
daemon.start();
4.2 判断是否后台线程
daemon.isDaemon()方法来确定线程是否是一个后台线程,如果是一个后台线程,那么它创建的任何线程将自动设置成后台线程。
5、加入一个线程
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
Thread的join方法理解:http://uule.iteye.com/blog/1101994
6、捕获异常
run()中未捕获的异常该如何处理。
/**
*/
package com.XXX.Concurrency;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* 捕获异常
*/
public class ExceptionThread implements Runnable {
/**
* @see java.lang.Runnable#run()
*/
public void run() {
throw new RuntimeException();
}
/**
* version1 结果:抛异常
*Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at com.XXX.Concurrency.ExceptionThread.run(ExceptionThread.java:21)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)
* 解决方案
* 我们可以修改产生线程的方式。Thread.UncaughtExceptionHandler是java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器。
* hread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用。为了使用它,我们创建了一个新类型的ThreadFactory,
* 它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler。我们将这个工厂传递给Executors创建新的ExecutorService的方法
* version2 结果:未抛异常
* 再进一步,如果在代码中处处要使用相同的异常处理器,则可将其设置为线程的默认处理器。
* @param args
*/
public static void main(String[] args) {
// version1
// final ExecutorService exec = Executors.newCachedThreadPool(); //version1 未处理线程中抛出的异常
// exec.execute(new ExceptionThread());
// exec.shutdown();
// version2
// final ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());//version2 处理了线程中抛出的异常
// exec.execute(new ExceptionThread2());
// exec.shutdown();
//Version3
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
final ExecutorService exec = Executors.newCachedThreadPool();//version3 处理了线程中抛出的异常
exec.execute(new ExceptionThread2());
exec.shutdown();
}
}
class ExceptionThread2 implements Runnable {
/**
* @see java.lang.Runnable#run()
*/
public void run() {
final Thread t = Thread.currentThread();
System.out.println("run by " + t);
System.out.println("eh= " + t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
/**
* @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread, java.lang.Throwable)
*/
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
class HandlerThreadFactory implements ThreadFactory {
/**
* @see edu.emory.mathcs.backport.java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable)
*/
public Thread newThread(Runnable r) {
System.out.println(this + " creating new Thread");
final Thread t = new Thread(r);
System.out.println(this + " created " + t);
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
三、共享受限资源
3.1 解决共享资源竞争
基本上所有的并发模式在解决线程冲突问题的时候,都是采取序列化访问共享资源的方案,这意味着在给定时刻只允许一个任务访问共享资源。通常是在代码签加上锁语句,这就是的在一段时间内只有一个任务可以运行这段代码,因为锁语句产生了一种互相排斥的效果,所以这种机制常称为互斥量。
共享资源一般是以对象形式存在的内存片段,但也可以是文件、输入/输出端口,或者是打印机。要控制对共享资源的访问,得先把它包装进一个对象,然后把所有要访问这个资源的方法标记为synchronized。
重要的几点概念:
1、所有对象自动含有单一的锁。当在对象上调用其任意synchronized方法的时候,此对象都被加锁。对于某个特定对象来说,其所有synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象内存。
2、一个任务可以多次获得对象的锁。
3、针对每个类,也有一个锁,所以synchronized static方法可以在类的范围内防止对Static数据的并发访问。
4、Brian同步规则:如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的件事情锁同步。
5、每个访问临界共享资源的方法都必须被同步,否则它们就不会正确地工作。
并发的处理包含两个概念:互斥和同步。
互斥即当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。而如果没有同步,一个线程的变化就不能被其他线程看到。同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
java语言规范保证度或者写一个变量(除过long和double)是原子的,即使是这样,也不能说为了提高性能,在读或者写原子数据的时候可以避免使用同步,虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但它并不保证一个线程写入的值对另一个线程将是可见的。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。这归因于java语言规范中的内存模型,它规定了一个线程所做的变化何时以及如何变成对其他线程可见。这种情况可以使用volatile,虽然volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候将看到最近刚刚被写入的值。
关键字synchronized 和显示的Lock对象
synchronized和ReetrantLock的区别:http://www.cnblogs.com/benshan/p/3551987.html
3.2 原子操作和原子类
java语言规范保证度或者写一个变量(除过long和double)是原子的,即使是这样,也不能说为了提高性能,在读或者写原子数据的时候可以避免使用同步,虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但它并不保证一个线程写入的值对另一个线程将是可见的。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。这归因于java语言规范中的内存模型,它规定了一个线程所做的变化何时以及如何变成对其他线程可见。这种情况可以使用volatile,虽然volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候将看到最近刚刚被写入的值。
java SE5引入了诸如AtomicInteger,AtomicLong,AtomicReference等特殊的原子性变量类
3.3 临界区
有时,你只是希望防止多个线程同时访问方法内部的部分代码而不是访问整个方法。通过这种方式分离出来的代码段被称为临界区,使用synchronized关键字建立。
3.4 线程本地存储
防止任务在共享资源上产生冲突的第二个方式是根除对变量的共享,线程本地化存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。
创建和管理本地存储可以有java.lang.ThreadLoacl类来实现。
四、终结任务
在某些情况下,任务必须突然的终止,这是一种合理的需求。
1、线程的状态
新建(new):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必须的系统资源,并执行了初始化。此刻线程已经有资格获得cpu时间了,之后调度器将把这个线程转变为可运行状态或阻塞状态。
就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以下运行也可以不允许。只要调度器能分配时间片给线程,它就可以运行,这不同于死亡和阻塞状态。
阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才有可能执行操作。
死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。
进入阻塞状态,一个任务进入阻塞状态,可能有如下原因:
1) 通过调用sleep(milliseconds)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
2) 通过调用wait()使线程挂起。直到线程得到了notify()或notifyAll()消息(或者signal或signalAll消息),线程才会进入就绪状态。
3) 任务在等待某个输入/输出完成。
4) 任务视图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。
2、中断
执行中断动作:
1) 任务已经处于阻塞或者任务要进入到阻塞操作中时,执行interrupt()。
Executor调用shutDown(),将会发送一个interrupt()给它启动的所有线程。如果要中断其中的某一个线程,则可通过调用submit()而不是executor()来启动任务,这样就可以持有该任务的上下文。submit()将返回一个Future对象,在该future上调用cancel(),则只会在该线程上调用interrupt()以听着这个线程的权限。因此,cancel()是一种中断由Executor启动的单个线程的方式。
interrupt()不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
如果线程没有被阻塞,这时调用interrupt()将不起作用;否则,线程就将得到异常(该线程必须事先预备好处理此状况),接着逃离阻塞状态。
2) 如何在run()循环碰巧没有产生任何阻塞调用的情况下,我们需要第二种方式来退出。
这种方式是通过中断状态来表示的,其状态可以通过调用interrupt()来设置。你可以通过interrupted()来检查中断状态。我们自己在程序中根据中断状态来判断是否退出run(),而不能依赖程序阻塞是抛出中断异常来退出。
从上可知,阻塞有多种,sleepBlock是可中断的阻塞,而IOBlocked和SynchronizedBlocked是不可中断的阻塞。
对于可中断的阻塞可以使用上面所讲的执行中断的动作方式,但是对于IOBlocked这种不可中断的异常,我们可以通过关闭底层资源以释放锁。而对于SynchronizedBlocked是不可中断的,但是在ReentrantLock上阻塞的任务具备可以被中断的能力,这与在synchronized方法或临界区上阻塞的任务完全不同。