精华内容
下载资源
问答
  • 线程调度,切换线程上下文,主要切换堆栈,以及各寄存器,因为同一个进程里的线程除了堆栈不同。协程又称为轻量级线程,每个协程都自带了一个栈,可以认为一个协程就是一个函数和这个存放这个函数运行时数据的...

    一:进程-线程-协程简介

    进程和线程的主要区别是:进程独享地址空间和资源,线程则共享地址空间和资源,多线程就是多栈。

    1、进程

    进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

      2、线程

    线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

      3、协程

    协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

    调度

    进程调度,切换进程上下文,包括分配的内存,包括数据段,附加段,堆栈段,代码段,以及一些表格。
    线程调度,切换线程上下文,主要切换堆栈,以及各寄存器,因为同一个进程里的线程除了堆栈不同。

    协程又称为轻量级线程,每个协程都自带了一个栈,可以认为一个协程就是一个函数和这个存放这个函数运行时数据的栈,这个栈非常小,一般只有几十kb。



    什么是协程

    wikipedia 的定义:协程是一个无优先级的子程序调度组件,允许子程序在特点的地方挂起恢复。

    线程包含于进程,协程包含于线程。只要内存足够,一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。

    为什么需要协程

    简单引入

    就实际使用理解来讲,协程允许我们写同步代码的逻辑,却做着异步的事,避免了回调嵌套,使得代码逻辑清晰。code like this:

       co(function*(next){
         let [err,data]=yield fs.readFile("./test.txt",next);//异步读文件
         [err]=yield fs.appendFile("./test2.txt",data,next);//异步写文件
         //....
       })()

    异步 指令执行之后,结果并不立即显现的操作称为异步操作。及其指令执行完成并不代表操作完成。

    协程是追求极限性能和优美的代码结构的产物。

    一点历史

    起初人们喜欢同步编程,然后发现有一堆线程因为I/O卡在那里,并发上不去,资源严重浪费。

    然后出了异步(select,epoll,kqueue,etc),将I/O操作交给内核线程,自己注册一个回调函数处理最终结果。

    然而项目大了之后代码结构变得不清晰,下面是个小例子。

      async_func1("hello world",func(){
         async_func2("what's up?",func(){
           async_func2("oh ,friend!",func(){ 
             //todo something
           })
         })
      })

    于是发明了协程,写同步的代码,享受着异步带来的性能优势。

    程序运行是需要的资源

    • cpu
    • 内存
    • I/O (文件、网络,磁盘(内存访问不在一个层级,忽略不计))

    协程的实现原理(异步实现)

    libco 一个C++协程库实现

    libco 是腾讯开源的一个C++协程库,作为微信后台的基础库,经受住了实际的检验。项目地址:https://github.com/Tencent/libco

    个人源码阅读项目:https://github.com/yyrdl/libco-code-study (未完结)

    libco源代码文件一共11个,其中一个是汇编代码,其余是C++,阅读起来相对较容易。

    在C++里面实现协程要解决的问题有如下几个:

    • 何时挂起协程?何时唤醒协程?
    • 如何挂起、唤醒协程,如何保护协程运行时的上下文?
    • 如何封装异步操作?

    前期知识准备

    1. 现代操作系统是分时操作系统,资源分配的基本单位是进程,CPU调度的基本单位是线程。
    2. C++程序运行时会有一个运行时栈,一次函数调用就会在栈上生成一个record
    3. 运行时内存空间分为全局变量区(存放函数,全局变量),栈区,堆区。栈区内存分配从高地址往低地址分配,堆区从低地址往高地址分配。
    4. 下一条指令地址存在于指令寄存器IP,ESP寄存值指向当前栈顶地址,EBP指向当前活动栈帧的基地址
    5. 发生函数调用时操作为:将参数从右往左依次压栈,将返回地址压栈,将当前EBP寄存器的值压栈,在栈区分配当前函数局部变量所需的空间,表现为修改ESP寄存器的值。
    6. 协程的上下文包含属于他的栈区和寄存器里面存放的值。

    何时挂起,唤醒协程?

    如开始介绍时所说,协程是为了使用异步的优势,异步操作是为了避免IO操作阻塞线程。那么协程挂起的时刻应该是当前协程发起异步操作的时候,而唤醒应该在其他协程退出,并且他的异步操作完成时。

    如何挂起、唤醒协程,如何保护协程运行时的上下文?

    协程发起异步操作的时刻是该挂起协程的时刻,为了保证唤醒时能正常运行,需要正确保存并恢复其运行时的上下文。

    所以这里的操作步骤为:

    • 保存当前协程的上下文(运行栈,返回地址,寄存器状态)
    • 设置将要唤醒的协程的入口指令地址到IP寄存器
    • 恢复将要唤醒的协程的上下文

    二:什么是上下文切换

    即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。

    CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换

    三:线程上下文切换和进程上下文切换的区别

    进程切换分两步
    1.切换页目录以使用新的地址空间
    2.切换内核栈和硬件上下文。

    对于linux来说,线程和进程的最大区别就在于地址空间。
    对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。所以明显是进程切换代价大

     

    线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

    另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

    四:上线文切换的实质




      对于线程或者协程来说切换的本质都是堆栈地址的保存和恢复。

     大概的切换流程如下:

    X86 32 Bists
    SS  --> 选择子--->段描述表-->(段限,段基址)
    CR3 --->页目录,页表 
    ESP--> 
    EBP-->   
    这其实和参数传递有点相似,只需要传递地址就够了。
    ESP,EBP 正式 堆栈指针寄存器。
    变量通过ESP,EBP 两个指针 加偏移量访问。

    五:比较

    1、进程多与线程比较

    线程是指进程内的一个执行单元,也是进程内的可调度实体。线程与进程的区别:

    1) 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间

    2) 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源

    3) 线程是处理器调度的基本单位,但进程不是

    4) 二者均可并发执行

    5) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

      2、协程多与线程进行比较

    1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。

    2) 线程进程都是同步机制,而协程则是异步

    3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

    4) 协程是用户级的任务调度,线程是内核级的任务调度。

    5) 线程是被动调度的,协程是主动调度的。 

    补充协程上下文环境的切换

    协程

            协程是一种编程组件,可以在不陷入内核的情况进行上下文切换。如此一来,我们就可以把协程上下文对象关联到fd,让fd就绪后协程恢复执行。

           当然,由于当前地址空间和资源描述符的切换无论如何需要内核完成,因此协程所能调度的,只有在同一进程(线程)中的不同上下文而已。

            我们在内核里实行上下文切换的时候,其实是将当前所有寄存器保存到内存中,然后从另一块内存中载入另一组已经被保存的寄存器。对于图灵机来说,当前状态寄存器意味着机器状态——也就是整个上下文。其余内容,包括栈上内存,堆上对象,都是直接或者间接的通过寄存器来访问的。 但是请仔细想想,寄存器更换这种事情,似乎不需要进入内核态么。事实上我们在用户态切换的时候,就是用了类似方案。也就是说协程是在用户态保存寄存器状态的!


    作为推论:在单个线程中执行的协程,可以视为单线程应用。这些协程,在未执行到特定位置(基本就是阻塞操作)前,是不会被抢占,也不会和其他CPU上的上下文发生同步问题的。因此,一段协程代码,中间没有可能导致阻塞的调用,执行在单个线程中。那么这段内容可以被视为同步的。

    我们经常可以看到某些协程应用,一启动就是数个进程。这并不是跨进程调度协程。一般来说,这是将一大群fd分给多个进程,每个进程自己再做fd-协程对应调度。

    基于就绪通知的协程框架(epool本身是同步的)
    1. 协程
    2. 首先需要包装read/write,在发生read的时候检查返回。如果是EAGAIN,那么将当前协程标记为阻塞在对应fd上,然后执行调度函数。
    3. 调度函数需要执行epoll(或者从上次的返回结果缓存中取数据,减少内核陷入次数),从中读取一个就绪的fd。如果没有,上下文应当被阻塞到至少有一个fd就绪。
    4. 查找这个fd对应的协程上下文对象,并调度过去。
    5. 当某个协程被调度到时,他多半应当在调度器返回的路上——也就是read/write读不到数据的时候。因此应当再重试读取,失败的话返回1。
    6. 如果读取到数据了,直接返回。

    这样,异步的数据读写动作,在我们的想像中就可以变为同步的。而我们知道同步模型会极大降低我们的编程负担。

    我们经常可以看到某些协程应用,一启动就是数个进程。这并不是跨进程调度协程。一般来说,这是将一大群fd分给多个进程,每个进程自己再做fd-协程对应调度。

    基于就绪通知的协程框架

    1. 首先需要包装read/write,在发生read的时候检查返回。如果是EAGAIN,那么将当前协程标记为阻塞在对应fd上,然后执行调度函数。
    2. 调度函数需要执行epoll(或者从上次的返回结果缓存中取数据,减少内核陷入次数),从中读取一个就绪的fd。如果没有,上下文应当被阻塞到至少有一个fd就绪。
    3. 查找这个fd对应的协程上下文对象,并调度过去。
    4. 当某个协程被调度到时,他多半应当在调度器返回的路上——也就是read/write读不到数据的时候。因此应当再重试读取,失败的话返回1。
    5. 如果读取到数据了,直接返回。

    这样,异步的数据读写动作,在我们的想像中就可以变为同步的。而我们知道同步模型会极大降低我们的编程负担。

    C/C++怎么实现协程

    作为一个C++后台开发,我知道像go, lua之类的语言在语言层面上提供了协程的api,但是我比较关心C++下要怎么实现这一点,下面的讨论都是从C/C++程序员的角度来看协程的问题的。

    boost和腾讯都推出了相关的库,语言层面没有提供这个东西。我近期阅读了微信开源的libco协程库,协程核心要解决几个问题:

    1. 协程怎么切换? 这个是最核心的问题,有很多trick可以做到这点,libco的做法是利用glibc中ucontext相关调用保存线程上下文,然后用swapcontext来切换协程上下文,libco的实现中对swapcontext的汇编实现做了一些删减和改动,所以在性能上会比C库的swapcontext提升1个数量级。

    2. IO阻塞了怎么办?试想在一个多协程的线程里,一个阻塞IO由一个协程发起,那么整个线程都阻塞了,别的协程也拿不到CPU资源,多个协程在一起等着IO的完成。libco中的做法是利用同名函数+dlsym来hook socket族的阻塞IO,比如read/write等,劫持了系统调用之后把这些IO注册到一个epoll的事件循环中,注册完之后把协程yield掉让出cpu资源,在IO完成的时候resume这个协程,这样其实把网络IO的阻塞点放在了epoll上,如果epoll没有就绪fd,那其实在超时时间内epoll还是阻塞的,只是把阻塞的粒度缩小了,本质上其实还是用epoll异步回调来解决网络IO问题的。那么问题来了,对于一些没有fd的一些重IO(比如大规模数据库操作)要怎么处理呢?答案是:libco并没有解决这个问题,而且也很难解决这个问题,首先要明确的一点是我们的目的是让用户只是仅仅调用了一个同步IO而已,不希望用户感知到调用IO的时候其实协程让出了cpu资源,按libco的思路一种可能的方法是,给所有重IO的api都hook掉,然后往某个异步事件库里丢这个IO事件,在异步事件返回的时候再resume协程。这里的难点是可能存在的重IO这么多,很难写成一个通用的库,只能根据业务需求来hook掉需要的调用,然后协程的编写中依然可以以同步的方式调用这些IO。从以上可能的做法来看协程很难去把所有阻塞调用都hook掉,所以libco很聪明的只把socket族的相关调用给hook掉,这样可以libco就成为一个通用的网络层面的协程库,可以很容易移植到现有的代码中进行改造,但是也让libco适用场景局限于如rpc风格的proxy/logic的场景中。在我的理解里,阻塞IO让出cpu是协程要解决的问题,但是不是协程本身的性质,从实现上我们可以看出我们还是在用异步回调的方式在解决这个问题,和协程本身无关。

    3. 如果一个协程没有发起IO,但是一直占用CPU资源不让出资源怎么办?无解,所以协程的编写对使用场景很重要,程序员对协程的理解也很重要,协程不适合于处理重cpu密集计算(耗时),只要某个协程即一直占用着线程的资源就是不合理的,因为这样做不到一个合理的并发,多线程同步模型由OS来调度并发,不存在说一个并发点需要让出资源给另一个,而协程在编写的时候cpu资源的让出是由程序员来完成的,所以协程代码的编写需要程序员对协程有比较深刻的理解。最极端的例子是程序员在协程里写个死循环,好,这个线程的所有协程都可以歇歇了。

     

    协程有什么好处

    说了这么多协程,协程的好处到底是啥?为什么要使用协程?

    1. 协程极大的优化了程序员的编程体验,同步编程风格能快速构建模块,并易于复用,而且有异步的性能(这个看具体库的实现),也不用陷入callback hell的深坑。

    2. 第二点也是我最近一直在纠结的一点,协程到底有没有性能提升?

    1)从多线程同步模型切到协程来看,首先很明确的性能提升点在于同步到异步的切换,libco中把阻塞的点全部放到了epoll线程中,而协程线程并不会发生阻塞。其次是协程的成本比线程小,线程受栈空间限制,而协程的栈空间由用户控制,而且实现协程需要的辅助数据结构很少,占用的内存少,那么就会有更大的容量,比如可以轻松开10w个协程,但是很难说开10w个线程。另外一个问题是很多人拿线程上下文切换比协程上下文切换开销大来推出协程模型比多线程并发模型性能优这点,这个问题我纠结了很久。对于这个问题,我先做一个简单的具体抽象:在不考虑阻塞的情况下,假设8核的cpu,不考虑抢占中断优先级等因素,100个任务并发执行,100个线程并发和10个线程每个线程10个协程并发对比两者都可以把cpu资源利用起来,对OS来说,前者100个线程参与cpu调度,后者10个线程参与cpu调度,后者还有额外的协程切换调度,先考虑线程切换的上下文,根据Linux内核调度器CFS的算法,每个线程拿到的时间片是动态的,进程数在分配的时间片在可变区间的情况下会直接影响到线程时间片的长短,所以100个线程每个线程的时间片在一定条件下会要比10个线程情况下的要短,也就意味着在相同时间里,前者的上下文切换次数是比后者要多的,所以可以得出一个结论:协程并发模型比多线程同步模型在一定条件下会减少线程切换次数(时间片有固定的范围,如果超出这个范围的边界则线程的时间片无差异),增加了协程切换次数,由于协程的切换是由程序员自己调度的,所以很难说协程切换的代价比省去的线程切换代价小,合理的方式应该是通过测试工具在具体的业务场景得出一个最好的平衡点。

    2)从异步回调模型切到协程模型来看,从一些已有协程库的实现来看,协程的同步写法却有异步性能其实还是异步回调在支撑这个事情,所以我认为协程模型是在异步模型之上的东西,考虑到本身协程上下文切换的开销(其实很小)和数据结构调用的一些开销,理论上协程是比异步回调的性能要稍微差一点,但是可以处于几乎持平的性能,因为协程实现的代价非常小。

    3)从一些异步驱动库的角度来看协程的话,因为异步框架把代码封装到很多个小类里面,然后串起来,这中间会涉及相当多的内存分配,而数据大都在离散的堆内存里面,而协程风格的代码,可以简单理解为一个简洁的连续空间的栈内存池,辅助数据结构也很少,所以协程可能会比厚重的封装性能会更好一些,但是这里的前提是,协程库能实现异步驱动库所需要的功能,并把它封装到同步调用里。

    展开全文
  • 线程上下文切换

    千次阅读 2018-08-29 10:05:04
    一、为什么要减少线程上下文切换  当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为...

                一、为什么要减少线程上下文切换
                    当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。上下文切换并不廉价,是比较耗时的
                二、线程上下文切换发生的条件
                    1.中断处理:中断分为硬件中断和软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起
                    2.多任务处理:每个程序都有相应的时间处理片,当前任务的时间片用完之后,系统CPU正常调度下一个任务
                    3.用户状态切换:这种情况下,上下文切换并非一定发生,只在特定操作系统才会发生上下文切换
                三、上下文切换的步骤
                    1.为了理解为什么上下文切换的时候会损耗性能,我们应该先看看上下文切换的过程中究竟发生了什么。在切换过程中,正在执行的进程的状态必须以某种方式存储起来,这样在未来才能被恢复。这里说的进程状态包括该进程正在使用的所有寄存器(尤其是程序计数器),和一些必要的操作系统数据。保存进程状态的数据结构叫做“进程控制块”(PCB,process control block);
                    2.PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息,它使一个在多道程序环境下不能独立运行的程序成为一个能独立运行的基本单位或一个能与其他进程并发执行的进程。
                    上下文切换的具体步骤是(假设当前进程是进程A,要切换到的下一个进程是进程B):
                        1.保存进程A的状态(寄存器和操作系统数据);
                        2.更新PCB中的信息,对进程A的“运行态”做出相应更改;
                        3.将进程A的PCB放入相关状态的队列;
                        4.将进程B的PCB信息改为“运行态”,并执行进程B;
                        5.B执行完后,从队列中取出进程A的PCB,恢复进程A被切换时的上下文,继续执行A。
                        6.线程分为用户级线程和内核级线程。同一进程中的用户级线程切换的时候,只需要保存用户寄存器的内容,程序计数器,栈指针,不需要模式切换。但是这样会导致线程阻塞和无法利用多处理器。而同一进程中的内核级线程切换的时候,就克服了这两个缺点,但是除了保存上下文,还要进行模式切换。
                    线程切换和进程切换的步骤不同。进程的上下文切换分为两步:1.切换页目录以使用新的地址空间;2.切换内核栈和硬件上下文。对于linux来说,线程和进程的最大区别就在于地址空间。对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。所以明显是进程切换代价大。线程上下文切换和进程上下文切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
                四、如何减少线程上下文切换
                        1.无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。 
                        2.CAS算法:Java的Atomic包使用CAS(compare and swap)算法来更新数据,而不需要加锁。 
                        3.使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。 
                        4.协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

    一、上下文切换
        1.即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停的切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒。
        2.CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是在切换之前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
    二、多线程并不一定快
    三、测试上下文切换的次数和时长
        1.通过Lmbench3测量上下文切换的次数
        2.通过vmstat测量上下文切换的次数
    五、避免死锁的常见方法
        1.避免一个线程同时获取多个锁
        2.避免一个线程在锁内同时占用多个资源
        3.尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
        4.对于数据库锁,加锁和解锁必须在一个数据库连接中,否则会出现解锁失败的情况
    六、资源限制的挑战
        1.什么是资源限制:资源限制是指在进行并发编程的时候,程序的执行速度受限于计算机硬件资源或软件资源。例如:硬件资源限制有带宽的上传和下载速度、硬盘读写速度和CPU的处理速度,软件资源限制有数据库的连接数和socket连接数等
        2.资源限制引起的问题:在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变为并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这个时候系统性能反而会变慢,因为增加了上下文切换和资源调度的时间。
        3.如何解决资源限制的问题:对于硬件资源限制,可以考虑使用资源池将资源服用。
        4.在资源限制下进行并发编程:如何在资源限制的情况下,让程序执行的更快呢?根据不同的资源限制调整程序的并发度。

    展开全文
  • 请求对象存在于当前上下文环境中。也就是说,上下文可以临时地把某些对象变成为全局访问的变量。多线程则完成上下文环境的切换,以适应多个进程/应用“同时”运行。一、上下文1.概念关于上下文,网上有很多好的...

    Flask从客户端收到请求时,要让视图函数能访问一些对象,这样才能处理请求。Flask使用请求对象封装了客户端发送的HTTP请求,传递给视图函数。

    请求对象存在哪?请求对象存在于当前的上下文环境中。也就是说,上下文可以临时地把某些对象变成为全局访问的变量。多线程则完成上下文环境的切换,以适应多个进程/应用“同时”运行。


    一、上下文


    1.概念

    关于上下文,网上有很多好的帖子,比如这个 https://www.jianshu.com/p/7a7efbb7205f



    相对于进程而言,上下文就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。可以理解上下文是环境的一个快照,是一个用来保存状态的对象。在程序中我们所写的函数大都不是单独完整的,在使用一个函数完成自身功能的时候,很可能需要同其他的部分进行交互,需要其他外部环境变量的支持,上下文就是给外部环境的变量赋值,使函数能正确运行。

                                                                                                                             简书

    作者:馒头白啊白



    但我们不能把它认为是真正的全局变量,线程之间必须是相互独立的!

    试想,如果你开发了一个网站。此时有多个人同时访问你的网站,如果你把这些对服务器的请求信息都放在一个全局变量request中,数据还能完整的存储得来吗?


    这时候就需要在服务器应用上设置一个多线程,完成上下文环境的切换,使得线程与线程之间互不干扰。


    2.实现机制

    通过栈结构来实现上下文的保存。要知道栈是一个后入先出的存储机制,为什么是栈结构呢?


    1. 应用上下文:Flask底层是基于werkzeug,werkzeug是可以包含多个app的,所以这时候用一个栈来保存。如果你在使用app1,那么app1应该是要在栈的顶部,如果用完了app1,那么app1应该从栈中删除。方便其他代码使用下面的app。

    2. 如果在写测试代码,或者离线脚本的时候,我们有时候可能需要创建多个请求上下文,这时候就需要存放到一个栈中了。使用哪个请求上下文的时候,就把对应的请求上下文放到栈的顶部,用完了就要把这个请求上下文从栈中移除掉。


    具体参考:https://blog.tonyseek.com/post/the-context-mechanism-of-flask/


    二、线程&多线程

    1.概念

    也许你不理解线程以及多线程的概念,先让我介绍一下


    线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程

    百度百科

    简单理解,一个线程就是程序中的一条执行路径,多线程就是多个不同的执行路径(路径与路径之间没有交叉)。


    2.flask使用


    python中有多线程的构造方法,利用的是Thread Local对象,实现线程隔离的概念。不过通过flask的Local对象能实现更加强大的功能。

    #encoding: utf-8

    from threading import Thread
    from werkzeug.local import Local

    local = Local()

    local.request = '123'

    class MyThread(Thread):
    def run(self):
    local.request = 'abc'
           
    print('子线程:',local.request)

    mythread = MyThread()
    mythread.start()
    mythread.join()

    print('主线程:',local.request)

    输出:

    子线程: abc

    主线程: 123

    说明此事两个线程之间已经互不影响了!!


    三、回到上下文

    之前讲了flask实现上下文切换的方法,但是关于上下文还没深入了解。现在言归正传,

    1.在Flask中,上下文分为两种:程序上下文、请求上下文

    应用上下文和请求上下文都是存放到一个`LocalStack`的栈中。

    和应用app相关的操作就必须要用到应用上下文,比如通过`current_app`获取当前的这个`app`;

    和请求相关的操作就必须用到请求上下文,比如使用`url_for`反转视图函数。

    具体参考:https://blog.tonyseek.com/post/the-context-mechanism-of-flask/



    2.两种上下文提供的变量


    current_app:程序上下文,当前激活程序的程序实例

    g:                 程序上下文,处理请求时临时存储的对象。每次请求重设该变量。

    request:        请求上下文,请求对象,封装了客户端发出的HTTP请求内容。

    session:        用户会话,用于存储请求之间需要“记住”的值的字典。


    3.上下文推送

    在进行请求处理之前必须要推送相应的上下文,请求完成后再将其删除,否则会报错。


    1. 在视图函数中,不用担心上下文的问题。因为视图函数要执行,那么肯定是通过访问url的方式执行的,那么这种情况下,Flask底层就已经自动的帮我们把请求上下文和应用上下文都推入到了相应的栈中。


    2. 如果想要在视图函数外面执行相关的操作,比如获取当前的app(current_app),或者是反转url,那么就必须要手动推入相关的上下文:

        * 手动推入app上下文:

    # 第一种方式:
    app_context = app.app_context()
    app_context.push()
    # 第二种方式:
    with app.app_context():
    print( current_app )

        * 手动推入请求上下文:推入请求上下文到栈中,会首先判断有没有应用上下文,如果没有那么就会先推入应用上下文到栈中,然后再推入请求上下文到栈中:

    with app.test_request_context():
    print( url_for( 'my_list' ) )



    四、总结

    本文算是一篇入门级别的文章,更多关于上下文的资源还需自己去了解,这里有关于flask上下文的几篇不错的文章:


    Flask的Context(上下文)学习笔记 https://www.jianshu.com/p/7a7efbb7205f

    flask上下文机制:https://blog.tonyseek.com/post/the-context-mechanism-of-flask/


    展开全文
  • 1 线程上下文类加载器 2 何时使用Thread.getContextClassLoader()? 3 类加载器与Web容器 4 类加载器与OSGi 总结 1 线程上下文类加载器  线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的...

    目录

    1 线程上下文类加载器

    2 何时使用Thread.getContextClassLoader()?

    3 类加载器与Web容器

    4 类加载器与OSGi

    总结


    1 线程上下文类加载器

      线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
      前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
      线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
      Java默认的线程上下文类加载器是系统类加载器(AppClassLoader)。以下代码摘自sun.misc.Launch的无参构造函数Launch()。

    // Now create the class loader to use to launch the application
    try {
        loader = AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
    "Could not create application class loader" );
    }
    
    
    // Also set the context class loader for the primordial thread.
    Thread.currentThread().setContextClassLoader(loader);

      使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。典型的例子有:通过线程上下文来加载第三方库jndi实现,而不依赖于双亲委派。大部分java application服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。还有一些采用hot swap特性的框架,也使用了线程上下文类加载器,比如 seasar (full stack framework in japenese)。
      线程上下文从根本解决了一般应用不能违背双亲委派模式的问题。使java类加载体系显得更灵活。随着多核时代的来临,相信多线程开发将会越来越多地进入程序员的实际编码过程中。因此,在编写基础设施时, 通过使用线程上下文来加载类,应该是一个很好的选择。
      当然,好东西都有利弊。使用线程上下文加载类,也要注意保证多个需要通信的线程间的类加载器应该是同一个,防止因为不同的类加载器导致类型转换异常(ClassCastException)。
      defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)是java.lang.Classloader提供给开发人员,用来自定义加载class的接口。使用该接口,可以动态的加载class文件。例如在jdk中,URLClassLoader是配合findClass方法来使用defineClass,可以从网络或硬盘上加载class。而使用类加载接口,并加上自己的实现逻辑,还可以定制出更多的高级特性。
      下面是一个简单的hot swap类加载器实现。hot swap即热插拔的意思,这里表示一个类已经被一个加载器加载了以后,在不卸载它的情况下重新再加载它一次。我们知道Java缺省的加载器对相同全名的类只会加载一次,以后直接从缓存中取这个Class object。因此要实现hot swap,必须在加载的那一刻进行拦截,先判断是否已经加载,若是则重新加载一次,否则直接首次加载它。我们从URLClassLoader继承,加载类的过程都代理给系统类加载器URLClassLoader中的相应方法来完成。

    package classloader;
    
    
    import java.net.URL;
    import java.net.URLClassLoader;
    
    
    /**
     * 可以重新载入同名类的类加载器实现
     * 放弃了双亲委派的加载链模式,需要外部维护重载后的类的成员变量状态
     */
    public class HotSwapClassLoader extends URLClassLoader {
    
    
        public HotSwapClassLoader(URL[] urls) {
            super(urls);
        }
    
    
        public HotSwapClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }
    
    
        // 下面的两个重载load方法实现类的加载,仿照ClassLoader中的两个loadClass()
        // 具体的加载过程代理给父类中的相应方法来完成
        public Class<?> load(String name) throws ClassNotFoundException {
            return load(name, false);
        }
    
    
        public Class<?> load(String name, boolean resolve) throws ClassNotFoundException {
            // 若类已经被加载,则重新再加载一次
            if (null != super.findLoadedClass(name)) {
                return reload(name, resolve);
            }
            // 否则用findClass()首次加载它
            Class<?> clazz = super.findClass(name);
            if (resolve) {
                super.resolveClass(clazz);
            }
            return clazz;
        }
    
    
        public Class<?> reload(String name, boolean resolve) throws ClassNotFoundException {
            return new HotSwapClassLoader(super.getURLs(), super.getParent()).load(
                    name, resolve);
        }
    }

      两个重载的load方法参数与ClassLoader类中的两个loadClass()相似。在load的实现中,用findLoadedClass()查找指定的类是否已经被祖先加载器加载了,若已加载则重新再加载一次,从而放弃了双亲委派的方式(这种方式只会加载一次)。若没有加载则用自身的findClass()来首次加载它。
      下面是使用示例:

    package classloader;
    
    
    public class A {
        
        private B b;
    
    
        public void setB(B b) {
            this.b = b;
        }
    
    
        public B getB() {
            return b;
        }
    }
    package classloader;
    
    
    public class B {
        
    }
    package classloader;
    
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    
    public class TestHotSwap {
    
    
        public static void main(String args[]) throws MalformedURLException {
            A a = new A();  // 加载类A
            B b = new B();  // 加载类B
            a.setB(b);  // A引用了B,把b对象拷贝到A.b
            System.out.printf("A classLoader is %s\n", a.getClass().getClassLoader());
            System.out.printf("B classLoader is %s\n", b.getClass().getClassLoader());
            System.out.printf("A.b classLoader is %s\n", a.getB().getClass().getClassLoader());
    
    
            try {
                URL[] urls = new URL[]{ new URL("file:///C:/Users/JackZhou/Documents/NetBeansProjects/classloader/build/classes/") };
                HotSwapClassLoader c1 = new HotSwapClassLoader(urls, a.getClass().getClassLoader());
                Class clazz = c1.load("classloader.A");  // 用hot swap重新加载类A
                Object aInstance = clazz.newInstance();  // 创建A类对象
                Method method1 = clazz.getMethod("setB", B.class);  // 获取setB(B b)方法
                method1.invoke(aInstance, b);    // 调用setB(b)方法,重新把b对象拷贝到A.b
                Method method2 = clazz.getMethod("getB");  // 获取getB()方法
                Object bInstance = method2.invoke(aInstance);  // 调用getB()方法
                System.out.printf("Reloaded A.b classLoader is %s\n", bInstance.getClass().getClassLoader());
            } catch (MalformedURLException | ClassNotFoundException | 
                    InstantiationException | IllegalAccessException | 
                    NoSuchMethodException | SecurityException | 
                    IllegalArgumentException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    运行输出:

    A classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
    B classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
    A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93
    Reloaded A.b classLoader is sun.misc.Launcher$AppClassLoader@73d16e93

      HotSwapClassLoader加载器的作用是重新加载同名的类。为了实现hot swap,一个类在加载过后,若重新再加载一次,则新的Class object的状态会改变,老的状态数据需要通过其他方式拷贝到重新加载过的类生成的全新Class object实例中来。上面A类引用了B类,加载A时也会加载B(如果B已经加载,则直接从缓存中取出)。在重新加载A后,其Class object中的成员b会重置,因此要重新调用setB(b)拷贝一次。你可以注释掉这行代码,再运行会抛出java.lang.NullPointerException,指示A.b为null。
      注意新的A Class object实例所依赖的B类Class object,如果它与老的B Class object实例不是同一个类加载器加载的, 将会抛出类型转换异常(ClassCastException),表示两种不同的类。因此在重新加载A后,要特别注意给它的B类成员b传入外部值时,它们是否由同一个类加载器加载。为了解决这种问题, HotSwapClassLoader自定义的l/oad方法中,当前类(类A)是由自身classLoader加载的, 而内部依赖的类(类B)还是老对象的classLoader加载的。

    2 何时使用Thread.getContextClassLoader()?

      这是一个很常见的问题,但答案却很难回答。这个问题通常在需要动态加载类和资源的系统编程时会遇到。总的说来动态加载资源时,往往需要从三种类加载器里选择:系统或程序的类加载器、当前类加载器、以及当前线程的上下文类加载器。在程序中应该使用何种类加载器呢?
      系统类加载器通常不会使用。此类加载器处理启动应用程序时classpath指定的类,可以通过ClassLoader.getSystemClassLoader()来获得。所有的ClassLoader.getSystemXXX()接口也是通过这个类加载器加载的。一般不要显式调用这些方法,应该让其他类加载器代理到系统类加载器上。由于系统类加载器是JVM最后创建的类加载器,这样代码只会适应于简单命令行启动的程序。一旦代码移植到EJB、Web应用或者Java Web Start应用程序中,程序肯定不能正确执行。
      因此一般只有两种选择,当前类加载器和线程上下文类加载器。当前类加载器是指当前方法所在类的加载器。这个类加载器是运行时类解析使用的加载器,Class.forName(String)和Class.getResource(String)也使用该类加载器。代码中X.class的写法使用的类加载器也是这个类加载器。
      线程上下文类加载器在Java 2(J2SE)时引入。每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。Web应用和Java企业级应用中,应用服务器经常要使用复杂的类加载器结构来实现JNDI(Java命名和目录接口)、线程池、组件热部署等功能,因此理解这一点尤其重要。
      为什么要引入线程的上下文类加载器?将它引入J2SE并不是纯粹的噱头,由于Sun没有提供充分的文档解释说明这一点,这使许多开发者很糊涂。实际上,上下文类加载器为同样在J2SE中引入的类加载代理机制提供了后门。通常JVM中的类加载器是按照层次结构组织的,目的是每个类加载器(除了启动整个JVM的原初类加载器)都有一个父类加载器。当类加载请求到来时,类加载器通常首先将请求代理给父类加载器。只有当父类加载器失败后,它才试图按照自己的算法查找并定义当前类。
      有时这种模式并不能总是奏效。这通常发生在JVM核心代码必须动态加载由应用程序动态提供的资源时。拿JNDI为例,它的核心是由JRE核心类(rt.jar)实现的。但这些核心JNDI类必须能加载由第三方厂商提供的JNDI实现。这种情况下调用父类加载器(原初类加载器)来加载只有其子类加载器可见的类,这种代理机制就会失效。解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。
      顺便提一下,XML解析API(JAXP)也是使用此种机制。当JAXP还是J2SE扩展时,XML解析器使用当前类加载器方法来加载解析器实现。但当JAXP成为J2SE核心代码后,类加载机制就换成了使用线程上下文加载器,这和JNDI的原因相似。
      好了,现在我们明白了问题的关键:这两种选择不可能适应所有情况。一些人认为线程上下文类加载器应成为新的标准。但这在不同JVM线程共享数据来沟通时,就会使类加载器的结构乱七八糟。除非所有线程都使用同一个上下文类加载器。而且,使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。即使你想尽可能只使用上下文类加载器,总是有这样那样的代码不是你所能控制的。这些代码都使用代理到当前类加载器的模式。混杂使用代理模式是很危险的。
      更为糟糕的是,某些应用服务器将当前类加载器和上下文类加器分别设置成不同的ClassLoader实例。虽然它们拥有相同的类路径,但是它们之间并不存在父子代理关系。想想这为什么可怕:记住加载并定义某个类的类加载器是虚拟机内部标识该类的组成部分,如果当前类加载器加载类X并接着执行它,如JNDI查找类型为Y的数据,上下文类加载器能够加载并定义Y,这个Y的定义和当前类加载器加载的相同名称的类就不是同一个,使用隐式类型转换就会造成异常。
      这种混乱的状况还将在Java中存在很长时间。在J2SE中还包括以下的功能使用不同的类加载器:
      (1)JNDI使用线程上下文类加载器。
      (2)Class.getResource()和Class.forName()使用当前类加载器。
      (3)JAXP使用上下文类加载器。
      (4)java.util.ResourceBundle使用调用者的当前类加载器。
      (5)URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器。
      (6)Java序列化API缺省使用调用者当前的类加载器。
      这些类加载器非常混乱,没有在J2SE文档中给以清晰明确的说明。
      该如何选择类加载器?
      如若代码是限于某些特定框架,这些框架有着特定加载规则,则不要做任何改动,让框架开发者来保证其工作(比如应用服务器提供商,尽管他们并不能总是做对)。如在Web应用和EJB中,要使用Class.gerResource来加载资源。
      在其他情况下,我们可以自己来选择最合适的类加载器。可以使用策略模式来设计选择机制。其思想是将“总是使用上下文类加载器”或者“总是使用当前类加载器”的决策同具体实现逻辑分离开。往往设计之初是很难预测何种类加载策略是合适的,该设计能够让你可以后来修改类加载策略。
      考虑使用下面的代码,这是作者本人在工作中发现的经验。这儿有一个缺省实现,应该可以适应大部分工作场景:

    package classloader.context;
    
    
    /**
     * 类加载上下文,持有要加载的类
     */
    public class ClassLoadContext {
    
    
        private final Class m_caller;
    
    
        public final Class getCallerClass() {
            return m_caller;
        }
    
    
        ClassLoadContext(final Class caller) {
            m_caller = caller;
        }
    }
    
    package classloader.context;
    
    
    /**
     * 类加载策略接口
     */
    public interface IClassLoadStrategy {
    
    
        ClassLoader getClassLoader(ClassLoadContext ctx);
    }
    /**
     * 缺省的类加载策略,可以适应大部分工作场景
     */
    public class DefaultClassLoadStrategy implements IClassLoadStrategy {
    
    
        /**
         * 为ctx返回最合适的类加载器,从系统类加载器、当前类加载器
         * 和当前线程上下文类加载中选择一个最底层的加载器
         * @param ctx
         * @return 
         */
        @Override
        public ClassLoader getClassLoader(final ClassLoadContext ctx) {
            final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();
            final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
            ClassLoader result;
    
    
            // If 'callerLoader' and 'contextLoader' are in a parent-child
            // relationship, always choose the child:
            if (isChild(contextLoader, callerLoader)) {
                result = callerLoader;
            } else if (isChild(callerLoader, contextLoader)) {
                result = contextLoader;
            } else {
                // This else branch could be merged into the previous one,
                // but I show it here to emphasize the ambiguous case:
                result = contextLoader;
            }
            final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
            // Precaution for when deployed as a bootstrap or extension class:
            if (isChild(result, systemLoader)) {
                result = systemLoader;
            }
            
            return result;
        }
        
        // 判断anotherLoader是否是oneLoader的child
        private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){
            //...
        }
    
    
        // ... more methods 
    }

      决定应该使用何种类加载器的接口是IClassLoaderStrategy,为了帮助IClassLoadStrategy做决定,给它传递了个ClassLoadContext对象作为参数。ClassLoadContext持有要加载的类。
      上面代码的逻辑很简单:如调用类的当前类加载器和上下文类加载器是父子关系,则总是选择子类加载器。对子类加载器可见的资源通常是对父类可见资源的超集,因此如果每个开发者都遵循J2SE的代理规则,这样做大多数情况下是合适的。
      当前类加载器和上下文类加载器是兄弟关系时,决定使用哪一个是比较困难的。理想情况下,Java运行时不应产生这种模糊。但一旦发生,上面代码选择上下文类加载器。这是作者本人的实际经验,绝大多数情况下应该能正常工作。你可以修改这部分代码来适应具体需要。一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。
      最后需要检查一下,以便保证所选类加载器不是系统类加载器的父亲,在开发标准扩展类库时这通常是个好习惯。
      注意作者故意没有检查要加载资源或类的名称。Java XML API成为J2SE核心的历程应该能让我们清楚过滤类名并不是好想法。作者也没有试图检查哪个类加载器加载首先成功,而是检查类加载器的父子关系,这是更好更有保证的方法。
      下面是类加载器的选择器:

    package classloader.context;
    
    
    /**
     * 类加载解析器,获取最合适的类加载器
     */
    public abstract class ClassLoaderResolver {
            
        private static IClassLoadStrategy s_strategy;  // initialized in <clinit>
        private static final int CALL_CONTEXT_OFFSET = 3;  // may need to change if this class is redesigned
        private static final CallerResolver CALLER_RESOLVER;  // set in <clinit>
        
        static {
            try {
                // This can fail if the current SecurityManager does not allow
                // RuntimePermission ("createSecurityManager"):
                CALLER_RESOLVER = new CallerResolver();
            } catch (SecurityException se) {
                throw new RuntimeException("ClassLoaderResolver: could not create CallerResolver: " + se);
            }
            s_strategy = new DefaultClassLoadStrategy();  //默认使用缺省加载策略
        }
    
    
        /**
         * This method selects the best classloader instance to be used for
         * class/resource loading by whoever calls this method. The decision
         * typically involves choosing between the caller's current, thread context,
         * system, and other classloaders in the JVM and is made by the {@link IClassLoadStrategy}
         * instance established by the last call to {@link #setStrategy}.
         * 
         * @return classloader to be used by the caller ['null' indicates the
         * primordial loader]
         */
        public static synchronized ClassLoader getClassLoader() {
            final Class caller = getCallerClass(0); // 获取执行当前方法的类
            final ClassLoadContext ctx = new ClassLoadContext(caller);  // 创建类加载上下文
            return s_strategy.getClassLoader(ctx);  // 获取最合适的类加载器
        }
    
    
        public static synchronized IClassLoadStrategy getStrategy() {
            return s_strategy;
        }
    
    
        public static synchronized IClassLoadStrategy setStrategy(final IClassLoadStrategy strategy) {
            final IClassLoadStrategy old = s_strategy;  // 设置类加载策略
            s_strategy = strategy;
            return old;
        }
    
    
        /**
         * A helper class to get the call context. It subclasses SecurityManager
         * to make getClassContext() accessible. An instance of CallerResolver
         * only needs to be created, not installed as an actual security manager.
         */
        private static final class CallerResolver extends SecurityManager {
            @Override
            protected Class[] getClassContext() {
                return super.getClassContext();  // 获取当执行栈的所有类,native方法
            }
    
    
        }
    
    
        /*
         * Indexes into the current method call context with a given
         * offset.
         */
        private static Class getCallerClass(final int callerOffset) {
            return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET
                    + callerOffset];  // 获取执行栈上某个方法所属的类
        }
    }

      可通过调用ClassLoaderResolver.getClassLoader()方法来获取类加载器对象,并使用其ClassLoader的接口如loadClass()等来加载类和资源。此外还可使用下面的ResourceLoader接口来取代ClassLoader接口:

    package classloader.context;
    
    
    import java.net.URL;
    
    
    public class ResourceLoader {
    
    
        /**
         * 加载一个类
         * 
         * @param name
         * @return 
         * @throws java.lang.ClassNotFoundException 
         * @see java.lang.ClassLoader#loadClass(java.lang.String)
         */
        public static Class<?> loadClass(final String name) throws ClassNotFoundException {
            //获取最合适的类加载器
            final ClassLoader loader = ClassLoaderResolver.getClassLoader();
            //用指定加载器加载类
            return Class.forName(name, false, loader);
        }
    
    
        /**
         * 加载一个资源
         * 
         * @param name
         * @return 
         * @see java.lang.ClassLoader#getResource(java.lang.String)
         */
        public static URL getResource(final String name) {
            //获取最合适的类加载器
            final ClassLoader loader = ClassLoaderResolver.getClassLoader();
            //查找指定的资源
            if (loader != null) {
                return loader.getResource(name);
            } else {
                return ClassLoader.getSystemResource(name);
            }
        }
    
    
        // ... more methods ...
    }

      ClassLoadContext.getCallerClass()返回的类在ClassLoaderResolver或ResourceLoader使用,这样做的目的是让其能找到调用类的类加载器(上下文加载器总是能通过Thread.currentThread().getContextClassLoader()来获得)。注意调用类是静态获得的,因此这个接口不需现有业务方法增加额外的Class参数,而且也适合于静态方法和类初始化代码。具体使用时,可以往这个上下文对象中添加具体部署环境中所需的其他属性。

    3 类加载器与Web容器

      对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
      绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
      (1)每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
      (2)多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
      (3)当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

    4 类加载器与OSGi

      OSGi是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse就是基于OSGi 技术来构建的。
      OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。
      假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类 com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类 com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类,都是由父类加载器来加载的。如果声明了系统属性 org.osgi.framework.bootdelegation=com.example.core.*,那么对于包 com.example.core中的类,都是由父类加载器来完成的。
      OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:
      (1)如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。
      (2)如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
      (3)如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。

    总结

      类加载器是 Java 语言的一个创新。它使得动态安装和更新软件组件成为可能。本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器、与 Web 容器和 OSGi 的关系等。开发人员在遇到 ClassNotFoundException和 NoClassDefFoundError等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。

    参考文献:

    https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

    http://www.blogjava.net/lihao336/archive/2009/09/17/295489.html

    http://kenwublog.com/structure-of-java-class-loader

    展开全文
  • 线程上下文切换 中断上下文切换 Go程序内核线程能有多少个? Linux创建的线程是用户级还是内核级线程? 什么是:CPU寄存器 CPU上下文切换? CPU寄存器,是CPU内置的容量小、但速度极快的...
  • 线程上下文切换

    千次阅读 2017-12-30 22:05:40
    本人写博客是为了加深对知识内容的理解,文中的大部分...这么问也就说明答案是否定的,而这道题的精髓就在于你能不能说出上下文切换这几个字。那什么是上下文切换呢? 上下文切换 定义1:即使是单核的cpu也能够执行
  • Java线程上下文类加载器

    千次阅读 2015-01-09 00:01:18
    1 线程上下文类加载器  线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置...
  • 1、信号来自进程或内核 ...在创建线程时,调用pthread_create函数的线程和新建线程的执行顺序随机 3、在linux中,使用轻量级进程来模拟线程线程操作的相关函数通过第三方线程库 (Linuxthreads 或 Native POS
  • 调度、线程上下文以及IRQL

    千次阅读 2014-06-29 19:37:56
    线程调度以及线程上下文当前的IRQL(中断请求级)对于每个处理器上面的驱动程序有很大的影响。而一个线程的调度优先级和处理器的当前IRQL能够决定一个运行的线程能否被中断或者抢占。在抢占式调度过程当中,系统...
  • 内核线程 中断上下文 睡眠

    千次阅读 2011-03-19 09:53:00
    1)中断处理程序(top half)中不能睡眠,是因为哪个原因:a)没有进程上下文,睡眠之后不能重新调度?b)中断程序可能关闭了所有中断(使用SA_INTERRUPT),那么再睡眠,就没有抢占点了(中断都进不来,哪里可以schedule()...
  • 此前我对线程上下文类加载器(ThreadContextLoader)的理解仅仅局限于下面这段话: Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC...
  • 介绍了多线程的概念、优点,以及线程上下文切换时的性能损耗问题
  • 线程编程-减少上下文切换(1)

    千次阅读 2016-07-31 21:40:17
    1、多线程编程-1-如何减少上下文切换  我们在使用多线程时,不是多线程就能提升程序的执行速度,程序在执行时,... 操作系统中,CPU时间分片切换到另一个就绪的线程,则需要保存当前线程的运行的位置,同时需要加载需
  • 可怕的线程上下文类装载器(TCCL)

    千次阅读 2017-02-03 15:42:56
    在演讲中我将会提及 Java 的线程上下文类加载器(TCCL),但是整个演讲只有 25 分钟,我没有更多时间对此进行深入讨论。所以我希望写这篇博客能够帮助大家了解到一些相关背景信息。本文中的很多技术信息和研究取自于 ...
  • Sentinel 调用上下文环境实现原理

    千次阅读 2020-01-12 13:32:55
    用源码与图解的方式详细探究 Sentinel 调用上下文环境是如何管理的。 本节将详细介绍 Sentienl 的上下文环境管理机制。 1、Sentinel Context 调用上下文环境管理 我们从 sentinel-apache-dubbo-adapter 模块的 ...
  • 当一个计算机是多道程序设计系统时,会频繁的有很多进程或者线程来同时竞争 CPU 时间片。当两个或两个以上的进程/线程处于就绪状态时,就会发生这种情况。如果只有一个 CPU 可用,那么必须选择接下来哪个进程/线程...
  • Java线程上下文-ThreadLocal的那些事

    千次阅读 2018-07-15 22:25:29
    其含义是ThreadLocal为变量在每个线程中都创建一个副本,则每个线程可以访问自身内部的副本变量。 概念总是抽象而且晦涩的,我们从两个例子说起。 1、对象间/方法间跨层传递 如下图,有个多层调...
  • 除了上面三种状态之外,还有一种就是永远处于内核态的内核线程,内核也有自己的任务需要处理,这类内核线程有一部分完全运行于内核空间,它们也有自己的上下文,所以我们把这种状态归纳为运行于内核线程上下文吧。...
  • 线程线程设置多少合适

    千次阅读 2020-06-30 01:15:04
    线程数的设置的最主要的目的是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能,因此让我们一起去探索吧! 首先要考虑到 CPU 核心数,那么在 Java 中如何获取核心线程数? 可以使用 Runtime....
  • 文章目录基本概念上下文原子为什么会有上下文这种概念进程上下文中断上下文进程上下文 VS 中断上下文原子上下文 基本概念 上下文   上下文是从英文context翻译过来,指的是一种环境。相对于进程而言,就是进程执行...
  •  上下文是从英文context翻译过来,指的是一种环境。相对于进程而言,就是进程执行时的环境;  具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。 b -- 原子  原子(atom)本意是...
  • 进程是操作系统的伟大发明之一,对应用程序屏蔽了CPU调度、内存管理等硬件细节,而抽象出一个进程的概念,让应用程序专心于实现自己的业务逻辑既可,而且在有限的CPU可以“同时”进行许多个任务。但是它为用户带来...
  • 在这个运行过程中,线程由 RUNNABLE 转为非 RUNNABLE 的过程就是线程上下文切换。 一个线程的状态由 RUNNING 转为 BLOCKED ,再由 BLOCKED 转为 RUNNABLE ,然后再被调度器选中执行,这就是一个上下文切换的过程。 ...
  • 中断上下文、进程上下文

    千次阅读 2016-03-21 16:28:03
    介绍什么是上下文,为什么要引入上下文。内核的几种运行状态,中断上下文和进程上下文的区别,已经中断一些特有的属性。
  • * Description: spring上下文工具类 * User: zhouzhou * Date: 2018-08-08 * Time: 10:14 */ @Component public class SpringContextUtil implements ApplicationContextAware { private static ...
  • 什么是上下文切换

    千次阅读 多人点赞 2019-04-29 13:46:56
    文章目录什么是上下文切换上下文上下文切换系统调用进程上下文切换线程上下文切换中断上下文切换感谢 上下文 首先,需要讲清楚什么是上下文。 每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,这...
  • 就像名字写那样,过滤器...监听器可以监听,上下文的概念。过滤器:什么是过滤器: servlet规范当中定义的一种特殊的组件,用来拦截servlet容器的调用过程。 会先调过过滤器的方法,过滤器决定是否向后继续调用就是调
  • 线程,到底该设置多少个线程

    千次阅读 2019-06-02 13:26:30
    作者:享学课堂老顾 微信公众号: 享学课堂online 一、前言 “不好了,线上服务器超时严重,请求非常...有个严重误区,以为线程池设置太小了,调大点请求就会快了。 今天就带着小伙伴们沟通一下,线程池的大小应该...
  • 进程上下文与中断上下文的理解

    千次阅读 多人点赞 2015-11-06 17:03:17
    进程上下文:(1)进程上文:其是指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值,进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 95,215
精华内容 38,086
关键字:

当前线程未设置上下文环境