一、线程
1.线程的创建方式
有4 种方式可以用来创建线程:
- 1.继承Thread 类
- 2.实现Runnable 接口
- 3.应用程序可以使用Executor 框架来创建线程池
- 实现Callable 接口
实现Runnable 接口比继承Thread 类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java 中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable 或callable 类线程,不能直接放入继承Thread 的类
5)runnable 实现线程可以对线程进行复用,因为runnable 是轻量级的对象,重复new 不会耗费太大资源,而Thread 则不然,它是重量级对象,而且线程执行完就完了,无法再次利用
- 实现Callable 接口
2.线程的生命周期
![]() |
---|
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。
该状态的线程位于可运行线程池中,变得可运行,等待获取CPU 的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU 使用权,暂时停止
运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM 会把该线程放入等待池中。(wait
会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,
则JVM 会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O 请求时,JVM
会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O 处
理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命
周期。
3.终止线程的方法
- 正常结束
- 使用退出标识
- interrupt
- stop
参考:https://blog.csdn.net/lyabc123456/article/details/81010995
4.线程的基本方法:
![]() |
---|
线程的基本方法有:
-
sleep():
-
wait():
-
notify():
-
notifyAll():
-
join():加入线程(当前执行的线程是A线程,调用join()方法得是B线程) 当前线程阻塞 执行B线程 B执行结束之后A线程才能执行
放弃当前线程的执行 -
yield():暂停当前方法,释放自己拥有的CPU,线程进入就绪状态。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
yield也不会释放同步锁 -
interrupt():
interrupt() 中断线程 由运行状态到死亡状态
1.中断线程操作实质上是修改了一下中断标示位为true
2.当前线程正在运行,仅仅修改标示位,不在做其他的事
3.当前线程正在阻塞,修改标识位,如果是join,sleep,yield,则会抛出Interrup异常,修改标示位为false -
getName():获取线程名
-
setName():设置线程名
setDaemon():
setPriority():
参考:https://blog.csdn.net/qq_37937537/article/details/82760339
五、sleep与wait的区别
- sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep 不会释放对象锁。sleep()使当前线程进入阻塞状态,在指定时间内不会执行。
- wait 是Object 类的方法,对此对象调用wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify 方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
区别比较:
1、这两个方法来自不同的类分别是Thread 和Object
2、最主要是sleep 方法没有释放锁,而wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3、wait,notify 和notifyAll 只能在同步控制方法或者同步控制块里面使用,而sleep 可以在任何地方使用(使用范围)
4、sleep 必须捕获异常,而wait,notify 和notifyAll 不需要捕获异常
(1) sleep 方法属于Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep 方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep 的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException 异常,如果你的程序不捕获这个异常,线程
就会异常终止,进入TERMINATED 状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch 语句块(可能还有finally 语句块)以及以后的代码。
注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t 线程
(2) wait 属于Object 的成员方法,一旦一个对象调用了wait 方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。
6.线程同步
7.线程通信
- 传统
- condition
Lock lock = new ReentrantLock();
Condition conn = lock.newCondition();
lock.await(): 相当于 wait()
lock.signal():相当于 notify()
lock.signalAll():相当于 notifyAll()
- 阻塞队列
二、线程池
1.线程池的优势
(1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
2.线程池的组成
- 线程池管理器:用于创建并管理线程池,包括创建线程,线程池的销毁,添加新任务
- 工作线程:线程池中的线程
- 任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口、任务的执行完毕后的收尾工作、任务的执行状态
- 任务队列:用于存放待处理的任务,提供一种缓冲机制
3.线程池的主要参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
3、keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
5、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
6、handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
4.线程池的工作流程
![]() |
---|
1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
6.线程池为什么要使用阻塞队列而不使用非阻塞队列?
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。
(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下
while (task != null || (task = getTask()) != null) {})。
不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?
5.如何配置线程池
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
6.java中提供的线程池
- 线程池的返回值ExecutorService简介:
ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程
Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor
![]() |
---|
四种线程池简介 (返回值都是ExecutorService)
1、Executors.newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
2、newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
3、newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
4、newScheduledThreadPool:适用于执行延时或者周期性任务。
7.execute()和submit()方法
1、execute(),执行一个任务,没有返回值。
2、submit(),提交一个线程任务,有返回值。
submit(Callable<T> task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用(IntentService中有体现)。
submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。
Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。
参考:https://www.cnblogs.com/dolphin0520/p/3949310.html
网友评论