深入浅出:线程底层原理

来源:互联网 发布:4g网络能玩英雄联盟吗 编辑:程序博客网 时间:2024/06/03 13:01

猜大家都很了解线程的使用了,现在我们以java为例,来看看线程是怎样在底层(jvm里面)产生和运行的。
线程控制模块:
当我们构造一个线程,java虚拟机会在内存中生成一个线程控制块,其中包括PC寄存器、Java栈、本地方法栈,这是每个线程独自拥有的,互不干涉。
PC计数器存放当前正在被执行的字节码指令(JVM指令)的地址。说白了,就是PC计数器用来记住这个线程被执行到那一步了(方便下次继续执行)。
Java栈:这个栈中存放着一系列的栈帧(Stack Frame),JVM只能进行压入(Push)和弹出(Pop)栈帧这两种操作。每当调用一个方法时,JVM就往栈里压入一个栈帧,方法结束返回时弹出栈帧。每个栈帧包含三个部分:本地变量数组、操作数栈(操作数栈中存放方法执行时的一些中间变量,JVM在执行方法时压入或者弹出这些变量。其实,操作数栈是方法真正工作的地方,其中我们定义的各种基础数据类型的变量,和对象的引用变量都在操作数栈的内存中储存。当一个函数执行完后,它对栈内存的占用也会被释放,供下一个函数使用)、方法所属类的常量池引用。
本地方法栈:这个栈用来存放本地语言(如C或者C++代码)的方法调用信息,我们知道java是通过在操作系统的基础上虚拟出一层环境(称为JRE)来运行我们的java 程序的,编写操作系统的语言多数时候并不是java(例如windows和linux都是C语言编写的),当程序通过JNI(Java Native Interface)调用本地方法(如C或者C++代码)时,就根据本地方法的语言类型建立相应的栈。

线程的数据共享方式:
所有线程公用的数据区域:Java堆、方法区域、运行常量池。
Java堆中储存的是java中所有被创建出来的对象。操作数栈中的对象的引用(这个类似于C++中的指针),就是指向java堆的。
方法区域是一个JVM实例中的所有线程共享的,当启动一个JVM实例时,方法区域被创建。它用于存运行放常量池、有关域和方法的信息、静态变量、类和方法的字节码。我们在运行java程序之前,jvm会先查找并且加载有关的类,被加载的类就被储存在这里。
运行常量池:这个区域存放类和接口的常量,除此之外,它还存放方法和域的所有引用。当一个方法或者域被引用的时候,JVM就通过运行常量池中的这些引用来查找方法和域在内存中的的实际地址。

线程的各种状态:
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、暂停运行状态(Blocked):暂停运行状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

暂停运行状态的情况分三种:
(1)等待:运行的线程执行wait()或join()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(2)阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)睡眠:运行的线程执行sleep()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时,线程重新转入就绪状态。(网上有部分文章也会把join()和sleep()归为一类,这里为了描述特性方便,把wait()和join()归为一类)。

这里写图片描述
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程的各种状态如图所示,已经非常明了,这里不再累述。

线程的各种操作:
线程的挂起(又称为等待),和进程相类似,线程在运行中也同样会有挂起的状态,挂起是指进程控制模块,在设备资源(比如说cup计算资源,内存资源)快要用尽的时候,为了节省设备资源,把一部分不太重要的线程从内存转移到外存去(这时候,这些线程就不能运行了)。

1.被线程A占用的对象H,可以发出请求,使线程A进去挂起状态,在java中,可以使用对象中的object类下的wait(),(因为所有类都继承自object类,所以任何对象都有此方法,也就是说java中的任何一个对象都自带一个同步锁) 线程A在进入等待以后,需要notify()方法才能被唤醒,才能继续执行。
2.可以使用Thread类下的join()方法,以及等待Lock或Condition。这一实现方法会让被等待的线程一直被挂起,直到调用这一方法的线程执行完毕(线程死亡),例如:线程A运行Thread.join(),挂起了线程B,这时候,这时候线程B必须在A的run()函数完全执行完以后,才能再次开始执行。
阻塞:
1.在某一个线程尝试获取某一个资源(比如一个对象的对象锁),而该锁被其他线程持有,这个线程暂时没有办法继续执行,所以CPU不再分配时间片给它(但是它的内存并不释放),来加速程序运行。

这里写图片描述
挂起和阻塞的比较:
挂起一般是程序的主动行为,由系统或程序发出,甚至于辅存中去。(不释放CPU,可能释放内存,放在外存);阻塞一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待某种资源或信号量(即有了资源)将他唤醒。(释放CPU,不释放内存)。
线程的挂起和释放,需要显式调用wait和notify方法;阻塞和释放则是java的虚拟机自动完成的。不需要程序员去处理

