内容提要
Java多线程核心是Runnable接口与Thread类。实现Runnable可避免单继承限制,适合多线程共享同一资源。线程分为用户线程和守护线程,后者随JVM结束。示例涵盖Runnable基本用法、传参、Thread实现并行、抢购场景下的共享资源、线程池ThreadPoolExecutor与Spring ThreadPoolTaskExecutor,以及FutureTask异步获取结果。建议优先实现Runnable而非继承Thread。
延伸解读
守护线程的使用限制
文章指出守护线程随所有用户线程结束而终止,适合后台服务。但并非所有任务都适合守护线程,例如读写操作或计算逻辑,因为守护线程可能在任何时刻被JVM终止,导致任务未完成或数据不一致。设置守护线程必须在start()之前调用setDaemon(true),否则抛出IllegalThreadStateException。
共享Runnable与资源竞争
抢购示例中,两个线程各自使用独立的OrderRunnable实例,导致每个实例的productNum独立,共卖出20个产品。正确做法是多个Thread共享同一个Runnable实例,这样productNum是共享的,输出10个订单。但文章未讨论同步问题,实际中仍需考虑线程安全。
FutureTask的异步获取结果
FutureTask结合Callable可以在未来获取返回值,且不阻塞后续代码。示例中FutureTask启动后立即处理,主线程继续执行其他任务,最后调用get()时若任务未完成才阻塞。注意示例使用run()而非start(),实际中应使用start()启动新线程,否则run()会在当前线程同步执行。
线程池的配置与拒绝策略
文章展示了ThreadPoolExecutor和Spring ThreadPoolTaskExecutor的用法。ThreadPoolExecutor核心参数包括核心线程数、最大线程数、队列容量等。Spring配置中使用了CallerRunsPolicy拒绝策略,当队列满且线程数达最大时,由提交任务的线程执行任务,避免任务丢失,但可能影响提交线程的性能。
Q&A
Java中实现多线程为什么推荐用Runnable接口而不是继承Thread类?
因为Java是单继承,继承Thread类会占用唯一的继承机会,而实现Runnable接口可以避免这个缺陷。同时,Runnable的代码可以被多个线程共享,适合多个线程处理同一资源的情况。
Java中的守护线程是什么?怎么设置?
守护线程运行在后台,为其他前台线程服务。当所有用户线程结束后,守护线程会随JVM一起结束。可以通过调用Thread类的setDaemon(true)方法设置,但必须在start()方法之前调用,否则会抛出IllegalThreadStateException异常。在守护线程中产生的新线程也是守护线程。
如何向Runnable传递参数?
可以定义一个Runnable的子类,在子类中声明成员变量,并通过构造函数传入参数。例如:public MyRunnable(String name, int max) { this.name = name; this.max = max; },然后在run方法中使用这些参数。
多个线程如何共享同一个Runnable对象?
创建同一个Runnable实例,然后用多个Thread实例包装它,并分别调用start()方法。例如:Runnable r1 = new OrderRunnable("A"); new Thread(r1).start(); new Thread(r1).start(); 这样多个线程会共享Runnable中的资源。
ThreadPoolExecutor和Spring的ThreadPoolTaskExecutor有什么区别?
ThreadPoolExecutor是Java原生线程池,通过构造函数直接创建,如new ThreadPoolExecutor(2, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3))。ThreadPoolTaskExecutor是Spring框架提供的线程池,可以通过XML配置或注解注入,配置更灵活,适合Spring项目。
FutureTask有什么作用?如何使用?
FutureTask用于异步获取结果,它使用Callable接口取得返回值,因为结果可能不需要立刻返回。通过Thread的run()或start()调用后马上开始处理,但不阻塞后面的线程。在真正需要结果时调用get()方法,如果处理尚未完成,才会阻塞等待。