关于线程 thread (1)概念简介

文章参考:
Java线程详解,写的很好

一 认识线程

什么是线程

线程是进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能够独立运行的基本单位。线程自己本身不拥有系统资源,只拥有一点在运行中必不可少的资源(比如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另外一个线程,同一进程中的多个线程可以进行并发执行。线程有就绪,阻塞和运行三种状态。

这里提到了一个叫进程的东西。尽管简单都知道,但是还是写一下吧! 进程指的是运行中的应用程序,每个进程都有自己的独立的地址空间,也就是内存空间。例如用户在桌面上点击了IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击左面的IE浏览器,就又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。

2 硬件线程与软件线程

http://www.uedsc.com/reading-thread.html

多核微处理器带有一个以上的物理内核,物理内核是真正独立的处理单元,多个物理内核使得多条指令能够并行的运行。为了充分发挥物理内核的功效,有必要运行多个进程,或者一个进程里运行多个线程,创建多线程的代码。

然而每个物理内核都可能会提供多个硬件线程,也就是逻辑内核或者逻辑处理器。使用了超线程技术的微处理器在每一个物理内核上提供了多分架构状态,比如很多带有4个物理内核并且使用了超线程技术的微处理器在每一个物理内核上提供两份架构状态,从而获得了8个硬件线程。这种技术称为对称多线程,他通过额外的架构状态在微处理器的指令级别对并行执行进行优化和增强,对称线程技术并不局限于每个物理内核两个硬件线程,有的是4个。所以可以看出,一个硬件线程并不代表一个物理内核。某些情况下,对称线程技术可以提升多线程代码的性能。

在操作系统,比如windows中,每个运行的程序都是进程。每一个进程中都运行了至少一个线程,这种线程被称为是软件线程,用于区分前面提到的硬件线程。一个进程至少有一个线程,而且必有一个主线程。操作系统的调度器在所有要运行的线程之间公平的分享可用的处理资源。Windows的调度器会给每一个软件线程分配处理时间。当windows调度器运行在多核微处理器上时,调度器必须从物理内核支持的硬件线程中分配时间给每一个需要运行指令的软件线程。打一个比方,可以吧每一个硬件线程想象成一个泳道,而软件线程则是上面的游泳者。并且有个调度器还专门负责这个游泳者要在什么时候在泳道上游泳,什么时候在泳道上停住。

Windows将每一个硬件线程识别为一个可调度的逻辑处理器。每一个逻辑处理器可以运行软件线程的代码。运行了多个软件线程的进程可以充分发挥硬件线程和物理内核的优势,并行的运行指令。

每一个线程都与其父进程分享一个私有的唯一的内存空间。但是每一个软件线程都有自己的栈, 寄存器 和 私有局部存储区域。

windows调度器可以决定将一个软件线程赋给另一个硬件线程,通过这种方式均衡每一个硬件线程的负载。由于通常都有其他软件线程在等待处理时间,因此,负载均衡机制可以合理的组合有效的资源,让其他软件也有机会执行各自的指令。

负载均衡简略说明:他指的是将软件线程的任务分发在多个硬件线程里去操作,通过负载均衡,工作负载可以公平的分配在硬件线程之间。然而是否能够完美的实现负载均衡取决于应用程序的并行程度,工作负载,软件线程数,可用的硬件线程以及负载均衡策略。

多线程的终极目的就是最大限度的利用CPU资源,记住是充分利用CPU资源,而达到的用户使用上,感觉很好很快。而不是提高cpu的运行效率。运行效率这东西,除非你换一个CPU吧,,估计没有多少折儿。 所以把能够把多线程用好,决定了你开发的程序,在同一个机子上,比其他人开发的是不是用起来更好使更快。

软件线程的调度

上面说的内容涉及到了线程调度这个词,严格的说是针对软件线程的调度。下面来看看软件调度是什么。

由上面硬件线程与软件线程的解释可以看出来,你甚至可以把硬件线程就当成一个实物吧(尽管不合理,但是好理解),cpu内核的四分之一或者是八分之一,甚至你可以把主板啥的拆开反正就是看得到就是啦,至于里面的原理忽略。他们由于操作系统的调度器的调度,可能有些时候有的部分会歇着不干活来省电。你可以把硬件线程当做是逻辑处理器。我们所说的软件线程就是靠这些逻辑处理器来运行的。而软件线程你就可以当成一个基本单位,也可以当成一个任务,反正我对这里单位的解释感觉很疑惑,怎么就是单位了要使得cpu去运行这些逻辑起码你得是个线程,至少你有一个线程我才可以运行,否则,你连个整儿(都不算是一单位)的都不算,无法运行!嗯,一个硬件线程可以运行(处理线程中的逻辑)不止一个软件线程,并且硬件线程的调度器这位大管家还可以控制软件线程该在哪个硬件线程上执行。

那既然这样的话,一般一个进程里面会有1到n个不等的线程。假设一个进程开了32个线程,然后实际上的硬件线程也就8个,,要如何整,平均每个硬件线程要运行四个,要怎么搞?国家包分配哈哈哈,这就引出了线程调度了

一个进程里面32个线程,事实上现实生活中真的有不少进程真的就有这么多线程。那么为什么要有多线程呢?想象一个场景吧,假设你的手机,而且渣渣处理器只有一个硬件线程,,然而额你有一个开发工作需求,让你实现看电视的同时还下载,,你总不能只用一个软件线程搞吧,如果一个软件线程,会有什么样的下场?要么先下载文件,要么先看电视?不符合需求啊这样!所以把如果只用一个线程就只有懵逼了。那么我就开两个线程吧,一个用来看视频,一个用来下载文件,那么这两个线程就得运行在同一个硬件线程上,试想,如果你不对这两个线程进行调度的话,,那不还是先运行完a线程的逻辑再运行b的逻辑么?这不还是相当于你先干这个后干那个么。这样很明显是不行的!所以一定要有一套东西来控制这两个线程,要怎么走,,能够模拟出我同时走着a的逻辑和b的逻辑这种假象。起码效果是两者都在进行,好在处理器的运行速度贼快,我要不就一会儿运行一下a, 然后再过一会儿再运行b,交替着运行,,这样在宏观上看起来就像是两个线程同时在运行了,反正处理器运行速度快,任性,就频繁的切切切,人眼感知不到障眼法的哈哈哈,这种策略可以说就是一个easy的线程调度。

线程调度的模型(怎么个调度法儿):

  • 分时调度模型
  • 抢占式调度模型

分时调度:指让所有的线程轮流获得cpu的使用权,而且平均分配每个线程占用的cpu时间片。
抢占式调度:指有限让可运行池中优先级高的线程占用cpu,如果可运行线程池的优先级相同的话,就随机选择一个线程,使其占用cpu。处于运行状态的线程会一直运行,直至它不得不放弃cpu。 (什么叫不得不放弃?)

“不得不放弃”的解释
一个线程会因以下原因放弃cpu:

  1. java虚拟机让当前线程暂时放弃cpu,转到就绪状态,使其他线程获得运行的机会。 (额,,这不就像调度器么)
  2. 当前线程因为某些原因而进入阻塞状态(这里的某些原因有很多种暂时不管)
  3. 线程结束运行了。

需要注意的是: 线程的调度不是跨平台的,它不仅仅取决于java虚拟机,还依赖于操作系统。在某些操作系统中,只要运行中的线程没有遇到阻塞,就不会放弃cpu,在某些操作系统中,即使线程没有遇到阻塞,也会运行一段时间后放弃cpu,给其他线程机会。

在java里面,他的线程调度采用的是抢占式调度模型,不是分时的,同时启动多个线程后,不能保证每个线程轮流并且均等的获得cpu时间片。如果希望明确的让一个线程给另外一个线程运行的机会,可以采取下面的方法之一:(这些现在不懂没关系,接下来会讲)

  1. 让处于运行状态的线程调用 Thread.sleep();
  2. 让处于运行状态的线程调用Thread.yield()方法
  3. 让处于运行状态的线程调用另一个线程的join()方法

二 了解线程

线程对象与线程之间的区别

第一要搞清楚的问题,如同程序和进程的区别,要了解线程,多线程,第一个要搞清楚的问题就是,线程对象与线程之间的区别。

线程对象是可以产生线程的对象,比如java上的thread,Runnable对象。而线程,指的是正在执行的一个指令点序列。在java平台上的指的是从一个线程对象的start()开始,运行run()方法体中的那一段相对独立的逻辑的过程。

那么先从最简单的开始,咱们的main方法

public static void main(String[] args) {
	for(int i = 0; i < 100; i ++) {
		System.out.println(i + "");
	}
}

如果成功编译了该java文件,然后在命令上敲入运行这个文件的指令。那么就会发生:

  1. JVM进程被启动,在同一个JVM进程中,有且只有一个进程,就是他自己。
  2. 然后在这个JVM环境中,所有程序的运行都是以线程来运行的。JVM最先会产生一个主线程,由它来运行指定程序的入口点。在上方的代码中,这个入口就是main方法。当main方法结束后,主线程运行完成,JVM也就会随之退出。
  3. 我们看到的是一个主线程在运行main方法,这样只有一个线程执行程序的流程我们称之为单线程。这是JVM提供给我们的单线程环境,事实上,JVM底层至少还有垃圾回收这样的后台线程以及其他的非java线程,但是这些线程对我们而言是不可访问的,所以我们就把它认为是单线程就可以。并且主线程是JVM自己开的,在这里并不是由线程对象产生的。在这个线程中,它运行了main方法里面的逻辑
    ​​​​​​​​​​​​​​​​​​

线程的生命周期(这是基础)

线程是有周期的。当线程被创建并启动后,它既不是一启动就进入执行状态,也不是一直处于执行状态。线程是要经历一个生命周期的, 创建,就绪,运行,阻塞, 死亡。5种状态。尤其是当线程执行后,不可能一直霸占着cpu独自运行的,毕竟还有线程调度,就算线程想这么做,调度机制也不允许,通常进程中是有多线程的,所以cpu需要在多条线程之间切换,于是线程的状态也会多次在运行–>阻塞–>就绪–>运行–>阻塞–>就绪–>运行 中来回切换。下面简述一下这几种状态。
在这里插入图片描述

  • 新建状态:当程序使用new关键字创建了一个线程之后,他就立马进入了新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。
  • 就绪状态:当线程对象调用了start方法之后,该线程就进入了就绪状态。java虚拟机会为其创建调用栈和程序计数器,等待调度运行。 (线程持有的三个东西, 调用栈,计数器,和寄存器)
  • 运行状态:如果处于就绪状态的线程获得了cpu,开始执行run方法的线程执行体,则该线程就处于运行状态。
  • 阻塞状态:当处于运行状态的线程失去所占用的资源后,就进入了阻塞状态。有一下几个原因会导致线程处于阻塞的状态
    1. 线程自己调用了sleep(),进入了睡眠操作
    2. 线程执行一段同步代码,但是尚且无法获得相关的同步锁,只能进入阻塞状态,等到获取了同步锁,才能恢复执行(这个 我不太理解)
    3. 线程执行了一个对象的wait()方法,直接进入阻塞状态,等待其他线程的notify()或者notifyAll()
    4. 线程执行了某些io操作,因为等待相关资源而进入了阻塞状态。比如监听system.in这个输入,但是尚且没有键盘输入,就进入了阻塞的状态。
  • 死亡状态:当run方法正常退出或者一个未捕获的异常终止了闰方法而使线程猝死。就进入死亡状态了

新建和就绪状态:

当程序使用new关键字创建一个线程之后,该线程就处于新建状态,此时他和其他的java对象一样,仅仅由java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现任何线程的动态特征,程序当然也不会执行线程。

当线程对象调用了start()方法之后,该线程就处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有被运行,只是表示该线程可以运行了,至于什么时候开始运行,那就得看JVM线程调度器的调度。就是听天由命吧。。

(注意:启动线程一定要用start()方法,而不是run()方法。永远不要调用线程的run方法,调用start方法,系统就会把run方法当成线程执行体来处理,但是如果直接调用线程对象的run方法,则run方法就相当于一个普通方法,立即执行,并且在run方法返回之前其他线程无法并发执行。需要指出的是,调用了线程的run方法之后,该线程就已经不是新建状态了,所以不要再师徒调用线程的start方法,start方法只是针对新建状态的线程用的,用于把它转为就绪状态,随时等待cpu。如果执意执行st则会爆出IllegalThreadStateException的异常。)

调用了线程对象的start方法之后,该线程立即进入就绪状态–就绪状态相当于等待执行,但是该线程并没有真正的进入运行状态。如果希望调用子线程start方法后子线程立即执行的话,程序可以使用Thread.sleep(1)方法,让当前运行的线程,也就是主线程睡眠1毫秒,很快的,但是这一毫秒之内cpu是不会闲着的,他会去执行另一个处于就绪状态的线程,那么很明显就是刚刚start的线程,这样子线程就能立即执行了。

运行和阻塞状态

如下情况下,线程将会进入阻塞状态:

  • 自己调用了sleep自行阻塞的
  • 线程想获得一个同步锁,但是这个锁被别的线程持有时时不释放
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  • 线程在等某个通知 notify
  • 程序调用了线程的suspend方法将线程挂起,但这个方法易引起死锁,应当尽量避免使用该方法。

当正在执行线程被阻塞之后,其他线程就可以获得执行机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也会是说,被阻塞的线程的阻塞解除后,必须等待线程调度器重新调用它。

解除阻塞的方式

针对上面的几种情况当发生如下特定情况可以解除上面的阻塞,让线程重新进入就绪状态:

  • 调用sleep导致的阻塞,经过了指定的时间之后就可以进入就绪状态
  • 采用阻塞式IO方法导致的阻塞,等那个方法返回就解除阻塞了。
  • 线程成功的获得了试图得到的同步锁。(同步监视器)
  • 线程正在等待通知的时候,其他的线程发来了这个通知。
  • 调用线程的suspend()方法将线程挂起导致的阻塞,被调用了resdme()方法。
    在这里插入图片描述
    从图中可以看出来
  1. 当一个线程处于阻塞状态的时候,他的走向只能是变成就绪状态。无法直接变成运行状态的。而就绪状态和运行状态之间的转换通常是不受程序控制的,而是由系统线程调度所决定。
  2. 当处于就绪状态的线程获得了处理器处理权之后就进入运行状态
  3. 当运行状态的线程失去了处理器权(阻塞状态了)该线程就会进入阻塞状态! 但是有一个方法有例外,调用yield()方法的话,可以让运行状态的线程直接转到就绪状态。稍后讲yield()这个方法。

线程死亡

线程会以以下三种状态结束,结束后就处于死亡状态:

  • run() 方法 或者call() 方法执行完成,线程正常结束。
  • 线程抛出了一个没有捕获的异常
  • 直接调用该线程的stop()方法来结束该线程,该方法很容易导致死锁,通常不推荐使用。什么是死锁?

线程生命周期简单例子

重复start() 线程引发的惨剧

当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来之后,他就拥有和主线程相同的地位,他不会受主线程的影响。为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪,阻塞,运行状态的时候就会返回true,当处于新建和死亡状态的时候就会返回false。0并且不要试图对一个已经死亡的线程调用start方法使他重新启动,死亡就是死亡,该线程将不可再次作为线程执行。强制start会抛异常

     /**
	 * 测试当一个线程死亡的时候,如果再开start方法,可行吗?
	 */
	public void testRestarThread(){
		Thread1 thread1 = new Thread1();	//此时线程是新建状态
		for (int i = 0; i < 300; i++) {
			System.out.println(Thread.currentThread() + "------ i = " + i);
			if (i == 20) {
				thread1.start();//线程处于就绪状态
			}else if (i > 40){	//这个情况下,估计thread1 已经死掉了
				if (!thread1.isAlive()) {	//当线程为就绪,运行, 和阻塞状态的时候这个判断方法会返回true
					thread1.start();//如果线程死了,就试图用这个不合理的方法开
				}
			}
		}
	}
class Thread1 extends Thread{
	public void run() {
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread() + "");
		//运行完run里面的逻辑就算是走完了,线程结束,死亡
	}

}

