1. 并发的多面性
- 更快地执行
- 改进代码设计
2. 基本的线程机制
-
定义任务
通过实现Runnable接口
-
Tread类(接收任务并启动线程)
当在Java程序中创建一个线程,它就被称为用户线程。
-
使用Executor
Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors框架可以非常方便的创建一个线程池。
Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。
Executors可以用于方便的创建线程池。
-
从任务中产生返回值(定义能够产生返回值的任务)
-
通过实现Callable<V>接口中的call()方法,call()方法的返回值与V相同。
-
由于Callable任务是并行的,我们必须等待它返回的结果。
-
使用ExecutorService中的submit()方法提交Callable任务,submit()会返回Future<T>对象,可以通过Future中的**get()**方法获取返回值。
-
Future
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
Future相当于拿到一个应用,这个引用在线程没有执行完时里面没有内容,但是获取它不报错,因此你可以持有它,一旦有线程执行完后就有数据了。
FutureTask是Future接口的一个唯一实现类。
-
Future对象为我们解决了Callable任务并行的问题。 调用get()方法时,如果实现了Callable接口的线程没有结束则会对当前线程造成堵塞,直至可以获取到返回值为止。可以通过Future中的**isDone()**判断线程有没有结束。
class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { return null; } } public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); Future<Integer> result = exec.submit(new MyCallable()); while (true) { if (result.isDone()){ System.out.println(result.get());//get()会抛出异常 break; } } exec.shutdown(); } }
-
-
休眠
-
TimeUnit.SECONDS.sleep()是对Thread.sleep方法的包装,实现是一样的,只是多了时间单位转换和验证,然而TimeUnit枚举成员的方法却提供更好的可读性。如果忽略性能方面考虑,从可读性方面建议使用TimeUnit枚举成员的sleep方法。
//TimeUnit中的sleep源码 public void sleep(long timeout) throws InterruptedException { if (timeout > 0) { long ms = toMillis(timeout); int ns = excessNanos(timeout, ms); Thread.sleep(ms, ns); } }
TimeUnit也是枚举实现一个很好的实例,Doug Lea太神了,佩服佩服!
-
-
优先级
通过Thread中的setPriority()为线程设置优先级,优先级的级别常用的有三种:Thread.MAX_PRIORITY、Thread.NORM_PRIORITY)、Thread.MIN_PRIORITY;
-
让步
通过静态方法Thread.yield(),发送一个建议给线程调度器(线程调度器可能不会理睬,这取决于具体平台和JVM),告诉它当前线程重要部分已执行完毕,可以执行其他线程了。
class MyTest implements Runnable{ @Override public void run() { while(true){ //重要事务 Thread.yield(); } System.out.println("end task"+id); } }
-
后台进程(守护线程)
通过Thread中的setDaemon(true)方法将线程设置为后台线程;
后台进程是在后台执行并且不会阻止JVM终止的线程。当没有用户线程在运行的时候,JVM关闭程序并且退出。
由后台线程创建的线程默认也是后台线程;
-
ThreadFactory
通过实现ThreadFactory中的newThread()方法在线程执行前做一些通用工作(如优先级,后台进程等),ThreadFactory通常配合ExecutorService一起使用,Executors中的每一个获取ExecutorService的静态方法都重载了一个带ThreadFactory参数的方法。
class MyThreadFactory implements ThreadFactory{ @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); //通用工作如:t.setPriority(); return t; } } public class TestUncaughtException { public static void main(String[] args) throws ExecutionException, InterruptedException, IOException { ExecutorService exec = Executors.newCachedThreadPool( new MyThreadFactory()); ...... } }
-
编码的变体
-
加入一个线程
使用Thread中的join()方法,如果某个线程a在另一个线程t上调用t.join(),a线程将被挂起,直至t线程结束。
-
线程组:一次不成功的尝试,最好忽略它——Joseph Stiglitz
-
捕获异常
线程中逃逸的异常会直接传播到控制台,不能通过常规的方式捕获他。
捕获线程中异常可以通过:(1)实现Thread.UncaughtExceptionHandler接口,并在线程开始前通过Thread中的setUncaughtExceptionHandler()为该线程添加未捕获异常处理器;(2)通过静态方法Thread.**setDefaultUncaughtExceptionHandler()**为所有线程添加异常处理器,这种处理器只有在不存在线程专有的未捕获异常处理器的情况下才会被调用。
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { } } public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException, IOException { Thread.setDefaultUncaughtExceptionHandler( new MyUncaughtExceptionHandler()); Thread t = new Thread(r); t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); } }
3. 共享受限资源
-
不正确的访问资源
-
解决共享资源竞争
-
使用synchronized关键字
当在方法声明中使用synchronized修饰时,所有对象都自动含有单一的锁,因此当调用该对象中被synchronized修饰的方法时,此对象会被加锁。对于某个特定对象来说,其所有synchronized方法共享同一个锁。
-
使用显式的Lock对象
Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:可以使锁更公平;可以使线程在等待锁的时候响应中断;可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间;可以在不同的范围,以不同的顺序获取和释放锁。
-
**对象锁:**对象锁也叫实例锁,对应synchronized关键字,当多个线程访问多个实例时,它们互不干扰,每个对象都拥有自己的锁,如果是单例模式下,那么就是变成和类锁一样的功能。对象锁防止在同一个时刻多个线程访问同一个对象的synchronized块。如果不是同一个对象就没有限制。
private synchronized void method(){ }
-
类锁: 类锁对应的关键字是static sychronized,是一个全局锁,无论多少个对象否共享同一个锁(也可以锁定在该类的class上或者是classloader对象上),同样是保障同一个时刻多个线程同时访问同一个synchronized块,当一个线程在访问时,其他的线程等待。
private static synchronized void method(){ }
-
-
原子性与易变性
-
原子操作是指一个不受其他操作影响的操作任务单元。
-
不要试图使用原子性来替代同步
-
volatile的作用当使用volatile关键字去修饰变量的时候,所有线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。
-
-
原子类
-
原子性变量类:AtomicInteger、AtomicLong
-
Timer计时器类
new Timer().schedule(new TimerTask() { @Override public void run() { } }, 6000);
-
-
临界区(同步控制块)
临界区是为了防止多个线程同时访问方法内部的部分代码而不是整个方法而分离出来的代码段,也被称为同步控制块。
使用synchronized关键字指定某个对象,并使用此对象的锁对花括号内的代码进行同步。
synchronized (syncObject){//对象锁 } synchronized (syncObject.class){//类锁 }
-
在其他对象上同步
synchronized块必须给定一个对象,当对不同的对象使用synchronized块时,各个synchronized块互相不受影响。
-
线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。
Java中创建和管理线程本地存储可以使用TreadLocal类来实现
4. 终结任务
-
线程状态
1)新建(new)
2)就绪(Runnable)
3)阻塞(Blocked)
4)死亡(Dead)
-
中断的原理
Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。
Java中断模型也是这么简单,每个线程对象里都有一个boolean类型的标识(不一定就要是Thread类的字段,实际上也的确不是,这几个方法最终都是通过native方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
-
中断的响应
当在线程上调用interrupt()时,中断发生的唯一时刻是在任务要进入到阻塞状态中,或者已经在阻塞状态中。
程序里发现中断后该怎么响应?这就得视实际情况而定了。有些程序可能一检测到中断就立马将线程终止,有些可能是退出当前执行的任务,继续执行下一个任务……作为一种协作机制,这要与中断方协商好,当调用interrupt会发生些什么都是事先知道的,如做一些事务回滚操作,一些清理工作,一些补偿操作等。若不确定调用某个线程的interrupt后该线程会做出什么样的响应,那就不应当中断该线程。
-
中断的使用
在Java中中断线程的方式有两种:(1)使用Thread中的interrupt()方法;(2)使用Executor中的**shutdownNow()方法或Future中的cancel()**方法。
-
interrupt()
如果有一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将会抛出InterruptedException异常。
当抛出异常或任务调用Thread.interrupted()方法时,中断状态将会复位。
-
shutdownNow()/cancel()
为了使用interrupt()方法,必须先要持有Thread对象。但新的concurrent类库(似乎)避免对Thread的操作。在新的类库中如果想要中断线程,可以调用Executor中的**shutdownNow()**方法,该方法会向它启动的所有线程发送interrupt()消息。
由于**shutdownNow()方法是对所有线程中断,而实际使用当中可能会中断单个线程,这时可以调用submit()方法来启动任务,而不是execute()方法。submit()方法返回一个Future对象,可以调用其上的cancel()**方法来中断任务。使用cancel()方法时需要传递一个true来获得在该线程上发送interrupt()消息的权限。
-
不同的阻塞情况
sleep()方法引起的阻塞可以中断。
I/O和synchronized引起的阻塞不可以被中断。
ReentrantLock上的阻塞任务可以被中断。
-
-
检查中断
检查中断的典型惯用法
public void run() { try { while (!Thread.interrupted()) { ... TimeUnit.SECONDS.sleep(1); ... } } catch (InterruptedException e) { ... } }