重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
本篇内容介绍了“java线程的原理和实现方式”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
创新互联建站是一家专业提供京口企业网站建设,专注与网站制作、网站建设、H5技术、小程序制作等业务。10年已为京口众多企业、政府机构等服务。创新互联专业网络公司优惠进行中。
我们都知道,实现多线程的方式是继承Thread类和实现Runable接口,那么除了java中不允许多继承的这个特性,他们之间还有什么区别呢?我们看下面这个例子:
//继承Thread类 public class ThreadTest extends Thread { private AtomicInteger count = new AtomicInteger(0); @Override public void run() { for(int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName() + " " + count.incrementAndGet()); } } public static void main(String[] args) { new ThreadTest().start(); new ThreadTest().start(); } }
//实现Runable接口 public class RunnableTest implements Runnable { private AtomicInteger count = new AtomicInteger(0); @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " " + count.incrementAndGet()); } } public static void main(String[] args) { RunnableTest rab=new RunnableTest(); new Thread(rab).start(); new Thread(rab).start(); } }
通过观察console信息我们可以看到,继承Thread类,无法共享对象资源,而实现Runable则可以;这让我们可以针对不同的业务情况作出不同的选择。
Thread.State枚举中定义了线程的六种状态
状态 | 说明 |
---|---|
NEW | new 对象后线程状态 |
RUNNABLE | start方法调用后;或者waiting状态下的线程调用了notify、LockSupport.Unpark等方法 |
BLOCKED | 等待synchronized代码块时的状态 |
WAITING | 调用wait()、join(),LockSupport.park()方法后 |
TIMED_WAITING | 调用wait(long)、sleep(long),join(long)方法后 |
TERMINATED | 线程运行完毕,或者调用terminate方法成功 |
关于线程状态流转,看下面这幅图;左边是正常状态流转,右边是存在block和waiting的情况:
网上有些文章说线程还分为ready和running状态,这里需要注意的是,这两种执行状态都属于Runable状态;ready和running状态其实是描述VM/OS是否线程分配CPU资源,JVM并不能决定这一事情。当然运行中的线程通过调用yield方法是可以将running状态的线程切换到ready状态,但这一过程是OS层面的事情而非JVM层面。
在实现多线程的过程中,我们需要用到相关方法来进行线程状态的切换,已达到不同的目的。
Thread类的相关方法
方法 | 说明 |
---|---|
start | 启动线程,使线程进入运行状态 |
setPriority | 给线程设置优先级,有三个可选项:Thread.MIN_PRIORITY 最低优先级、Thread.NORM_PRIORITY 普通、Thread.MAX_PRIORITY 最高 |
sleep | 使线程进入睡眠状态,需要指定时间,线程进入等待状态,此时线程仍然持有锁 |
yield | 使线程交出CPU资源给优先级是同级及以上的线程,不可以指定时间,线程仍然是RUNABLE状态,此时线程仍然持有锁 |
interrupt | 终止未执行的线程,正在执行的线程不受影响,被终止的线程进入TERMINATED终止状态 |
stop | 强行终止线程,不管线程是否在执行中,此方法为废弃方法,不是线程安全方法,因为会释放线程持有的锁导致数据不一致的问题产生 |
join | 等待线程执行完毕,一般是在主线程中等待子线程执行结束,此方法会阻塞当前线程 |
Object类的相关方法
Obj类主要有wait()和notify()、notifyAll()三个方法来控制多线程读共享数据的访问;注意这三个方法不是Thread类的,故只能在同步代码块中才会产生效果。
方法 | 说明 |
---|---|
wait | 使线程暂停,并释放持有的锁,需要手动调用notify方法唤醒;线程暂停后会放入等待队列 |
wait(long) | 同wait,但是可以指定暂停的时间 |
notify | 唤醒暂停的线程,将线程移出等待队列;线程唤醒后并不立即执行,而是放入获取锁的队列中等待获取锁 |
notifyAll | 唤醒所有暂停的线程,将所有线程冲等待队列中移出,并放入获取锁的队列中 |
我们在系统中直接new线程是可以实现多线程,但是存在以下弊端:
线程不能重用,过多的线程创建和销毁会降低系统性能
不能控制线程并发数量
不能指定线程的定时执行等
ThreadPoolExecutor让我们可以自定义线程池,它有三个构造函数,它的参数含义如下 :
参数 | 说明 |
---|---|
int corePoolSize | 核心线程数量,当线程池内的线程数小于corePoolSize,会新建线程马上运行任务;这些线程创建后不会进行回收操作,即便是闲置状态 |
int maximumPoolSize | 最大线程数量,此参数需大于corePoolSize;当添加任务时,如果当前线程池线程数量大于corePoolSize,会提交给等待队列,当队列满了后,会新建线程(运行线程数不超过maximumPoolSize情况下),这一部分线程我们称之为非核心线程数,它和核心线程没有区别,只是系统会定时回收线程,最终线程数会保持在corePoolSize数量 |
long keepAliveTime | 非核心线程的回收时间,默认60 |
TimeUnit unit | keepAliveTime的单位,默认秒 |
BlockingQueue | 线程池中的任务队列,提交任务时如果核心线程数达到,则会提交到这里排队 |
ThreadFactory threadFactory | 创建线程的工厂,可以给每个创建出来的线程设置名字。一般情况下无须设置该参数 |
RejectedExecutionHandler handler | 拒绝策略,这是当任务队列和线程池都满了时所采取的应对策略,默认是AbordPolicy,表示直接抛出RejectedExecutionException 异常 |
任务队列
workQueue指明了当核心线程数达到最大时,任务的排队策略;有三种类型:
参数 | 说明 | 缺点 |
---|---|---|
直接提交 | 例如:SynchronousQueue(默认),这是一个没有数据缓冲的阻塞队列,队列中只能存放一个元素,超出之后后续线程提交会阻塞(maximumPoolSize达到阈值情况下) | 线程阻塞 |
无界队列 | 例如:不设定容量的LinkedBlockingQueue,当核心线程数满了之后,任务提交后可以一直存放在队列中 | maximumPoolSize失效,且有OOM风险 |
有界队列 | 例如:ArrayBlockingQueue,当核心线程数满了之后,一定量的任务提交可以存放在队列中,但是后续如果添加新的任务,需要有拒绝策略 | 需要指定拒绝策略 |
拒绝策略
在使用有界队列的前提下,如果工作队列已满,我们需要设定拒绝策略
参数 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默认策略;直接抛出RejectedExecutionException异常 |
ThreadPoolExecutor.CallerRunsPolicy | 将任务交给主线程执行,通过阻塞主线程达到减缓提交的作用 |
ThreadPoolExecutor.DiscardPolicy | 直接丢弃当前任务 |
ThreadPoolExecutor.DiscardOldestPolicy | 丢弃最老的任务 |
以上拒绝策略中,除了CallerRunsPolicy其他的都好理解,下面我们使用CallerRunsPolicy策略来和DiscardPolicy进行一个比对:
我们定义了最大线程是3个,排队两个,线程睡眠0.5s以达到效果;使用DiscardPolicy策略我们可以看到10个任务丢弃了5个。
但是CallerRunsPolicy策略会让主线程来执行任务,同时将主线程阻塞,已达到延缓提交任务的效果;当主线程执行完毕后,线程池内任务也执行完毕了,这时线程池会继续接受后续任务。
我们可以通过submit、execute方法提交任务给线程池执行,其中submit方法有三个重载,他们有以下区别
方法 | 说明 | 是否关心结果 |
---|---|---|
void execute | 无返回值,提交后任务和主线程再无瓜葛 | 否 |
返回一个代表执行结果的Future对象,当调用Future的get方法时会获取到执行结果,如果线程发生异常会获取到异常信息 | 是 | |
Future> submit(Runnable task) | 返回一个代表执行结果的Future对象,,当调用Future的get方法时,成功返回null,如果线程发生异常会获取到异常信息 | 是 |
当线程正常结束的时候调用Future的get方法会返回result对象,当线程抛出异常的时候会获取到对应的异常的信息 | 是 |
CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
IO密集型任务,参考值可以设置为2*NCPU
上面ThreadExecutorPool提供给我们手动实现线程池的方式,同时J.U.C包下面提供了线程池Executors类,可以让我们方便的创建线程池。
Executors提供了5种线程池的实现方式,以针对不同的业务场景:
从上图中我们可以看到,除了newWorkStealingPool以外,其他的线程池都只是通过ThreadExecutorPool构造函数,传递不同的参数而实现不同的效果,这也是本篇博客为什么先介绍ThreadExecutorPool的原因;虽然这几种线程池区别已经一目了然,我们还是列举一下它们的特点:
方法 | 说明 | 缺点 |
---|---|---|
newCachedThreadPool | 初始不指定线程池大小,提交任务后就创建线程,每隔60s回收一下空闲线程 | 最大并发数不可控制:导致系统资源耗尽;不拒绝任务:存在OOM风险 |
newFixedThreadPool | 指定固定大小线程池,不存在非核心线程,使用无界队列 | 无界队列:存在OOM风险 |
newScheduledThreadPool | 在手动创建ThreadExecutorPool的基础上加了一个定期任务,例如给线程池提交了两个任务,设置10s运行一次这两个任务 | 需要注意ThreadExecutorPool参数 |
newSingleThreadExecutor | 只有一个线程的线程池,使用无界队列 | 单线程略显单薄;无界队列:存在OOM风险 |
newWorkStealingPool | 返回一个ForkJoinPool而不是ThreadExecutorPool对象,可以指定线程数,详情见下面ForkJoinPool描述 | 适用于大任务情况 |
ForkJoinPool
ForkJoinPool适用于大型任务,其核心是Fork和Join;Fork可以将一个大任务拆分为多个小的任务,Join会将多个小的任务的结果汇总达到最终运行效果。
举个例子:假设一个线程池中并发数控制在两个,但是每个任务都要运行1分钟;假设我们当前服务器有4个CPU,两个在处理任务,其他的则为空闲状态,这时剩下的两个空闲CPU资源是浪费的。
试想一下:如果们能使剩下的两个空闲的CPU也能利用起来处理上面两个任务,任务运行肯定会加快,这就是ForkJoinPool的设计出发点。
多数情况下,不推荐使用Executors,而推荐手动创建ThreadExecutorPool,因为Executors的五种实现方式有一下缺点:
要么不能控制并发:资源耗尽、OOM
要么不能设置等待队列大小:OOM
不能指定拒绝策略
方法 | 说明 |
---|---|
shutdown | 关闭线程池,执行以前提交的任务,但是不接受新提交的任务 |
isTerminated | 如果调用了shutdown,并且所有任务已经完成,则返回true,否则永远false |
getActiveCount | 线程池存货的数量 |
getQueue().size | 等待队列大小 |
getPoolSize | 线程池中当前线程数量 |
getLargestPoolSize | 曾经有过的最大线程数量 |
getTaskCount | 未完成的任务数量 |
getCompletedTaskCount | 完成的任务数量 |
“java线程的原理和实现方式”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注创新互联网站,小编将为大家输出更多高质量的实用文章!