当前位置: 首页 > 后端技术 > Java

JDK成长10:你知道Thread的基本原理和常见应用场景吗?

时间:2023-04-01 18:07:35 Java

相信经过收藏的增长,大家已经对JDK源码的学习了如指掌。接下来,你将和我一起进入下半年的学习。开始吧!在接下来的10分钟里,你将学习线程的源码原理、线程的状态变化、线程的常见场景。线程基础回顾什么是线程?Thread,顾名思义,就是一个线程。要知道在操作系统上运行的java程序会启动一个JVM进程,进程ID是进程号,这个进程可以创建很多线程。操作系统、程序、进程、线程之间的关系如下图所示:运行一个线程实际上就是启动一个执行分支来执行不同的事情。执行分支可以是阻塞的或异步的。举个例子,如果你需要烧开水,又想玩玩手机。异步就是烧水的时候可以玩手机。阻塞就是等水烧开再开始玩手机。创建线程一般有两种方式Thread创建线程。一种是继承Thread重写run方法,另一种是实现Runnable或Callable接口后创建Thread。当然线程池也可以说是一种方式,但是底层还是以上两种方式之一,没有区别。这里带大家回顾一下,代码如下:代码清单:LinkedListDemo创建LinkedListpublicstaticvoidmain(String[]args){//创建线程1newThread(()->System.out.println(Thread.currentThread().getName()),"demo1").start();//创建线程2newThread(newMyThread(),"demo2").start();//创建线程3ThreadFactorynamedThreadFactory=newThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();ExecutorServicesingleThreadPool=newThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue(1024),namedThreadFactory,newThreadPoolExecutor.AbortPolicy());singleThreadPool.execute(()->System.out.println(Thread.currentThread().getName()));singleThreadPool.shutdown();}staticclassMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName());}}线程Thread的常用方法这些方法相信大家一定不陌生,这里不再赘述。我希望大家掌握的重点是分析一下线程在开源项目中是如何使用的,从而更好的理解原理,更好的使用线程。这个会在后面的成长故事中给大家讲解。start()启动一个线程join()加入一个线程sleep()线程休眠一段时间isAlive()线程是否存活interrupted()将线程标记为中断sinterupted()线程是否被中断此外,下面还有方法不常用,不推荐使用destroy(),stop(),suspend(),resume(),yeild()在开源项目和实际业务系统或基础设施系统中很少使用。线程的状态其实线程中最重要的就是它的状态流。这里放一张图给大家直接看。这张图很重要,对后面分析源码有很大的帮助。大家一定要牢记,线程状态图如下:线程应用场景举例在大家复习线程基础知识的时候,这里举几个例子。在各种开源框架中,线程的应用场景非常广泛。下面给大家举几个例子,主要是让大家明白熟练掌握线程的重要性。在后面的相关成长笔记中,你会了解到线程使用的细节。线程应用示例1.使用线程进行心跳和监控。在微服务系统中,经常用到的一个服务就是注册中心。简单的说,注册中心就是让一个服务在访问另一个服务的时候知道对方所有实例的地址,可以在调用的时候用来选择。这就需要每个服务实例都把自己的信息注册到一个公共的地方,这个地方就是注册中心。每个实例都有一个客户端与服务器通信。在SpringCloud实现的微服务技术栈中,Eureka组件作为注册中心。它的服务器Eureka-Server大量使用了Thread。这里我给出两个场景让大家体验一下Thread的应用。一种是场景,每个服务实例都需要发送心跳,告诉注册中心自己还在线,服务还活着。服务器通过线程判断。如果某个服务在一定时间内没有发送心跳,则认为该服务出现故障,其注册中心将被删除。如图中蓝色圆圈所示。另一个是场景。Eurekaserver将每个服务实例的注册信息放入一个内存映射中。为了提高速度和并发性,它设置了多个缓存,包括写缓存和读缓存。Eureka客户端客户端读取读缓存。写缓存数据什么时候会刷入读缓存?它是通过后台线程每30秒从写缓存刷新到读缓存。客户端还有一个后台线程,每30秒发起一次http请求,从服务端读取注册表中的数据。总之,在这个场景中,线程是用来做心跳和监控的,是一个很常见的场景。因为很多开源项目都是这样做的,比如hdfs,zookeeper等。线程应用示例2定时更新、保存、删除数据在MySQL、Redis、ES、HDFS、Zookeeper等开源项目中,都会有一个线程的概念,用于定时保存数据,或者刷新磁盘,清理磁盘盘等等。它们大体上很相似,一个是操作日志,一个是保存到磁盘的快照数据。比如hdfs中的checkpoint线程就是用来自动将edit_log合并到fsImage中,自动清除edit_log的线程,比如mysql周期性的把数据从bufferPool刷新到磁盘的线程。比如Zookeeper保存WAL日志,或者mysql定时保存dump文件,redis快照等。线程应用示例3多线程提高处理速度和性能。典型的分布式计算使用多线程计算来提高性能。当然,MQ的多线程消费也是多线程提升处理性能的典型例子。这里有些例子。有兴趣的同学可以把自己知道的开源项目中线程的使用方法写在评论区。线程源码分析回顾了线程的基础知识和使用场景。接下来需要了解Thread的源码才能更好的理解和使用它。下面主要通过创建线程、启动线程、线程状态改变操作来简单接触一下Thread的源码。第一种场景:创建线程newThread(()->System.out.println(Thread.currentThread().getName()),"demo1").start();我们来分析一下这行代码,首先newThread,会进入构造函数。publicThread(Runnabletarget){init(null,target,"Thread-"+nextThreadNum(),0);}在调用init方法之前,调用nextThreadNum方法,生成一个自增id,Thread-+num作为线程名。/*用于自动编号匿名线程。*/privatestaticintthreadInitNumber;privatestaticsynchronizedintnextThreadNum(){returnthreadInitNumber++;}上面源码总结如下图:默认生成线程名后,调用4个参数的init方法,然后继续call带有6个参数的init方法。privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize){init(g,target,name,stackSize,null,true);}这个init就是创建线程核心的步骤。让我们来看看它的上下文。privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize,AccessControlContextacc,booleaninheritThreadLocals){if(name==null){thrownewNullPointerException("namecannotbenull");}这个名字=名字;线程父=currentThread();SecurityManager安全=System.getSecurityManager();if(g==null){if(security!=null){g=security.getThreadGroup();}if(g==null){g=parent.getThreadGroup();}}g.checkAccess();if(security!=null){if(isCCLOverridden(getClass())){security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);}}g.addUnstarted();这个.group=g;this.daemon=parent.isDaemon();this.priority=parent.getPriority();如果(安全==null||isCCLOverridden(parent.getClass()))this.contextClassLoader=parent.getContextClassLoader();否则this.contextClassLoader=parent.contextClassLoader;this.inheritedAccessControlContext=acc!=null?acc:AccessController.getContext();(parent.inheritableThreadLocals);/*存储指定的堆栈大小以防VM关心*/this.stackSize=stackSize;/*设置线程ID*/tid=nextThreadID();}上面代码从上下文可以分为4点:设置线程名,默认为Thread+全局自增id,设置线程组,父线程默认为当前线程,默认线程所属父线程组优先级和是否为后台线程,默认和父线程一样保存传入的Runnable或Callablerun方法如下图所示:在线程创建过程中,核心指向你需要记住以下几点:创建你的线程,也就是你的父线程。如果你没有指定一个ThreadGroup,你的ThreadGroup就是父线程的ThreadGroup。守护状态默认为父线程的守护状态,你的优先级默认为父线程的优先级。如果不指定线程名称,则默认为Thread-0格式的名称。你的线程id是全局递增的,从1开始。第二个场景:启动线程创建线程后,下一步就是启动线程。你必须知道thread启动一个线程使用的是start方法。它的底层逻辑很简单,代码如下:publicsynchronizedvoidstart(){if(threadStatus!=0)thrownewIllegalThreadStateException();group.add(这个);布尔值开始=false;试试{开始0();开始=真;}finally{try{if(!started){group.threadStartFailed(this);}}catch(Throwableignore){}}}看一下核心上下文:1.判断线程状态,不能重复启动2.放入Thread组3.调用native方法启动线程,并移除如果失败,则从线程组中取出。这种朴素的方法是用C++实现的。我们不会研究细节。你只需要知道,其实质就是把线程交给CPU的线程执行调度器去执行,然后CPU通过时间片算法去执行每一个线程。如下图所示:启动完成后,肯定会调用run方法。这里的run方法就是前面构造函数保存的Runnable的run方法。如果是继承Thread创建的线程,则重写了run方法,所以以下逻辑只适用于Runnalbe或Callable创建的Thread。@Overridepublicvoidrun(){if(target!=null){target.run();第三种场景:线程状态改变操作了解了线程的创建不知道大家还记得之前的线程状态图吗?如何进入其他州?让我们看一下状态变化的一些线程操作。当一个线程被创建时,它在启动NEW后变为Runnable。后来怎么变成WAITING了?其实方法有很多。这里我们讲一个常用的方法join()。当然,调用Object.wait()或LockSupport.part()也会达到同样的效果。这一点在我们的成长笔记后面会提到,大家不要着急。顾名思义,加入就是加入的意思。这意味着另一个线程加入了执行过程,当前线程需要等待加入的线程执行完毕才能继续执行。如下图所示:那我们看看如何进入TimeWaiting状态?其实很简单。您可以使用sleep方法进入此状态。sleep方法支持传入等待时间。一般来说,有两种方式。一种是直接传入毫秒值,如60*1000表示60秒,另一种是通过TimeUnit工具传递时间。但是大多数开源项目为了灵活性都采用第一种方式,因为第二种方式限制了单位,如果需要修改的话使用起来不是很灵活。使用sleep的过程如下:查看sleep的源码会发现它是一个native方法,底层必须使用C++代码通知CPU线程休眠一段时间。无论是Java线程模型还是CPU线程模型,线程状态的变化其实都是一样的。实际上,线程状态模型是一个抽象的概念。最后一个线程是如何进入Block状态的?实际上,通过synchronized加锁后,被阻塞的线程就会进入block状态。后面说到synchronized的时候,我们会详细分析。我不会在这里详细介绍。好了,到这里我们已经通过线程的三个核心场景分析了线程的源码原理。相信大家对线程有了更深入的了解,但更重要的是要不断积累使用线程的场景,正确使用线程。了解线程的状态变化就更重要了。本文由博客多发平台OpenWrite发布!