运行结果:

Thread[main,5,main]------ i = 0
Thread[main,5,main]------ i = 1
Thread[main,5,main]------ i = 2
Thread[main,5,main]------ i = 3
Thread[main,5,main]------ i = 4
Thread[main,5,main]------ i = 5
Thread[main,5,main]------ i = 6
Thread[main,5,main]------ i = 7
Thread[main,5,main]------ i = 8
Thread[main,5,main]------ i = 9
Thread[main,5,main]------ i = 10
Thread[main,5,main]------ i = 11
Thread[main,5,main]------ i = 12
Thread[main,5,main]------ i = 13
Thread[main,5,main]------ i = 14
Thread[main,5,main]------ i = 15
Thread[main,5,main]------ i = 16
Thread[main,5,main]------ i = 17
Thread[main,5,main]------ i = 18
Thread[main,5,main]------ i = 19
Thread[main,5,main]------ i = 20
Thread[main,5,main]------ i = 21
Thread[main,5,main]------ i = 22
Thread[main,5,main]------ i = 23
Thread[main,5,main]------ i = 24
Thread[main,5,main]------ i = 25
Thread[main,5,main]------ i = 26
Thread[main,5,main]------ i = 27
Thread[main,5,main]------ i = 28
Thread[Thread-0,5,main]
Thread[main,5,main]------ i = 29
Thread[main,5,main]------ i = 30
Thread[main,5,main]------ i = 31
Thread[main,5,main]------ i = 32
Thread[main,5,main]------ i = 33
Thread[main,5,main]------ i = 34
Thread[main,5,main]------ i = 35
Thread[main,5,main]------ i = 36
Thread[main,5,main]------ i = 37
Thread[main,5,main]------ i = 38
Thread[main,5,main]------ i = 39
Thread[main,5,main]------ i = 40
Thread[main,5,main]------ i = 41
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Unknown Source)
	at Thread.ThreadDemo.testRestarThread(ThreadDemo.java:15)
	at Thread.ThreadDemoControlTool.main(ThreadDemoControlTool.java:10)

