原文章:多线程+线程池(知识分享)_多线程,线程池详解-CSDN博客
多线程
继承Thread类创建线程
(1)继承Thread类并重写run方法
(2)创建线程对象
(3)调用该线程对象的start()方法来启动线程
代码
public class CreateThreadTest {
public static void main(String[] args) {
new ThreadTest().start();
new ThreadTest().start();
}
}
class ThreadTest extends Thread{
private int i = 0;
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " is running: " + i);
}
}
}
实现Runnable接口创建线程
(1)定义一个类实现Runnable接口,并重写该接口的run()方法
(2)创建 Runnable实现类的对象,作为创建Thread对象的target参数,此Thread对象才是真正的线程对象
(3)调用线程对象的start()方法来启动线程
public class CreateThreadTest {
public static void main(String[] args) {
RunnableTest runnableTest = new RunnableTest();
new Thread(runnableTest, "线程1").start();
new Thread(runnableTest, "线程2").start();
}
}
class RunnableTest implements Runnable{
private int i = 0;
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " is running: " + i);
}
}
}
使用Callable与Future创建线程(实现Callable)
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大:call()方法可以有返回值,可以声明抛出异常。
(1)定义一个类实现Callable接口,并重写call()方法,该call()方法将作为线程执行体,并且有返回值
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象
(3)使用FutureTask对象作为Thread对象的target创建并启动线程
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CreateThreadTest {
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
FutureTask<Integer> futureTask = new FutureTask<>(callableTest);
new Thread(futureTask).start();
try {
System.out.println("子线程的返回值: " + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class CallableTest implements Callable{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i < 101; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + " is running: " + sum);
return sum;
}
}
三种方式实现方式对比
1.实现Runnable/Callable接口相比继承Thread类的优势
(1)适合多个线程进行资源共享
(2)可以避免java中单继承的限制
(3)增加程序的健壮性,代码和数据独立
(4)线程池只能放入Runable或Callable接口实现类,不能直接放入继承Thread的类
2.Callable和Runnable的区别
(1) Callable重写的是call()方法,Runnable重写的方法是run()方法
(2) call()方法执行后可以有返回值,run()方法没有返回值
(3) call()方法可以抛出异常,run()方法不可以
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果 。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果
线程的状态
线程状态 | 解释 |
新建(New) | 当线程对象被创建时,它处于新建状态,还未调用start()方法 |
运行(Runnable) | 线程进入运行状态,一旦调用了 start() 方法后,线程即进入了就绪状态,等待 CPU 分配时间片进入运行状态。 |
阻塞(Blocked) | 线程因为某些原因被暂停执行,进入阻塞状态,比如等待 I/O 操作、等待获取锁、调用 Thread.sleep() 方法,等待监视器锁等。 |
等待(Waiting) | 线程进入等待状态,等待其他线程的通知或中断,等待状态可以由 Object.wait()、Thread.join()、LockSupport.park() 方法进入。 |
计时等待(Timed Waiting) | 类似于等待状态,但是在一定时间后会自动解除阻塞,等待状态可以由 Thread.sleep()、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil() 方法进入。 |
终止(Terminated) | 线程执行完毕或者出现异常时,进入终止状态。 |
线程池
线程池与多线程:
创建和销毁线程:在多线程中,每次需要执行任务时,需要创建一个新的线程。当任务完成后,需要销毁该线程。而在线程池中,线程的创建和销毁都由线程池管理,线程可以被重复利用,从而避免了频繁的创建和销毁线程所带来的开销。
线程数量控制:在多线程中,线程数量由开发人员手动控制,如果线程数量过多,会导致系统资源的浪费,如果线程数量过少,则会导致任务等待时间过长。而线程池可以根据系统的负载情况自动调整线程数量,从而使系统资源的利用更加高效。
任务队列管理:线程池中的任务队列可以用来缓存未执行的任务,当线程池中的线程都在执行任务时,新的任务可以暂时存放在任务队列中,等待线程空闲时再进行执行。而在多线程中,需要自己手动管理任务队列,实现起来比较麻烦。
线程池七大参数
- 核心线程数(corePoolSize):线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁
- 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
-
空闲线程存活时间(keepAliveTime):当线程池中的线程数超过核心线程数时,多余的线程会被回收,此参数即为非核心线程的空闲时间,超过此时间将被回收。
-
存活时间单位(unit):keepAliveTime的计量单位
-
工作队列(workQueue):新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。
ArrayBlockingQueue
有界的阻塞队列,底层是由数组实现的,当队列满时,新元素将无法添加到队列中,直到队列中有空闲位置为止。当队列为空时,获取元素的操作将会阻塞,直到队列中有元素可用。 LinkedBlockingQuene
无界的阻塞队列,底层是由链表实现的,可以存储任意数量的元素。当队列满时,新元素将会一直阻塞等待,直到队列中有空闲位置为止。当队列为空时,获取元素的操作将会阻塞,直到队列中有元素可用。 SynchronousQuene
不保存任何元素,每次插入操作必须等待另一个线程的移除操作,每次移除操作必须等待另一个线程的插入操作,因此它可以用于两个线程之间进行数据交换。 PriorityBlockingQueue
支持优先级的阻塞队列,底层是由堆实现的,可以根据元素的优先级顺序进行排序。当添加元素时,会根据元素的优先级自动排序,获取元素时会返回当前队列中优先级最高的元素。当队列为空时,获取元素的操作将会阻塞,直到队列中有元素可用。 -
线程工厂(threadFactory):用于创建新的线程,可定制线程名字、线程组、优先级等。
-
拒绝策略(handler):当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理
AbortPolicy(默认) | 丢弃任务并抛出 RejectedExecutionException 异常。 |
CallerRunsPolicy | 由调用线程处理该任务。 |
DiscardPolicy | 丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。 |
DiscardOldestPolicy | 丢弃队列最早的未处理任务,然后重新尝试执行任务。 |
线程池状态
多线程注意事项
线程安全:多个线程访问共享数据时需要考虑线程安全问题,如使用同步机制、原子类等。
避免死锁:多个线程相互等待对方释放资源时,可能会导致死锁,需要避免。 线程生命周期的管理:线程应该在不需要时及时销毁,避免资源浪费。
线程优先级的设置:设置线程优先级时,需要注意不要过分依赖优先级带来的性能提升。
多线程的上下文切换开销:在多线程并发执行时,线程的切换会产生一定的开销,需要注意线程的数量和执行时间。
线程池注意事项
线程池大小的选择:线程池的大小应该根据任务类型和系统配置进行调整,避免线程过多或过少。
任务队列的选择:线程池的任务队列应该根据任务类型和负载情况进行选择,如使用有界队列或无界队列。
关闭线程池:线程池在不需要时应该及时关闭,否则可能会产生资源浪费和内存泄漏。
线程池的监控和调试:线程池在运行过程中可能会产生各种问题,如任务堆积、线程死锁等,需要进行监控和调试。