回顾线程与线程池
Posted SSimeng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回顾线程与线程池相关的知识,希望对你有一定的参考价值。
线程、进程、线程池的回顾
1.线程与进程
- 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。 进程在执行过程中拥有独立的内存单元,一个进程可以有多个线程,而多个线程共享内存资源,减少切换次数,效率更高。
- 线程是CPU调度和分配的基本单位,同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
2.线程的状态
- 创建:线程一旦被创建就进入了创建状态。
- 就绪:调用start()方法后,线程进入了就绪状态。
除过start()方法外,还有其他三种方式可以使线程进入就绪状态:
- 本来处于阻塞状态,后来阻塞解除;
- 如果运行的时候调用 yield() 方法,避免一个线程占用资源过多,中断一下,会让线程重新进入就绪状态。注意,如果调用 yield() 方法之后,没有其他等待执行的线程,此线程就会马上恢复执行;
- JVM 本身将本地线程切换到了其他线程,那么这个线程就进入就绪状态。
-
运行:当CPU选定了一个就绪状态的线程执行,这时候线程就进入了运行状态,线程真正开始执行线程体的具体代码块,基本是 run() 方法。一定是从 就绪状态 - > 运行状态,不会从阻塞到运行状态的。
-
阻塞:阻塞状态指的是代码不继续执行,而在等待,阻塞解除后,重新进入就绪状态。
阻塞的方法:
- sleep()方法,是Thread的方法,不释放资源在睡眠的,可以限制等待多久;
- wait() 方法,和 sleep() 的不同之处在于,是不占用资源的,限制等待多久;
- join() 方法,加入、合并或者是插队,这个方法阻塞线程到另一个线程完成以后再继续执行;
- 有些 IO 阻塞,比如 write() 或者 read() ,因为IO方法是通过操作系统调用的。
- 死亡:线程体的代码执行完毕或者中断执行。
死亡方式:
- stop() 和 destroy() 但是都不推荐使用,jdk里也写了已过时。
- 自然死亡:这个线程体里调用执行完了就完了。
- 如果不能自然死亡:加一些终止变量,然后用它作为run的条件,这样,外部调用的时候根据时机,把变量设置为false。
3.线程的创建方式
线程有4种创建方式
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Future创建线程
- 使用线程池例如用Executor框架
- 继承Thread类创建线程的步骤:
- 定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的执行体;
- 创建该类的实例对象,即创建了线程对象;
- 调用线程对象的start()方法来启动线程;
- 实现Runnable接口创建线程
- 定义一个类实现Runnable接口;
- 创建该类的实例对象obj;
- 将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
- 调用线程对象的start()方法启动该线程;
- 使用Callable和Future创建线程
- 创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
- 使用FutureTask对象作为Thread对象的target创建并启动新线程;
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
- 通过线程池创建
通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上
4.start()和run()方法的区别
- start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
- run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。
5.线程池的定义
- 线程池的概念
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;
- 线程池的好处
1:线程和任务分离,提升线程重用性;
2:控制线程并发数量,降低服务器压力,统一管理所有线程;
3:提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;
6.线程池的创建
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public class Test1 {
public static void main(String[] args) {
//1.创建可缓存的线程池,可重复利用
ExecutorService newExecutorService = Executors.newCachedThreadPool();
//创建了10个线程
for (int i = 0; i < 10; i++) {
int temp = i;
newExecutorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("threadName;"+Thread.currentThread().getName()+",i"+temp);
}
});
}
}
}
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public class Test2 {
public static void main(String[] args) {
//1.创建可固定长度的线程池
ExecutorService newExecutorService = Executors.newFixedThreadPool(3);
//创建了10个线程
for (int i = 0; i < 10; i++) {
int temp = i;
newExecutorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("threadName;"+Thread.currentThread().getName()+",i"+temp);
}
});
}
}
}
newScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。
public class Test3 {
public static void main(String[] args) {
//1.创建可定时线程池
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
final int temp = i;
newScheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println("i:" + temp);
}
}, 3, TimeUnit.SECONDS);//表示延迟3秒执行
}
}
}
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public class Test001 {
public static void main(String[] args) {
//1.创建单线程
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
newSingleThreadExecutor.shutdown();
}
}
7.线程池的核心参数
public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize,// 最大线程数
long keepAliveTime, // 最大空闲时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 饱和处理机制
)
- corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。 - maximumPoolSize 线程池最大线程数量
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。 - keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定 - unit 空闲线程存活时间单位
- workQueue 工作队列
- threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等 - handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
8.线程池的工作原理
1)当提交一个新任务到线程池时,线程池判断corePoolSize线程池是否都在执行任务,如果有空闲线程,则从核心线程池中取一个线程来执行任务,直到当前线程数等于corePoolSize;
2)如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
3)如果阻塞队列满了,那就创建新的线程执行当前任务,直到线程池中的线程数达到maxPoolSize,这时再有任务来,由饱和策略来处理提交的任务
9.sleep()、wait()、yield()、join()
- sleep():需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。
- wait():与sleep的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
- yield():和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。
- join():会使当前线程等待调用join()方法的线程结束后才能继续执行。
10.synchronized()和Lock的区别
-
lock是一个接口,是java写的控制锁的代码,而synchronized是java的一个内置关键字,synchronized是托管给JVM执行的;
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。 -
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
-
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
-
Lock可以尝试获取锁,synchronized获取不到锁只能一直阻塞
synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了; -
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、等待时可中断、可判断、可公平可非公平
10.LockSupport
LockSupport是用于创建锁和其他同步类的线程阻塞基本原语。
- 某个线程调用LockSupport.park,如果对应的“许可证”可用,则此次调用立即返回,否则线程将会阻塞直到中断发生、超时或者许可证状态变为可用。
- 线程调用LockSupport.unpark 可以使许可证变得可用。许可证只有一个,不会累积,多次调用unpark没什么用
- 调用一次park
如果count=0,阻塞,等待count 变成1
如果count=1,修改count=0,并且直接运行,整个过程没有阻塞- 调用一次unpark
如果count=0,修改count=1
如果count=1,保持count=1- 多次连续调用unpark 效果等同于一次
所以整个过程即使你多次调用unpark,他的值依然只是等于1,并不会进行累加
以上是关于回顾线程与线程池的主要内容,如果未能解决你的问题,请参考以下文章
newCacheThreadPool()newFixedThreadPool()newScheduledThreadPool()newSingleThreadExecutor()自定义线程池(代码片段