睡眠:
睡眠是线程主动请求暂停执行一段时间(这段时间里,进入阻塞状态的,释放CPU,不释放内存),多数时候线程之所以加入这个行为,主要是为了减轻当前线程对CPU的负荷,

让步:
例如java 中的yield(),它也可以让当前执行的线程暂停,但它不会阻塞线程,只是将该线程转入到就绪状态。 yield()只是让当前线程暂停下,让系统线程调度器重新调度下。系统线程调度器会让优先级相同或是更高的线程运行。
死亡(中断):
死亡(中断)以为着线程的结束,这个线程出现了致命异常,无法继续执行,然后线程会自动死亡。

程序中睡眠、阻塞、挂起的区别形象解释:
首先这些术语都是对于线程来说的。对线程的控制就好比你控制了一个雇工为你干活。你对雇工的控制是通过编程来实现的。
挂起线程的意思就是你对主动对雇工说:“你睡觉去吧,用着你的时候我主动去叫你,然后接着干活”。
使线程睡眠的意思就是你主动对雇工说:“你睡觉去吧,某时某刻过来报到,然后接着干活”。
线程阻塞的意思就是,你突然发现,你的雇工不知道在什么时候没经过你允许,自己睡觉呢,但是你不能怪雇工,肯定你这个雇主没注意,本来你让雇工扫地,结果扫帚被偷了或被邻居家借去了,你又没让雇工继续干别的活,他就只好睡觉了。至于扫帚回来后,雇工会不会知道,会不会继续干活,你不用担心,雇工一旦发现扫帚回来了,他就会自己去干活的。因为雇工受过良好的培训。这个培训机构就是操作系统。

针对对象而言:
对象在线程中的应用除了同步锁以外,还有一种应用——对象object中的这个synchronized(this)同步代码块,这种代码块类似于 python中的lock的使用方法:
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
简单的说,synchronized(this)同步代码块内的内容被视为一个整体,java的一个线程在执行代码块的过程中,必须执行完同步代码块内的所有内容(或遇到obj.wait()被挂起),才可以释放对象锁。
综上:
某个对象的池锁里的线程若获得对象锁,那么会继续执行wait()之后的代码,要想其释放对象锁,只有以下几种情况:
1. 执行完同步代码块。
2. 线程终止。
3. 对象的wait()方法被调用。

线程的执行顺序:
操作程序运行过程中,每个线程都会希望获得CPU的控制权,那么它们执行的先后顺序要怎么排列呢?怎么保证同级别的线程中JVM不会“厚此薄彼”呢?又怎么保证重要的线程能获得更多的执行时间呢?
这其中有很多种已经比较成熟调度线程的算法:例如先来先服务和短作业优先算法,高优先权调度算法,时间片轮转调度算法。
这里以JVM为例介绍一种控制线程的方法:高优先权调度算法。
先来熟悉一个概念:线程队列
JVM会把一个程序中所有的线程按照启动顺序先后分配内存,并且创建线程控制模块(参看文章开头),然后会给每一个线程创建一个引用(其作用类似于指针),并存入一个设置好的队列(一种数据结构,特点是先入先出。可参看《数据结构与算法》),于是乎所有的线程都会有序的去排队等待获得CPU的运行时间片(如果没有特殊设置,每个线程获得的时间片是相等的)。如果一个线程在自己拥有的时间片中完成(即run()方法执行结束。线程死亡)则撤出JVM,同时释放线程控制模块;如果没有完成,则这个线程会被从新放入队列的尾部,等待再次被调用。
高优先权调度算法就是基于这种“排队,平均分配”的思想来调度线程的,但是为了保证短作业可以优先完成,重要作业可以优先被执行的实际需要。
高优先权调度算法实现如下:

这里写图片描述
如上图,按照优先级,会分别建立多个队列,每个线程按照先来先排队的原则分别进入相应优先级的队列,然后CPU会先执行优先级最高的队列,然后依次类推(执行顺序如图中的数字序号)图中当1号线程在自己的时间片中没有执行完成时,它会被放入优先级3的队列的末尾,等待被再次执行,如果下次执行还是没有完成,则会被放入优先级2的队列的末尾,(其他线程也以此类推)。
在时间片的分配上:低优先级的线程的时间片会比高优先级的时间片长(对,你没有听错)。例如,优先级2的队列,时间片长度是优先级3的两倍,是优先级4的四倍(以此类推)。这是为了保证,高优先级的和短作业的任务会被优先执行。
嗯,本次就先分享这么多吧,如果本文有错误疏漏可以在下方留言指正,作者QQ:823811845。

阅读全文
0 0