基于springboot的多线程程序开发过程中,由于本身也需要注入spring容器进行管理,才能发挥springboot的优势。所以这篇文字主要用来记录开发中两者结合时需要注意的一些事项。
第一步我们把线程类的实例注入sping容器进行管理
@Configuration @SpringBootApplication @Import({ThreadConfig.class}) public class ThreadApp implements CommandLineRunner { public static void main(String[] args) throws Exception { ApplicationContext app = SpringApplication.run(ThreadApp .class, args); //这里主要保存上下文对象实例,需要加上。SpringBootUtils类网上很多,可以自己搜下 SpringBootUtils.setApplicationContext(app); } //access command line arguments @Override public void run(String... args) throws Exception { //do something } } //ComponentScan注解会扫描com.demo.thead下,也就是多线程类所在的包下的文件 @Configuration @ComponentScan(basePackages = { "com.demo.thread"}) public class ThreadConfig{ }
这里使用springboot @Import 注解,把ThreadConfig里扫描到的包中带注解的示例,如@Component等注入到spring容器当中.
然后是线程的启动,这里在我的业务场景中有两种情况:
1、程序运行时,自动启动;
这在一般的可执行程序里面,当然可以直接在main函数里执行通过代码启动线程。但在springboot中,我们可以使用@PostConstruct注解的方式,让已经注入bean容器的线程对象自启动
@Component public class demoThread extends Thread { //注意这里,如果你没有实现把多线程类的实例注入到spring容器中,这里你是无法拿到其他自动装配的对象实例的的,这也是我们第一步的意义所在。 @Autowired private XxxService xxxService; @PostConstruct public void start() { super.start(); } public void run() { // Ok,在这里你就可以实现线程要实现的功能逻辑了,自然也可以直接使用装配好的sevice对象实例。 } }
2、在程序中,需要开启线程时启动,比如在从kafka接收数据,开启线程处理,当然这种情况下也需要通过第一步,把线程类实例注入到sping容器中
private TaskThread thread; private ExecutorService taskPool= new ThreadPoolExecutor( 5, 10, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10), new ThreadPoolExecutor.CallerRunsPolicy()); @KafkaListener(topics = "xxTopic") public void receive(ConsumerRecord<Object, Object> consumerRecord) { JSONObject json = JSON.parseObject(consumerRecord.value().toString()); //通过SpringBootUtils获取线程类的实例 thread = SpringBootUtils.getBean(TaskThread.class); //启动线程 //new Thread(thread).start() ; //向线程对象里传值 thread.init(i); //放入线程池执行 taskPool.execute(thread); }
//注意这里是否添加@Scope("prototype")注解 @Component @Scope("prototype") public class TaskThread implements Runnable{ protected int value=0; @Autowired private XxxService xxxService; //ThreadLocal 对象,单例模式下可以保证成员变量的线程安全和独立性。 public ThreadLocal<Integer> valueLocal = new ThreadLocal < Integer > () { @Override protected Integer initialValue() { return 0; } }; protected static final Logger LOG = LoggerFactory.getLogger(GpsTaskThread.class); @Override public final void run() { try { LOG.info(value+""); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void init(int Value) { this.value=Value; } }
在这里我们需要注意,TaskThread这个线程类在spirngboot中是否要添加@Scope("prototype")注解设置为多例模式还是默认单例模式。
在单例模式下SpringBootUtils.getBean(TaskThread.class) 每次返回的都是同一个对象,虽然不需要每次都创建新的对象,但无法保证成员变量的线程安全,也就是说在线程池中的执行的线程,它们的value值是共享的。而多例模式下,由于每次创建的都是一个新的线程对象,则不存在上述问题。
所以在这里请大家注意无论是我上面的示例代码还是平常的web开发中,spirngboot默认为单例模式,自定义的成员变量是线程不安全的,需要通过ThreadLocal 或这其他方法做同步处理。
回到我们当前的业务场景,在这里我们需要每个线程处理的value值不同,互不影响,那么通过@Scope("prototype")注解把TaskThread设置为多例模式。
总结
通过上面的示例,我们可以看到springboot与多线程的结合还是比较简单,通过配置,我们既可以在spring容器中管理线程类,也可以在线程中使用sping容器中的对象实例。同时我们在使用的过程当中要有意识的去注意线程安全方面的问题和内部运行机制的问题。当然这里理解的还是比较浅显,如果有不正确的地方还请大家指出与海涵。