在写代码的时候,可以使用多进程进行并发编程(在Java中,不太推荐,很多很多关于进程相关的API,在Java标准库中,都没有提供),也可以使用多线程进行并发编程(系统提供了多线程编程的API,Java标准库,已经将这些API封装了,在代码中可以直接使用)。
目录
创建线程
方法1 继承Thread类
方法2: 实现Runnable接口,重写run方法
方法3. 继承Thread,重写run,但是使用匿名内部类
方法4. 实现Runnable,重写 run,匿名内部类
方法5. 使用 lambda 表达式(常用)
完!
创建线程
方法1 继承Thread类
继承Thread来创建一个线程类
细节补充:
1. 第一行代码: class MyThread extends Thread 中,继承的Thread这个类,好像可以直接使用,并不像Scanner,ArrayList,Random一样需要导包,这是为什么呢?
答:Java标准库中,有一个特殊的包,java.lang包,这个包中包含了Thread类,且这个lang包是Java自动引入的。
2.自己创建的类MyThread中,类前面没有public,这是怎么回事?前面能不能写public?
答:一个 .java文件中,只能有一个public的类。 一个类前面如果没有范围限定符,那就默认是包级作用域,就是只能在当前包中被其他类使用。
3.怎么理解run方法是线程的入口方法?
答:此处的run方法,并不需要程序员来手动的进行调用,该方法会在线程创建好之后,一个合适的时机,被JVM自动的调用执行。就类似于,main方法,是一个java进程的入口方法。一个进程中,至少会有一个线程,这个进程中的第一个线程,也就称为”主线程“。main方法,也就是主线程的入口方法。
补充:这种风格的函数,称之为”回调函数“(callback)
在Java数据结构中,优先级队列PriorityQueue,在使用的时候,必须要给对象实现一个比较的接口,才能进行使用。
回调函数 :作为参数传递给另一个函数,在该函数内部的某个特定时刻被调用执行的函数。其核心特点是函数的调用时机由其他代码控制。run方法的函数的调用时机由其他代码(这里是 JVM)控制。
4.run方法上面的 @Override 是什么意思?
答:方法重写。本质上:是让程序员能够对现有的类,进行扩展。我们要搞一个线程,肯定是要让这个线程执行一些代码的,Thread类本身会带有一个run方法。但很明显,标准库自带的run方法,是不知道我们的具体需求的,业务要求是什么样?必须要手动的进行指定,Override就是针对原有的Thread进行扩展(把一些能够复用的,进行了重用,把需要扩展的,进行扩展)。Thread这个API中,会有很多的属性方法,大部分内容直接复用即可,把需要扩展的内容,进行扩展即可。
那如果没有@Override这个注解,貌似也可以实现方法重写呀。为啥还要写这个注解呢?语法中有很多的机制,是方便让编译器,对我们的代码进行自动检查的。(人是非常不靠谱的!!!机器是较为靠谱的!!!)(就比如fina限定符,限定一个变量不能再被修改,就是方便让编译器为我们进行自动检查的)
5.一般来说,实例化的时候都是方式2。
6.什么是操作系统的”内核“?
答:操作系统中,最核心部分的功能模块(管理硬件,给软件提供稳定的运行环境)
操作系统大致可以分为内核空间(内核态)和用户空间(用户态),平时运行的普通的应用程序,网易云音乐,idea,qq等等,都是运行在用户态的。但是,这些应用程序,有时候,需要针对一些系统提供的硬件资源来进行操作。这些操作,都不是应用程序直接操作的,就是需要调用操作系统提供的API,进一步在内核中完成这样的操作。
为啥还要分出用户态和内核态?
最主要的目的还是稳定。防止某些应用程序把硬件设备或者软件资源给搞坏了。系统封装了一些API,这些API都属于是一些”合法“的操作,不会对硬件设备或者软件资源有破坏,应用程序只能调用这些API来实现对应的功能,从而不至于对系统 / 硬件设备产生太大的危害。假如让应用系统直接操作硬件,在极端条件下,代码出现bug,会把硬件干坏。
(就比如是银行系统,办事窗口里的工作人员,就是正经的经过培训政审的安全人员,不会对银行产生危害,用户要办理各种业务,就需要在办事窗口前,给工作人员说清楚需求,由工作人员代办)
具体解释:每个线程都是一个独立的执行流,每个线程都能够独立的去CPU上调度执行。
如下图代码:
下面的死循环,是在 t 线程中执行的
而这个死循环,是在 main 线程中执行的
以之前的理解,如果一个代码中,出现了两个死循环,则肯定最多只能执行一个,另一个循环就进不去了。但我们把进程运行起来,可以看到,两个循环,都在执行!这两个线程,就是两个独立的执行流,也就解释了我们最开始那句话:每个线程都是一个独立的执行流,每个线程都能够独立的去CPU上调度执行。
在调用start方法,创建线程之后,兵分两路,一路,沿着main方法,继续执行,打印hello main,另一路,进入到线程的run方法,打印hello thread。是相互独立的,互不干扰的。
注意:
当有多个线程的时候,这些线程的执行的先后顺序,是不确定的!!!(这一点,是因为操作系统内核中,有一个”调度器“模块,这个模块的实现方法,是一种类似于”随机调度“的效果)
那随即调度又是什么呢?
1. 一个线程,什么时候调度到CPU上去执行,时机是不确定的。
2. 一个线程,什么时候从 CPU上下来,给别人让位,时机也是不确定的)
也称为”抢占式执行“。
所以,每秒钟到底是先执行 main 还是先执行thread,这是不一定的(随机调度,抢占式执行)
主线程,在调用 strat 方法之后,就立即往下执行打印了,与此同时,内核就要通过刚才 API 构建出线程,并且执行run。由于创建线程本身也有开销(虽然开销比创建进程低,但也不是0).所以,在第一轮打印中,因为创建线程有一定的开销影响,导致hello thread一般情况下 都比hello main略慢一筹。
我们刚刚,只是通过打印的方式,看到了两个执行流,还可以通过一些第三方工具,更直观的来查看多个线程的情况。 --> JDK中,有一个jconsole工具,该工具在JDK的bin文件夹中(该工具,只能列出java的进程,其他不是java的进程,无法进行分析)
注意,我们需要先把进程运行起来,然后再打开工具,找到对应的类名,然后进行链接
进入之后,选择线程
main 对应的就是main方法的主线程
黄色框的,就是我们缩写的代码,创建的 t 线程(这个Thread - 0,1,2...是默认名称 可以改)
剩下的线程,都是JVM自带的线程,这些自带的线程,要完成一些垃圾回收,监控统计各种指标....
点进具体的线程,就可以看到相关的调用栈(线程里当前执行到了那个方法的第几行代码,这个方法是如何如何一层调用过去的...)
这里这个线程的在不断的运行的,点击线程详细情况的这个瞬间,就相当于咔嚓来一个闪照一样,把这一瞬间的状态展示到这里了。
这就是使用 jconsole 来检测线程的方法
再回头看我们的举例代码
我们在循环中加入了sleep方法,来降低这两个死循环的速度(在C语言中,用的是Windows api中提供的Sleep函数,Windows.h),我们此处使用的sleep,是Java中封装后的版本,属于是Thread提供的静态方法。
如果我们不进行 try - catch的话,该方法会编译报错,为什么必须需要抛出异常呢? --> 这个异常,意味着,在sleep(1000)的过程中,可能会被提前唤醒。(换句话说:sleep(1000)的作用是休眠1s,但在这休眠1s中,可能会被其他操作给提前唤醒,也就是没休眠够一秒,就被唤醒了,此处我们就应该在catch块中 提出具体的解决方法,到底应该怎么做)
还有一点需要注意:
为什么在run方法中,解决sleep的异常只能是try - catch方法
但是在main方法中,可以是try - catch方法,也可以是在方法体上throws出异常
为什么呢?
原因很简单,run方法中的sleep,如果加上throws的话,就修改了方法签名了,这样的话是无法构成”重写“的,因为父类的run方法中,并没有throws这个异常,子类重写的时候,也就不能throws异常。
方法2: 实现Runnable接口,重写run方法
1.实现Runnable接口
Runnable可以理解成”可执行的“,通过这个接口,就可以抽象表示出一段可以被其他实例来执行的代码。
2.创建Thread实例,调用Thread的构造方法时,将Runnable对象作为参数传入。
Runnable接口,还需要搭配Thread类,才能真正在系统中创建出线程
3.调用start方法
上面这种写法,其实是把 线程 和 要执行的任务 进行了解耦合了。
方法3. 继承Thread,重写run,但是使用匿名内部类
匿名内部类:是一种内部类,在一个类里面定义的类,其最大的特点是-->没有名字,不能重复使用,用一次之后就找不到了。
解释:
写这个 { } 的意思是:定义一个类,与此同时,这个新的类,继承自Thread。此处的 { } 中,可以定义子类的属性和方法。此处最主要的目的就是重写 run 方法。与此同时,这个代码,还创建了子类的实例。且 t 指向的实例,并非是单纯的Thread,而是 Thread的子类(但并不知道这个子类叫什么,因为是匿名的内部类)
方法4. 实现Runnable,重写 run,匿名内部类
这种方法,在Thread构造方法的参数中,填写了 Runnable 的匿名内部类的实例
方法5. 使用 lambda 表达式(常用)
在lambda表达式中, () 中是形参列表,这里可以带参数,但因为线程的入口不需要参数,所以这里为空,() 的前面,应该还有一个函数名,此处作为匿名函数,就没有名字了。
解释:
在Java中,方法是不能脱离类单独存在的,所以就不得不设置回调函数,从而多套了一层。但与此同时,java 语法也开了一个特殊的口子,就是 lambda 表达式,函数式接口,属于是 lambda 背后的实现,相当于 java 在没破坏原有的规则的基础上,给了一个 lambda 一个合法性的解释。
上述五种方法,都是等价的,可以互相转换的,只不过是第五种 lambda 表达式的方法,更为简洁一些,更加常用一些!