多线程之Executor框架
来源:互联网 发布:广东动易软件 编辑:程序博客网 时间:2024/06/05 11:30
Executor框架是指JAVA 5中引入的java.util.concurrent包中的一系列功能类。包括了Executor,Executors,ExecutorService,AbstractExecutorService,Future,Callable,Runnable等。如下图:
一:Executor
Executor作为顶层接口,只提供了一个执行任务的方法。
<span style="font-size:14px;">void execute(Runnable command)</span>
二:ExecutorService
三:ThreadPoolExecutor
四:Executors
Executors是一个工具类,提供了很多方法:创建线程池,创建Callable,创建ThreadFactory等。
Executors的主要方法:
1. public static ExecutorService newFixedThreadPool(int nThreads)
创建一个线程数固定的线程池(ExecutorService)
<span style="font-size:14px;">public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }</span>
2. public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
功能与newFixedThreadPool()相同,区别在于线程有指定的线程工厂类创建
<span style="font-size:14px;">public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }</span>
3. public static ExecutorService newSingleThreadExecutor()
创建只拥有一个线程的线程池
<span style="font-size:14px;">public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }</span>
4. public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
功能与newSingleThreadExecutor()相同,区别在于线程有指定的线程工厂类创建
<span style="font-size:14px;">public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }</span>
5. public static ExecutorService newCachedThreadPool()
创建线程数可动态调整的线程池,线程的空闲时间为60s,超过60s则回收该线程,无上限。
<span style="font-size:14px;">public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }</span>
6. public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
功能与newCachedThreadPool()相同,区别在于线程有指定的线程工厂类创建
<span style="font-size:14px;">public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }</span>
7. public static ScheduledExecutorService newSingleThreadScheduledExecutor()
创建只有一个线程的定时任务线程池,可定时执行任务
<span style="font-size:14px;">public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1)); }</span>
8. public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory)
功能与newSingleThreadScheduledExecutor()相同,区别在于线程有指定的线程工厂类创建
<span style="font-size:14px;">public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1, threadFactory)); }</span>
9. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个指定线程数的定时任务线程池,详细介绍参考章节:多线程之ScheduledExecutorService
<span style="font-size:14px;">public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }</span>
10. public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize, ThreadFactory threadFactory)
功能与newScheduledThreadPool()相同,区别在于线程有指定的线程工厂类创建
<span style="font-size:14px;">public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize, ThreadFactory threadFactory) { return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); }</span>
11. public static ThreadFactory defaultThreadFactory()
返回一个默认的线程工厂类
<span style="font-size:14px;">public static ThreadFactory defaultThreadFactory() { return new DefaultThreadFactory(); }</span>DefaultThreadFactory类源码:
<span style="font-size:14px;">static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }</span>
12. public static <T> Callable<T> callable(Runnable task, T result)
由Runnable接口和执行结果的返回值类型创建一个Callable类返回
13. public static Callable<Object> callable(Runnable task)
有Runnable接口和默认的类型Void创建一个Callable类返回
五:线程池使用
<span style="font-size:14px;">public class Task implements Runnable {private String name;int count = 0;public Task(String name) {this.name = name;}@Overridepublic void run() {while (count < 4) {count++;System.out.println(Thread.currentThread().getName() + "执行任务" + name+ "中。。。");try {Thread.sleep(1000 * 1);} catch (InterruptedException e) {e.printStackTrace();}}}}</span>
线程工厂类
<span style="font-size:14px;">public class WorkThreadFactory implements ThreadFactory{private static AtomicInteger count = new AtomicInteger(0);@Overridepublic Thread newThread(Runnable r) {return new Thread(r, "线程"+ count.addAndGet(1));}}</span>
1. CacheThreadPool使用
主程序:<span style="font-size:14px;">public class MainThread {private static WorkThreadFactory factory = new WorkThreadFactory();//线程工厂private static ExecutorService cacheThreadPool = Executors.newCachedThreadPool(factory);private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2, factory);private static ExecutorService singleExecutor = Executors.newSingleThreadExecutor(factory);private static ScheduledExecutorService sheduleExecutor = Executors.newScheduledThreadPool(2, factory);private static ScheduledExecutorService singleThreadscheduleExecutor = Executors.newSingleThreadScheduledExecutor(factory); public static void main(String[] args) {for(int i=0; i<3; i++){cacheThreadPool.execute(new Task(String.valueOf(i)));}}}</span>
运行结果:
线程1执行任务0中。。。
线程3执行任务2中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程3执行任务2中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程3执行任务2中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程3执行任务2中。。。
2. FixedThreadPool使用
依旧使用上面的程序,只是更换下线程池
<span style="font-size:14px;">public static void main(String[] args) {for(int i=0; i<3; i++){fixedThreadPool.execute(new Task(String.valueOf(i)));}}</span>
运行结果:
线程1执行任务0中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程2执行任务1中。。。
线程1执行任务2中。。。
线程1执行任务2中。。。
线程1执行任务2中。。。
线程1执行任务2中。。。
从运行结果可知,虽然有3个任务要运行,但是只有2个线程在执行这两个任务,因为配置该线程时就配置了该线程池线程的个数为固定2个。
3. SingleExecutor的使用
依旧使用上面的程序,只是更换下线程池
<span style="font-size:14px;">public static void main(String[] args) {for(int i=0; i<3; i++){singleExecutor.execute(new Task(String.valueOf(i)));}}</span>运行结果:
线程1执行任务0中。。。
线程1执行任务0中。。。
线程1执行任务0中。。。
线程1执行任务0中。。。
线程1执行任务1中。。。
线程1执行任务1中。。。
线程1执行任务1中。。。
线程1执行任务1中。。。
线程1执行任务2中。。。
线程1执行任务2中。。。
线程1执行任务2中。。。
线程1执行任务2中。。。
从运行结果可知,只有一个线程在执行所有的任务。
4. ScheduledExecutorService的使用
请参考章节:多线程之ScheduledExecutorService
5. SingleScheduledExecutorService的使用
请参考章节:多线程之ScheduledExecutorService
六:ExecutorService的shutdown()与shutdownNow()
1. 用shutdown()停止
public static void main(String[] args) {for(int i=0; i<3; i++){fixedThreadPool.execute(new Task(String.valueOf(i)));}if(!fixedThreadPool.isShutdown()){System.out.println("++++++++线程池处于运行状态+++++++++++++++++");}fixedThreadPool.shutdown();while(!fixedThreadPool.isShutdown()){}System.out.println("********线程池处于关闭状态****************");while(!fixedThreadPool.isTerminated()){}System.out.println("=========线程池处于终止状态===============");}
********线程池处于关闭状态****************
线程2执行任务1中。。。
线程1执行任务0中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程2执行任务2中。。。
线程2执行任务2中。。。
线程2执行任务2中。。。
线程2执行任务2中。。。
=========线程池处于终止状态===============
2. 用shutdownNow()停止
<span style="font-family:System;">public static void main(String[] args) {for(int i=0; i<3; i++){fixedThreadPool.execute(new Task(String.valueOf(i)));}if(!fixedThreadPool.isShutdown()){System.out.println("++++++++线程池处于运行状态+++++++++++++++++");}fixedThreadPool.shutdownNow();//改换成shutdownNow()System.out.println("********线程池处于关闭状态****************");while(!fixedThreadPool.isTerminated()){}System.out.println("=========线程池处于终止状态===============");}}</span>
运行结果:
********线程池处于关闭状态****************
线程1执行任务0中。。。
线程1执行任务0中。。。
线程2执行任务1中。。。
线程2执行任务1中。。。
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.test.executor.Task.run(Task.java:26)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.test.executor.Task.run(Task.java:26)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
线程1执行任务0中。。。
线程2执行任务1中。。。
线程1执行任务0中。。。
线程2执行任务1中。。。
=========线程池处于终止状态===============
<span style="white-space:pre"></span>public static void main(String[] args) {for(int i=0; i<3; i++){fixedThreadPool.execute(new Task(String.valueOf(i)));}if(!fixedThreadPool.isShutdown()){System.out.println("++++++++线程池处于运行状态+++++++++++++++++");}fixedThreadPool.shutdown();for(int i=0; i<3; i++){//继续添加任务fixedThreadPool.execute(new Task(String.valueOf(i)));}System.out.println("********线程池处于关闭状态****************");while(!fixedThreadPool.isTerminated()){}System.out.println("=========线程池处于终止状态===============");}
运行结果:
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2048)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:821)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1372)
at cn.test.executor.MainThread.main(MainThread.java:36)
4. 用shutdownNow()停止后继续添加任务
七:ExecutorService的invokeAny()与invokeAll()
<span style="font-family:System;font-size:10px;">public class SleepSecondsCallable implements Callable<String>{private String name;private int seconds;public SleepSecondsCallable(String name, int seconds){this.name = name;this.seconds = seconds;}public String call() throws Exception{System.out.println(name + ",begin to execute");try{TimeUnit.SECONDS.sleep(seconds);} catch (InterruptedException e){System.out.println(name + " was disturbed during sleeping.");e.printStackTrace();return name + "_SleepSecondsCallable_failed";}System.out.println(name + ",success to execute");return name + "_SleepSecondsCallable_succes";}}</span><span style="font-size:18px;font-family: FangSong_GB2312;"></span>这是一个通过睡眠来模拟的耗时任务,该任务是可中断/可终止的任务,能够响应中断请求。
<span style="font-family:System;font-size:10px;">public class ExceptionCallable implements Callable<String> {private String name = null;public ExceptionCallable() {}public ExceptionCallable(String name) {this.name = name;}@Overridepublic String call() throws Exception {System.out.println("begin to ExceptionCallable.");System.out.println(name.length());System.out.println("end to ExceptionCallable.");return name;}}</span>这是一个可能会在执行过程中,抛出空指针异常的任务。
<span style="font-family:System;font-size:10px;">public class RandomTenCharsTask implements Callable<String> {@Overridepublic String call() throws Exception {System.out.println("RandomTenCharsTask begin to execute...");StringBuffer content = new StringBuffer();String base = "abcdefghijklmnopqrstuvwxyz0123456789";Random random = new Random();for (int i = 0; i < 10; i++) {int number = random.nextInt(base.length());content.append(base.charAt(number));}System.out.println("RandomTenCharsTask complete.result=" + content);return content.toString();}}</span>这是一个正常的短时的任务,产生10个随机字符组成的字符串。
1.测试invokeAny()
<span style="font-family:System;font-size:10px;">/** * 提交的任务集合,一旦有1个任务正常完成(没有抛出异常),会终止其他未完成的任务 */public static void invokeAny1() throws Exception {ExecutorService executorService = Executors.newFixedThreadPool(3);List<Callable<String>> tasks = new ArrayList<Callable<String>>();tasks.add(new SleepSecondsCallable("t1", 2));tasks.add(new SleepSecondsCallable("t2", 1));String result = executorService.invokeAny(tasks);System.out.println("result=" + result);executorService.shutdown();}</span>
<span style="font-family:System;font-size:10px;">/** * 没有1个正常完成的任务,invokeAny()方法抛出ExecutionException,封装了任务中元素的异常 * */public static void invokeAny2() throws Exception {ExecutorService executorService = Executors.newFixedThreadPool(3);List<Callable<String>> tasks = new ArrayList<Callable<String>>();tasks.add(new ExceptionCallable());tasks.add(new ExceptionCallable());tasks.add(new ExceptionCallable("a"));String result = executorService.invokeAny(tasks);System.out.println("result=" + result);executorService.shutdown();}</span>
第三种情况:先提交3个异常任务,再提交1个正常的耗时任务
<span style="font-family:System;font-size:10px;">/** * 有异常的任务,有正常的任务,invokeAny()不会抛异常,返回最先正常完成的任务 */public static void invokeAny3() throws Exception {ExecutorService executorService = Executors.newFixedThreadPool(3);List<Callable<String>> tasks = new ArrayList<Callable<String>>();tasks.add(new ExceptionCallable());tasks.add(new ExceptionCallable());tasks.add(new ExceptionCallable());tasks.add(new ExceptionCallable());tasks.add(new SleepSecondsCallable("t1", 2));String result = executorService.invokeAny(tasks);System.out.println("result=" + result);executorService.shutdown();}</span>程序执行结果是:不会抛出任何异常,打印出t2任务的返回结果。也就是说:invokeAny()和任务的提交顺序无关,只是返回最早正常执行完成的任务。
<span style="font-family:System;font-size:10px;">/** * 还没有到超时之前,所以的任务都已经异常完成,抛出ExecutionException<br> * 如果超时前满,还没有没有完成的任务,抛TimeoutException */public static void invokeAny4() throws Exception {ExecutorService executorService = Executors.newFixedThreadPool(3);List<Callable<String>> tasks = new ArrayList<Callable<String>>();tasks.add(new ExceptionCallable());tasks.add(new ExceptionCallable());tasks.add(new ExceptionCallable());tasks.add(new ExceptionCallable());String result = executorService.invokeAny(tasks, 2, TimeUnit.SECONDS);System.out.println("result=" + result);executorService.shutdown();}</span>程序执行结果是:抛出ExecutionException。这个其实很合理,也很好理解。如果在超时之前,所有任务已经都是异常终止,那就没有必要在等下去了;如果超时之后,仍然有正在运行或等待运行的任务,那么会抛出TimeoutException。
<span style="font-family:System;font-size:10px;">/** * Executes the given tasks, returning the result * of one that has completed successfully (i.e., without throwing * an exception), if any do. Upon normal or exceptional return, * tasks that have not completed are cancelled. * The results of this method are undefined if the given * collection is modified while this operation is in progress. * * @param tasks the collection of tasks * @return the result returned by one of the tasks * @throws InterruptedException if interrupted while waiting * @throws NullPointerException if tasks or any of its elements * are <tt>null</tt> * @throws IllegalArgumentException if tasks is empty * @throws ExecutionException if no task successfully completes * @throws RejectedExecutionException if tasks cannot be scheduled * for execution */ <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;</span>与我们测试结果一致,invokeAny()返回最先正常完成(without throwing exception)的任务直接结果;一旦有任务正常完成或者调用出现异常,线程池都会终止正在运行或等待运行(tasks that have not completed are cancelled)的任务。
2.测试invokeAll()
这个方法相对来说比较好理解,就是执行任务列表中的所有任务,并返回与每个任务对应的Futue。也就是说,任务彼此之间不会相互影响,可以通过future跟踪每一个任务的执行情况,比如是否被取消,是正常完成,还是异常完成,这主要使用Future类提供的API。<span style="font-family:System;font-size:10px;">public static void invokeAll1() throws Exception {ExecutorService executorService = Executors.newFixedThreadPool(5);List<Callable<String>> tasks = new ArrayList<Callable<String>>();tasks.add(new SleepSecondsCallable("t1", 2));tasks.add(new SleepSecondsCallable("t2", 2));tasks.add(new RandomTenCharsTask());tasks.add(new ExceptionCallable());// 调用该方法的线程会阻塞,直到tasks全部执行完成(正常完成/异常退出)List<Future<String>> results = executorService.invokeAll(tasks);// 任务列表中所有任务执行完毕,才能执行该语句System.out.println("wait for the result." + results.size());executorService.shutdown();for (Future<String> f : results) {// isCanceled=false,isDone=trueSystem.out.println("isCanceled=" + f.isCancelled() + ",isDone="+ f.isDone());// ExceptionCallable任务会报ExecutionExceptionSystem.out.println("task result=" + f.get());}}</span>程序的执行结果和一些结论,已经直接写在代码注释里面了。invokeAll是一个阻塞方法,会等待任务列表中的所有任务都执行完成。不管任务是正常完成,还是异常终止,Future.isDone()始终返回true。通过Future.isCanceled()可以判断任务是否在执行的过程中被取消。通过Future.get()可以获取任务的返回结果,或者是任务在执行中抛出的异常。
第二种情况,测试限时版本的invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
<span style="font-family:System;font-size:10px;">/** * 可以通过Future.isCanceled()判断任务是被取消,还是完成(正常/异常)<br> * Future.isDone()总是返回true,对于invokeAll()的调用者来说,没有啥用 */public static void invokeAll2() throws Exception {ExecutorService executorService = Executors.newFixedThreadPool(5);List<Callable<String>> tasks = new ArrayList<Callable<String>>();tasks.add(new SleepSecondsCallable("t1", 2));tasks.add(new SleepSecondsCallable("t2", 2));tasks.add(new SleepSecondsCallable("t3", 3));tasks.add(new RandomTenCharsTask());List<Future<String>> results = executorService.invokeAll(tasks, 1, TimeUnit.SECONDS);System.out.println("wait for the result." + results.size());for (Future<String> f : results) {System.out.println("isCanceled=" + f.isCancelled() + ",isDone=" + f.isDone());System.out.println("task result=" + f.get());}executorService.shutdown();}</span>执行结果是:
wait for the result.4
isCanceled=true,isDone=true
isCanceled=true,isDone=true
isCanceled=true,isDone=true
isCanceled=false,isDone=true
也就是说给定的超时期满,还没有完成的任务会被取消,即Future.isCancelled()返回true;在超时期之前,无论是正常完成还是异常终止的任务,Future.isCancelled()返回false。
第三种情况,测试在等待invokeAll执行完成之前,线程被中断。
<span style="font-family:System;font-size:10px;">/** * 如果线程在等待invokeAll()执行完成的时候,被中断,会抛出InterruptedException<br> * 此时线程池会终止没有完成的任务,这主要是为了减少资源的浪费. */public static void testInvokeAllWhenInterrupt() throws Exception {final ExecutorService executorService = Executors.newFixedThreadPool(5);// 调用invokeAll的线程Thread invokeAllThread = new Thread() {@Overridepublic void run() {List<Callable<String>> tasks = new ArrayList<Callable<String>>();tasks.add(new SleepSecondsCallable("t1", 2));tasks.add(new SleepSecondsCallable("t2", 2));tasks.add(new RandomTenCharsTask());// 调用线程会阻塞,直到tasks全部执行完成(正常完成/异常退出)try {List<Future<String>> results = executorService.invokeAll(tasks);System.out.println("wait for the result." + results.size());} catch (InterruptedException e) {System.out.println("I was wait,but my thread was interrupted.");e.printStackTrace();}}};invokeAllThread.start();Thread.sleep(200);invokeAllThread.interrupt();executorService.shutdown();}</span>invokeAllThread 线程调用了ExecutorService.invokeAll(),在等待任务执行完成的时候,invokeAllThread被别的线程中断了。这个时候,ExecutorService.invokeAll()会抛出java.lang.InterruptedException,任务t1和t2都被终止抛出java.lang.InterruptedException: sleep interrupted。也就是说一旦ExecutorService.invokeAll()方法产生了异常,线程池中还没有完成的任务会被取消执行。
- 多线程之Executor框架
- Java多线程之Executor框架
- Java多线程之Executor框架(1)
- Java多线程之Executor框架(2)
- 多线程进阶002 之 Executor框架
- java多线程---Executor框架
- Executor多线程框架
- Executor多线程框架
- 多线程执行框架Executor详解
- Java多线程—Executor框架
- 多线程--Executor框架
- Java多线程-Executor框架:ScheduledThreadPoolExecutor
- Java多线程-Executor框架:CompletionService
- 【多线程】——Executor框架
- Java多线程-Executor框架:CompletionService
- Java-多线程框架Executor解读
- Java多线程-实现多线程:Executor框架
- JAVA多线程之Executor&ExecutorService
- Oracle--触发器
- java操作excel
- 寒假自主学习项目一 - 链表(2、查找)
- C++读写txt文件
- eclipse 最佳配置
- 多线程之Executor框架
- bzoj 1079 题解
- watchify和browserify
- Android开发中在一个Activity中关闭另一个Activity
- python xlrd 使用
- Web App的零框架解决方案
- 好记性不如烂笔头26-JAVA处理文件事务(4)
- const的用法,特别是用在函数前面与后面的区别!
- webstorm、phpstorm、idea等使用技巧记录