目录
1.4.为什么说线程是操作系统调度的执行单位?而进程是资源分配的基本单位。
2.5.使用lambda表达式,创建一个新线程(推荐写法)。
2.6.isDaemon()方法简称 是否后台线程 ,又称守护线程,有后台线程就一定有前台线程,两者的区别在于:
2.8.在上一个展示代码中,在线程执行的时候,通过boolean型flag变量就能控制线程的运行,而在我们Thread类中是用isInterruped()方法来控制,来阻断线程的运行的。
3.3.为什么说当我们的pcb释放了,但是我们的Thread对象还在呢?
铁汁们准备好了嘛,我们要开始学习多线程了哦,发车>>>>
认识线程:
在生活中我们一般都会一边听歌一边搜索歌曲,而这种方式的实现很大程度上就采用了我们今天要学习的多线程编程原理。
1.1.理解:并发与并行
并发:只两个或多个事件在同一个时段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
线程中并发指:两个或多个事件,在一个核心上同时执行。
线程中并行指:两个或多个事件,在不同的核心上同时执行。
注意:
我们要理解的在操作系统中,并发指的是在一段时间内宏观上有多个程序同时执行,单核cpu下,每一时刻其实是只能执行一个程序的,但是在微观上来说这些程序是交替运行的,因为交替的运行的时间非常短,所以会让我们感觉到是同时运行的。
在多核cpu下,我们这些并发执行的事件就能分配到多个核心上,进行多任务并行执行,利用多个核心来执行我们并发执行事件,总而言之,多核处理器,核越多越好。好处就是提高电脑运行速率。
当然我们单核处理器上是不能进行并行运行的,并行运行只能在多核处理器上进行。其实我们的线程也是这样的,从宏观角度上看线程是并行执行的,但是从微观角度上理解的话,线程其实串行的,一个一个去执行的,,当系统只有一个cpu时,线程会以某种顺序执行多个线程,我们把这种情况称为线程的调度。
1.2.进程和线程
进程:是指内存中运行的某个程序,就每个进程它是有一个独立的内存空间的,一个应用程序可以同时运行多个进程,Google 浏览器应用的就是多进程原理。进程也是系统运行程序的基本单位;一个程序即一个线程的运行就是从创建到运行 消亡的过程。
线程:线程是进程中的执行单元,线程也是操作系统中调度执行的基本单位。一个线程是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码,多个线程“同时”执行多份代码。并且一个进程包含一个或多个线程。
1.3.进程和线程之间的区别与联系
1.一个进程可以包含多个线程,线程在进程的内部。
2.进程之间的资源是独立的,线程之间的资源则是共享的。
每个进程都有独立的虚拟地址空间,也有之间独立的文件描述符表,同一进程的多个线程之间则共用这一份虚拟地址空间和文件描述符表。
3.进程是操作系统中资源分配的基本单位,线程是操作系统中调度执行的基本单位。
4.多个进程同时执行时,如果一个进程崩溃,一般不会影响其他进程,而同一进程内的多个线程之间,如果一个线程崩溃,很可能使得整个进程崩 溃。
5.进程的上下文切换速度比较慢,而线程的上下文切换速度比较快。
6.进程的创建/销毁/调度开销大,线程的创建/销毁/调度开销相对少很多。
1.4.为什么说线程是操作系统调度的执行单位?而进程是资源分配的基本单位。
首先我们得认识到线程是包含在进程内部的,一个线程的创建必须是基于一个进程的存在,而进程的存在第一要素就是操作系统要为一个进程分配一个虚拟空间,在这个虚拟空间的基础上才因运而生的我们的线程,所以说进程是操作系统资源分配的基本单位,而线程是操作系统中调度执行的基本单位 。
线程调度
分时调度:
所有线程轮流使用cpu的使用权,平均分配每个线程占用的cpu的时间,这其实在代码串行中可以感受到,按顺序执行。
抢占式调度:
线程在系统中通过竞争来获得cpu的使用权,谁先拿到这个使用权谁就先执行,在我们所学的java中就是使用的抢占式调度。
抢占式调度的理解:
我们在使用电脑的时候,一般会同时打开很多软件,此时会让我们感觉到这些程序这些软件好像都在同一时刻运行。实际上,是cpu使用抢占式调度模式在多个线程之间来回切还,而造成我们觉得这些软件是同时运行的感觉,就是cpu切还的速度非常快。当我们在切还这些软件的时候会有卡顿或者加载不出界面,这个时候就是cpu没有反应过来,等一等就好了。其实,多线程程序提高扥是程序运行的速率,让cpu使用率更高。
五种常见创建线程的基本方法
2.1.继承Thread,重写run方法。
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello world");
//为了让这里打印慢点,方便看,加个sleep,休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.2.实现Runnable类,重写run方法。
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello thread");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
//上面的run方法只是来描述我们的线程中需要干什么的
//真正让我们的线程跑起来的还是下面这位老铁
thread.start();//这个是用来启动线程的,没这个线程走不了
}
}
2.3.使用匿名内部类,继承Thread 。
public class ThreadDemo4 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
thread.start();
}
}
2.4.实现匿名内部类实现Runnable。
public class ThreadDemo4 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
thread.start();
}
}
2.5.使用lambda表达式,创建一个新线程(推荐写法)。
public class ThreadDemo5 {
public static void main(String[] args) {
Thread thread = new Thread(() ->{
System.out.println("hello");
});
thread.start();
}
}
start和run之间的联系和区别
start才是真正在系统里面创建一个线程,线程是一个独立的执行流哦。
run只是在描述线程中要做的事情。如果我们在main方法中直接调用run ,此时是没有创建线程的,全是main线程一个人在做事情。
当执行完start后,创建出来的新线程才能执行run中的任务描述。(一定要搞清楚start和run之间的区别)。
了解以上知识其实还是很难感受到,多线程的优势在哪!所以在下面的展示代码中,我们可以感受一下多线程的优势在哪!
我们定义一个concurrency()方法和一个serial()方法,前者用并发的方式在线程内计算a,b的值,而后者用串行的方式计算a,b的值。
package TestDemo;
public class TestThread {
private static final long count = 10000_000_00;
private static void concurrency() throws InterruptedException{
long begin = System.nanoTime();
//利用一个线程计算a的值
Thread thread = new Thread(()->{
int a = 0;
for(long i = 0; i < count; i++){
a--;
}
});
thread.start();
//主线程类计算b的值结束
int b = 0;
for(long i = 0; i < count; i++){
b--;
}
//等待线程thread
thread.join();
long end = System.nanoTime();
double ms = (end-begin)*1.0/1000/1000;
System.out.printf("并发:%f 毫秒%n" , ms);
}
public static void serial(){
//全部在主线程类计算a,b的值
long begin = System.nanoTime();
int a = 0;
for(long i = 0; i < count; i++){
a--;
}
int b = 0;
for(long i = 0; i < count; i++){
b--;
}
long end = System.nanoTime();
double ms = (end-begin)*1.0/1000/1000;
System.out.printf("串行:%f 毫秒%n" , ms);
}
public static void main(String[] args) throws InterruptedException {
//并发
concurrency();
//串行
serial();
}
}
从上面代码执行的时间来看的话,其实多线程在系统中的优势就是提高程序运行速率,让cpu使用率更高。
拓展:
Thread类的几种常见用法解析
Thread() ---->创建一个线程对象
Thread(Runnable target) ---->使用Runnable创建线程对象
Thread(String name) --> 给对象起一个名字
Thread(Runnable target,String name) --->使用Runnable创建线程对象 并命名
认识Thread的几个常见属性
getID -->获取你的Id名称
getName() --->获取为构造法起的名字
getState() --->查看线程在操作系统的状态
getPriority() ----->这个就是设置线程执行顺序的优先级的(但是这个设置了也没啥用)!
isDaemon() ---->是否后台线程
isAlive() ---->获取线程存活状态
isInterrupted ---->中断线程的操作。
重点讲解isDaemon(),isAlive(),以及isInterruped()方法在线程中的运用
2.6.isDaemon()方法简称 是否后台线程 ,又称守护线程,有后台线程就一定有前台线程,两者的区别在于:
前台线程:前台线程没执行完一般会阻止进程的结束,在代码中手动创建的线程都属于前台线程(main方法也是默认的前台线程)。
后台线程(守护线程):通过手动使用setDaemon()方法设置成后台线程,后台线程是不会阻止进程的结束,就是后台线程工作没做完,进程也是可以结束的。
public class ThreadDemo{
public static void main(String[] args){
Thread thread = new Thread(()->{
while(true){
System.out.println("hello");
}
},"mythread");//为线程命名
//手动设置为后台线程
thread.setDaemon(true);
//此时进程的结束与否就和thread无关了
thread.start();
}
2.7.isAlive()方法就是在判断这个线程到底存不存在,在start之前调用它,会返回false状态,在创建start的后调用它,会显示true状态,但是在线程结束后调用它,会再次显示 false。
public class ThreadDemo{
private static boolean flag = false;
public static void main(String[] args){
Thread thread = new Thread(()->{
while(!flag){
System.out.println("hello");
try{
System.out.println(is.Alive());//此时此刻 一定是true
}catch (InterruptedException e){
e.printStackTrace();
}
}
},"mythread");
//手动设置为后台线程
//thread.setDaemon(true);
//此时进程的结束与否就和thread无关了
thread.start();
Thread.sleep(3000);
flag = true;
while(true){
try{
Thread.sleep(1000);
System.out.println(is.Alive());//线程结束了 结果是false
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
在展示代码中,我们可以看到,线程在不同阶段,调用is.Alive()会返回不同的状态结果,这就是isAlive()存在的价值。
2.8.在上一个展示代码中,在线程执行的时候,通过boolean型flag变量就能控制线程的运行,而在我们Thread类中是用isInterruped()方法来控制,来阻断线程的运行的。
public class ThreadDemo{
public static void main(String[] args){
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterupped()){
System.out.println("hello");
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
//break;//猜猜为什么加这一行代码
}
}
});
thread.start();
Thread.sleep(3000);
thread.isInterrupt();
}
}
其实isInterrupted()方法中就是封装了一个Boolean变量,也是在进行true/false 的判定。而上面的currentThread()方法是Thread中的一个静态方法,哪个线程调用它,它就代表那个对象。最后在调用isInterrupt()后,你会发现执行结果中出现了异常提醒,但是还没有结束程序的运行,为啥?因为在调用isInterrupt()后,他会把封装的Boolean型变量改为true,传递给线程,提示它中断线程,但同时他会唤醒异常中sleep,导致sleep提前返回,当sleep提前返回后 他会重新把标志位设置为false(就是刚刚isInterrupt()改的Boolean型变量),所以代码依然会继续运行。我们可以看到我在catch()语块中最后加了一个break;语句,其实这个语句的作用就是来终止线程不再运行。
注意:我们在理解isInterupted()的时候,你可以认为他,只传递信息,至于信息传过去有没有用,会导致什么结果,它就不管了,所以,我们后面代码的执行和停止很大程度上还是取决于开发者自己。
在展示的代码中 出现了Thread.currentThread()这个方法的调用,那么这是个啥东西,其实他就是一个返回当前线程对象的引用,就像我们之前this关键字一样,你调用的那个对象,他就代表那个对象。并且他还是Thread中的一个静态方法,所以是可以直接用Thread调用的。
public static Thread currentThread();
那么我们在线程中还有没有类似像isInterrupted()这样的方法了!
有哦!
就是Thread中的等待线程 -- join(),它是干嘛的?我们都知道线程是一个随机调度的过程,等待线程做的事情就是控制两个线程的执行顺序。
public class ThreadDemo{
public static void main(String[] args){
Thread thread = new Thread(()->{
System.out.println("hello");
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
System.out.println("join执行之前");
thread.start();
//此时此刻join就是让当前的main线程 来等待thread线程执行结束(就是等待thread中run 执行完)
try{
thread.join();
}catch (InterrupedException e){
e.printStackTrace();
}
System.out.println("join执行之后)");
}
}
执行完start后,main线程和thread线程应该一起并发执行,但是有了join()的加入,就会发生阻塞(block),一直阻塞到thread线程执行完后,main线程才会从join()中苏醒过来,这个时候main才能执行。(thread线程一定是先比main线程先结束的)。
等待线程不仅是可以用在主线程和thread线程之间的,还可以用在thread1线程和thread2线程之间,他同样是能够控制两个线程的执行顺序。
注意:假设开始执行join()的时候,thread已经结束了,此时join不会阻塞了,会立即返回。
拓展:join()的几种属性
public void join() --无参数版本,一直等到前面的线程执行完才会有响应。
public void join(long millis) --指定时间版,到时间自动解除阻塞(推荐使用)
public void join(long millis,int nanos)
刚刚我们学习到了等待线程的基本用法,在线程中还有一个休眠线程,让线程短暂的进入阻塞状态,因为线程是不可控制的,而休眠线程的最主要的作用就是让当前执行的线程在某个线程之前的先执行一部分时间。这组方法运用的比较多,我们一定要记住他。
代码展示:
public class TestDemo{
public static void main(String[] args){
Thread thread = new Thread(()->{
System.out.println("thread");
thread.start();
try{
//等待五秒中
Thread.sleep(5000);
}catch (InterruptedException e){
e.printStackTrace();
}
});
System.out.println("main");
}
}
休眠线程的几种常见属性:
public static void sleep(long millis) throws InterruptException -->休眠当前线程 millis毫秒
public static void sleep(long millis,int nanos) throws InterruptException -->更高精度的休眠
简单认识一下线程的状态
3.1.状态是是什么
状态就是针对线程调度的情况的一种描述的。而我们现在认为线程是调度的基本单位。状态更应该是线程的属性。在系统中谈到状态,我们可以理解为都是在考虑线程的状态了。
其实在我们java中对线程调度的时候对他的状态描述,进行了细化分类,接下来让我们简单了解一下线程中的几种状态以及状态之间的转换。
1.new -->创建一个Thread对象,但是还没调用start(就是内核里咱还没创建对应的pcb)
2.TERMINATED --->表示内核中的pcb已经执行完毕,但是Thread对象还在(下面解释为啥还在!!!)
3.RUNNABLE --->可运行的,这个状态就是表示,这个线程可能正在cpu上执行,也有可能在执行的就绪队列中,随时等待cpu执行
4.TIMED_WAITING
5.WAITING
6.BLOCKED 4.5.6都是表示阻塞(就是都是线程pcb正处在阻塞队列中)but这几个状态都是不同原因的阻塞(后面我们会重点讲解5,6,此处不做详细描述)
通过一下代码我们可以感受一下从new 到 TERMINATED状态的一个执行过程
public class ThreadDemo{
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(()->{
for(int i = 0; i < 100000; i++){
//这个循环啥都不干,也不sleep
}
});
//启动之前,获取thread的状态,就是new状态
System.out.println("start之前:" + thread.getState());
thread.start();
System.out.println("thread执行中的状态:" + thread.getState());
thread.join();
//线程执行完毕之后就是TERMINATED
System.out.println("thread结束之后:" + thread.getState());
}
}
3.2.理解线程状态间转换:
在new状态下创建一个线程对象,此时pcb还没创建,调用start()方法后真正创建一个线程对象,进行到RUNNABLE状态下此时我们pcb已经有啦,线程可以参与到我们的调度执行了,如果顺利的话线程执行完其中run方法,咱们这个线程的工作就结束了,此时此刻线程就进入到我们的TERMINATED状态下了这个时候我们pcb就释放了,但是我们的Thread对象还在(等会解释,别急!!),如果线程在参与调度的情况下有其他操作的话,那么就不回如此顺利的结束,当有一个加锁BLOCKED状态的时候,此时线程就会等待锁产生的阻塞,直到获取到锁的才会解锁,同理当有一个sleep状态的时候 我们的TIME-WAITING就会等待一定时间,等时间执行完了我们再回到线程调度的执行中,当有一个wait和join的时候也是这样的以上同理。
咱们可以通过展示图,直观的感受一下线程之间的转换过程。
3.3.为什么说当我们的pcb释放了,但是我们的Thread对象还在呢?
首先我们需要知道,java中的Thread类和操作系统里的线程是两回事,一旦内核里的pcb消亡了,此时代码中thread对象也就没啥用了,java中的对象的生命周期自有其规则,因为生命周期和系统内核里的线程是两回事,所以内核里的线程释放了,也无法保证java中的代码thread对象立即释放。
因此,我们就会看到线程里的pcb消亡了,但是thread对象还存在这个情况,放心这是正常的。
虽然thread对象还存在,但也就没啥用了,那我们该如何知道这个thread对象没用了呢!所以我们可以通过线程里边特定的状态,来判断thread对象的“有/无效性"。
3.4.拓展问题:
线程和线程之间到底共享着什么资源:
线程共享的资源包括:
(1) 进程代码段
(2) 进程的公有数据(利用这些数据,线程很容易实现相互之间的通讯)
(3) 进程的所拥有资源。
线程独立的资源包括:
(1)线程ID:每个线程都有自己唯一的ID,用于区分不同的线程。
(2)寄存器组的值:当线程切换时,必须将原有的线程的寄存器集合的状态保存,以便重新切换时得以恢复。
(3)线程的堆栈:堆栈是保证线程独立运行所必须的。
(4)错误返回码:由于同一个进程中有很多个线程同时运行,可能某个线程进行系统调用后设置了error值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。
(5)线程优先级:线程调度的次序(并不是优先级大的一定会先执行,优先级大只是最先执行的机会大)。
各位铁汁们,到这里就要结束了,嘿嘿,这篇博客希望能给各位老铁带来帮助,有不足的地方还希望各位老铁帮我指出,在下一章节中我们会学习到线程带来的风险安全,谢谢大家咯!!!