1 前言
根据 Quartz 的设计,一个 Job 可以绑定多个 Trigger,必然会遇到并发的问题。
2 并发
2.1 复现
让我们编写一个并发的例子:
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 public class AcceptConcurrentDemo { 6 7 public static void main(String[] args) throws SchedulerException, InterruptedException { 8 JobDetail detail = JobBuilder.newJob(AcceptConcurrentJob.class) 9 .withIdentity("detail", "group0") 10 .build(); 11 12 13 Trigger trigger = TriggerBuilder.newTrigger() 14 .withIdentity("ben_trigger") 15 .usingJobData("name", "ben") 16 .startNow() 17 .build(); 18 19 Trigger triggers = TriggerBuilder.newTrigger() 20 .withIdentity("mike_trigger") 21 .usingJobData("name", "mike") 22 .forJob("detail", "group0") 23 .startNow() 24 .build(); 25 26 27 Scheduler scheduler = new StdSchedulerFactory().getScheduler(); 28 29 scheduler.start(); 30 scheduler.scheduleJob(detail, trigger); 31 scheduler.scheduleJob(triggers); 32 /* 33 * 6 秒钟后关闭 34 */ 35 Thread.sleep(6_000); 36 scheduler.shutdown(); 37 } 38 39 @Data 40 public static class AcceptConcurrentJob implements Job { 41 private String name; 42 43 @Override 44 public void execute(JobExecutionContext context) { 45 try { 46 System.out.printf("i am %s \n", name); 47 Thread.sleep(2_000); 48 } catch (InterruptedException e) { 49 e.printStackTrace(); 50 } 51 } 52 } 53 }
请注意上边的 Details 的 Identity ,设置为 group0.detail,同时我们创建了两个 Trigger,第二个 trigger 在创建的时候通过指定 Identity 绑定到了目标 Job,接着提交这个 Job,与两个 Trigger ,可以看到两个触发器同时出发了 Job 的 execute 方法
上边的代码也可以简化为以下形式:
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 public class AcceptConcurrentDemo { 6 7 public static void main(String[] args) throws SchedulerException, InterruptedException { 8 JobDetail detail = JobBuilder.newJob(AcceptConcurrentJob.class) 9 .withIdentity("detail", "group0") 10 .build(); 11 12 13 Trigger trigger = TriggerBuilder.newTrigger() 14 .withIdentity("ben_trigger") 15 .usingJobData("name", "ben") 16 .startNow() 17 .build(); 18 19 Trigger triggers = TriggerBuilder.newTrigger() 20 .withIdentity("mike_trigger") 21 .usingJobData("name", "mike") 22 .startNow() 23 .build(); 24 25 26 Scheduler scheduler = new StdSchedulerFactory().getScheduler(); 27 28 scheduler.start(); 29 scheduler.scheduleJob(detail, Sets.newHashSet(trigger,triggers),true); 30 /* 31 * 6 秒钟后关闭 32 */ 33 Thread.sleep(6_000); 34 scheduler.shutdown(); 35 } 36 37 @Data 38 public static class AcceptConcurrentJob implements Job { 39 private String name; 40 41 @Override 42 public void execute(JobExecutionContext context) { 43 try { 44 System.out.printf("i am %s \n", name); 45 Thread.sleep(2_000); 46 } catch (InterruptedException e) { 47 e.printStackTrace(); 48 } 49 } 50 } 51 }
2.2 避免并发
为了避免并发,我们可以使用官方提供的注解 @DisallowConcurrentExecution,通过在 类上增加这个注解,我们可以观察到第二个 trigger 进行了排队处理:
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 public class RejectConcurrentDemo { 6 7 public static void main(String[] args) throws SchedulerException, InterruptedException { 8 JobDetail detail = JobBuilder.newJob(RejectConcurrentJob.class) 9 .withIdentity("detail", "group0") 10 .build(); 11 12 13 Trigger trigger = TriggerBuilder.newTrigger() 14 .withIdentity("ben_trigger") 15 .usingJobData("name", "ben") 16 .startNow() 17 .build(); 18 19 Trigger triggers = TriggerBuilder.newTrigger() 20 .withIdentity("mike_trigger") 21 .usingJobData("name", "mike") 22 .forJob("detail", "group0") 23 .startNow() 24 .build(); 25 26 27 Scheduler scheduler = new StdSchedulerFactory().getScheduler(); 28 29 scheduler.start(); 30 scheduler.scheduleJob(detail, trigger); 31 scheduler.scheduleJob(triggers); 32 /* 33 * 6 秒钟后关闭 34 */ 35 Thread.sleep(6_000); 36 scheduler.shutdown(); 37 } 38 39 40 @DisallowConcurrentExecution 41 @Data 42 public static class RejectConcurrentJob implements Job { 43 private String name; 44 45 @Override 46 public void execute(JobExecutionContext context) { 47 try { 48 System.out.printf("i am %s \n", name); 49 Thread.sleep(2_000); 50 } catch (InterruptedException e) { 51 e.printStackTrace(); 52 } 53 } 54 } 55 }
3 避免并发的原理探索
让我们找到 JobStore 的实现类,在这里是 RAMJobStore,点进去方法 org.quartz.simpl.RAMJobStore#acquireNextTriggers,可以看到这个方法的某个块:
通过对 Job 类上的是否存在 DisallowConcurrentExecution 注解,如果存在,表示拒绝并发执行 execute 方法。如果与即将执行的 Trigger 调用同一个 JobDetail 对象,则将当前 trigger 放入等待列表。
之后当前一个 Trigger 执行完毕,将等待中的 Trigger 重新加回去 RAMJobStore 持有的 trigger 列表中,等待下一次调用发生。