上一篇博文学习分享了Thread类的一些源码,本来打算这一周学习分享一下java虚拟机的垃圾回收,但是目前还没有一个好的思路去分享这部分知识,所以,我们暂时先继续学习分享多线程的知识;老规矩,复习一下上篇博文的内容:
①java线程的生命周期
②线程的实现方式
③引起线程等待 WAITTING 的几种方法
然后这篇博文主要包括以下内容点:
①ThreadPoolExecutor类做一个简要的分析。
②类比java.util.concurrent包下的其他“池”。
③浅度的介绍一下happens-before原则。
④站在巨人的肩膀上学习理解ReentrantLock可重入锁。
⑤由重入锁的共享变量可见性探索volatile关键字的线程之间变量可见性的实现机制。
⑥ThreadPoolExecutor中的几种拒绝策略
⑦了解jdk1.8新特性:@FunctionalInterface 函数式接口注解
一、ThreadPoolExecutor
java.util.concurrent包下大致包含了原子性操作类(atomic包)、锁(locks包)、并发容器、几种池。关于线程池相关类,有4种,ForkJoinPool、ScheduledThreadPoolExecutor、ThreadPoolExecutor、Executors类。
ThreadPoolExecutor继承了AbstractExecutorService,AbstractExecutorService实现了ExecutorService接口,ExecutorService接口又继承Executor接口,Executor接口中只定义了一个方法execute(Runnable command)。线程池核心类ThreadPoolExecutor在源码中定义了5种状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。(“温故”,线程Thread类定义了6种状态:NEW、RUNNABLE、BLOCKED、WAITTING、TIMED_WAITTING、TERMINATED)。runState提供了线程池主要的生命周期控制:
①RUNNING:接受新的任务,处理任务队列中的任务。
②SHUTDOWN:不再接受新的任务,继续处理任务队列中的任务。
③STOP:不再接受新的任务,不再处理队列中的任务,打断正在执行的任务。
④TIDYING:所有任务都已经完成,workerCount值为0,线程转化到TIDYING状态。
⑤TERMINATED:terminated()方法执行完成。
几种状态的关系可以数字化的比较,runState随着线程池运行时间的变化而增加,且不必经过每一个状态:
RUNNING --> SHUTDOWN(调用shutdown方法)
RUNNING or SHUTDOWN --> STOP(调用shutdownNow方法)
SHUTDOWN --> TIDYING(当队列和线程池都为空时)
STOP --> TIDYING(当线程池为空时)
TIDYING --> TERMINATED(当terminated方法执行结果时)
对ThreadPoolExecutor状态的分析,其实就是对线程池生命周期的分析。ThreadPoolExecutor类有4种构造器,根据构造器可以大致的了解到ThreadPoolExecutor类的一些主要属性:
①corePoolSize:核心线程池大小,在不允许空闲工作线程等待的情况下,保持存活工作线程的最小数量。当允许空闲线程等待的情况下,核心线程池大小值可以为0。
②maximumPoolSize:线程池允许的最大线程数量。
③keepAliveTime:线程池空闲工作线程,等待任务的最大时间值,如果当前线程的数量大于核心线程池的数量,空闲线程使用它作为其等待任务的最大时间,或者当属性allowCoreThreadTimeOut为true时,使用keepAliveTime,否则空闲线程会一直等待下去。
④allowCoreThreadTimeOut:默认为false,空闲的核心线程永远存活下去,如果为true,核心线程会以keepAliveTime作为一个等待新任务的最大超时时间。
⑤workQueue:任务队列(BlockingQueue<Runable>),用于存放execute方法提交到线程池的任务,使用isEmpty()方法来判断队列是否为空。
⑥threadFactory:线程工厂(ThreadFactory),创建工作线程的工厂。所有工作线程都是通过addWorker方法创建的。
⑦handler:任务拒绝处理器(RejectedExecutionHandler),当线程池饱和或处于shutdown时,处理器会被使用。
在ThreadPoolExecutor中提供了4种任务拒绝策略,分别为CallerRunsPolicy(任务饱和时,直接由调用execute方法的线程去执行任务,当线程池状态为shutdown时,任务被抛弃)、AbortPolicy(抛出异常,RejectedEexcutionException)、DiscardPolicy(不处理,忽略任务)、DiscardOldestPolicy(抛弃队列中最老的任务,即头任务,重试execute方法,当线程池状态为shutdown时,任务被抛弃)。
当通过execute方法提交了一个新的任务时,如果此时正在运行的工作线程数小于核心线程数,即使其他的线程为空闲的,依然会尝试创建一个新的线程,并将提交的任务作为其第一个任务去执行;如果工作线程数大于核心线程数且小于线程池允许最大线程数,当且仅当任务队列填满的时候,创建一个新的线程;如果我们无法添加一个新的任务时,尝试添加一个线程,如果失败,则根据当前线程池所采用的任务拒绝策略,拒绝任务。
//线程池核心方法 public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); } //通过代码可以发现,除了关于线程数量和线程池状态的判断,剩下就是addWorker和reject两个方法了 //顾名思义,任务的执行应该在addWorker方法中 private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); int rs = runStateOf(c); // Check if queue empty only if necessary. if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { int wc = workerCountOf(c); if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } //上半部分代码依旧是对线程池线程数量和状态的相关判断 boolean workerStarted = false; boolean workerAdded = false; //工作线程,实现了Runnable接口 Worker w = null; try { w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { final ReentrantLock mainLock = this.mainLock;//引用线程池的可重入锁 // mainLock.lock(); try { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. int rs = runStateOf(ctl.get()); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable,检查线程状态,Thread.isAlive()检查线程是否为活跃的,即它已经启动,还没有销毁的状态; throw new IllegalThreadStateException(); workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { t.start();//线程启动 workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; } //工作线程 private final class Worker extends AbstractQueuedSynchronizer implements Runnable{ //这里我们只关心它的run方法 public void run() { runWorker(this); } // final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock();//同步锁, // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock();//保证了completedTasks的原子性 } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly);//实现中依旧加锁 //同步线程池变量completedTaskCount,在任务队列中清除任务 //completedTaskCount += w.completedTasks; //workers.remove(w); } } }二、concurrent包下的其他“池”
文章开篇提到了concurrent包下的几种“池”,简要介绍一下ForkJoinPool。ForkJoinPool是java7提供的一个任务并行执行框架,其思想类似于MapReduce思想(Hadoop),就是拆分任务,将任务拆分成多个子任务,如果满足一定条件,子任务可以继续分解出多个子任务,然后将每个子任务计算结果再合并起来。(其实与动态规划算法也有一定的相似之处)要求各个子任务之间相互独立,且能够独立执行任务,互不影响。
ForkJoinPool采用了工作窃取算法,当一个工作线程的任务队列为空,没有任务执行时,便从其他线程中获取任务主动执行。为了实现工作窃取,工作线程中维护了双端队列,窃取任务的线程从队尾获取,被窃取任务的线程从队头获取任务执行,ForkJoin框架同样提供了自己的线程池实现,即ForkJoinPool。提供了三种任务调度的方法:
①execute:无返回结果。
②invoke、invokeAll:异步执行,并等待结果返回。
③submit:异步执行,并立即返回一个Future对象。
ForkJoin框架中实现了自己的任务执行类:
①RecursiveAction:用于无结果返回的子任务
②RecursiveTask:用于有结果返回的子任务
三、ReenTrantLock
关于重入锁,大家可以参考学习java技术栈中周同的博文:http://www.sohu.com/a/209714929_505779
四、happens-before(先行执行原则)
先行发生是Java内存模型中定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法等。
下面是Java内存模型中存在一些“天然的”先行发生关系,这些发生关系无需任何同步器(AQS,AbstractQueuedSynchronized)协助就已经存在,可以在编码中直接使用,如果2个操作之间的关系不在此列,则它们就没有顺序性保障,虚拟机可以对它们随意的重排序。
①程序次序规则(Program Order Rule),在一个线程内,按照程序代码的顺序,书写在前面的操作先行发生于书写在后面的操作。
②管程锁定规则,一个unlock操作先行发生于后面对同一个锁的lock操作,必须是同一个锁。
③volatile变量原则,对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
④线程启动规则,Thread的start()方法先行发生于此线程的每一个动作。
⑤线程终止规则,线程中的多有操作都先行发生于对此线程的终止检查,可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检查到线程已经终止执行。
⑥线程中断原则,对线程的interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
⑦对象终结原则,一个对象的初始化完成先行发生于它的finalize()方法。
⑧传递性,如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
Java语言无需任何同步手段保障就能成立的先行发生规则就只有这些了。需要注意,一个操作“时间上的先发生”不代表这个操作会先行发生,同样,一个操作的“先行发生”也不代表这个操作必定是“时间上的先发生”,关于此部分的学习思考可以参考上面关于ReentrantLock的链接。此处就不再多说了。
五、关于volatile关键字
关于volatile关键字简简单单概括几句吧,都7点多了,留给我的时间不多了,关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量定义为volatile后,它将具备2中特性:
①保证此变量对所有线程的立即可见性。
②禁止指令重排序优化
但是volatile并不能保证变量的原子性,也就是说在一些场景下并不能保证线程安全,线程安全最终还是要回归到锁机制上。
好了,不多说了,我写的都嫌烦了,更不用多能看到最后的人了。最后一句话常“温故”,必“知新”。