抛异常了吧,start不能随便开的。

线程优先级

JVM线程调度采用的是基于优先级的抢占式调度机制, 线程总是存在优先级,并且优先级的范围是在1到10之间。在大多数情况下,当前线运行的线程优先级将大于或者等于线程池的任何线程的优先级。但这仅仅是大多数的情况。

注意:当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作时没有保障的,只能把线程的优先级作为一种提高程序效率的方法,但是要保证程序不依赖于这种操作。

当线程池中的线程丢都具有相同的优先级,调度程序的JVM实现自由选择它喜欢的线程。这时候的调用程序的操作方式可能有两种,一种是选择一个线程,直到它阻塞或者运行完成为止。二是分时间片,为线程池的每个线程提供均等的机会。

设置线程的优先级:线程默认的优先级是跟随创建它的线程的优先级,也就是谁带出来的就随谁。我们可以通过setPriority(int priority)更改线程的优先级。例如:

Thread t = new MyThread();
t.serPriority(8);
t.start();

线程的优先级是1到10之间的整数,JVM不会改变一个线程的优先级。然而1到10之间的值是没有保证的。一些JVM可能不能识别10个不同值,而将这些优先级进行每两个或者多个进行合并,变成少于10个优先级,则两个或多个优先级的线程肯能被映射为一个优先级。线程默认的优先级是5,Thread类中有三个常量,定义线程的优先级范围。

 static int MAX_PRIORITY //线程可以具有最高的优先级
static int MIN_PRIORITY //线程具有最低的优先级
static int NORM_PRIORITY //分配给线程的默认优先级

猜你喜欢

转载自blog.csdn.net/weixin_28774815/article/details/88742065