java线程的原理和实现方式

本篇内容介绍了“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则可以;这让我们可以针对不同的业务情况作出不同的选择。
java线程的原理和实现方式

线程的状态

Thread.State枚举中定义了线程的六种状态

状态说明
NEWnew 对象后线程状态
RUNNABLEstart方法调用后;或者waiting状态下的线程调用了notify、LockSupport.Unpark等方法
BLOCKED等待synchronized代码块时的状态
WAITING调用wait()、join(),LockSupport.park()方法后
TIMED_WAITING调用wait(long)、sleep(long),join(long)方法后
TERMINATED线程运行完毕,或者调用terminate方法成功

关于线程状态流转,看下面这幅图;左边是正常状态流转,右边是存在block和waiting的情况:
java线程的原理和实现方式

网上有些文章说线程还分为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

ThreadPoolExecutor让我们可以自定义线程池,它有三个构造函数,它的参数含义如下 :
java线程的原理和实现方式

参数说明
int corePoolSize核心线程数量,当线程池内的线程数小于corePoolSize,会新建线程马上运行任务;这些线程创建后不会进行回收操作,即便是闲置状态
int maximumPoolSize最大线程数量,此参数需大于corePoolSize;当添加任务时,如果当前线程池线程数量大于corePoolSize,会提交给等待队列,当队列满了后,会新建线程(运行线程数不超过maximumPoolSize情况下),这一部分线程我们称之为非核心线程数,它和核心线程没有区别,只是系统会定时回收线程,最终线程数会保持在corePoolSize数量
long keepAliveTime非核心线程的回收时间,默认60
TimeUnit unitkeepAliveTime的单位,默认秒
BlockingQueue workQueue线程池中的任务队列,提交任务时如果核心线程数达到,则会提交到这里排队
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进行一个比对:
java线程的原理和实现方式
我们定义了最大线程是3个,排队两个,线程睡眠0.5s以达到效果;使用DiscardPolicy策略我们可以看到10个任务丢弃了5个。
但是CallerRunsPolicy策略会让主线程来执行任务,同时将主线程阻塞,已达到延缓提交任务的效果;当主线程执行完毕后,线程池内任务也执行完毕了,这时线程池会继续接受后续任务。

线程池提交方式

我们可以通过submit、execute方法提交任务给线程池执行,其中submit方法有三个重载,他们有以下区别

方法说明是否关心结果
void execute无返回值,提交后任务和主线程再无瓜葛
Future submit(Callable task)返回一个代表执行结果的Future对象,当调用Future的get方法时会获取到执行结果,如果线程发生异常会获取到异常信息
Future submit(Runnable task)返回一个代表执行结果的Future对象,,当调用Future的get方法时,成功返回null,如果线程发生异常会获取到异常信息
Future submit(Runnable task, T result)当线程正常结束的时候调用Future的get方法会返回result对象,当线程抛出异常的时候会获取到对应的异常的信息

线程池优化策略

  • CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1

  • IO密集型任务,参考值可以设置为2*NCPU

Executors

上面ThreadExecutorPool提供给我们手动实现线程池的方式,同时J.U.C包下面提供了线程池Executors类,可以让我们方便的创建线程池。
Executors提供了5种线程池的实现方式,以针对不同的业务场景:
java线程的原理和实现方式

从上图中我们可以看到,除了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

多数情况下,不推荐使用Executors,而推荐手动创建ThreadExecutorPool,因为Executors的五种实现方式有一下缺点:

  • 要么不能控制并发:资源耗尽、OOM

  • 要么不能设置等待队列大小:OOM

  • 不能指定拒绝策略

线程池的相关方法

方法说明
shutdown关闭线程池,执行以前提交的任务,但是不接受新提交的任务
isTerminated如果调用了shutdown,并且所有任务已经完成,则返回true,否则永远false
getActiveCount线程池存货的数量
getQueue().size等待队列大小
getPoolSize线程池中当前线程数量
getLargestPoolSize曾经有过的最大线程数量
getTaskCount未完成的任务数量
getCompletedTaskCount完成的任务数量

“java线程的原理和实现方式”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注创新互联网站,小编将为大家输出更多高质量的实用文章!


网页标题:java线程的原理和实现方式
当前网址:http://pwwzsj.com/article/pochei.html