JavaWeb 基础知识(二)多线程01
上节回顾
??我们在介绍本节内容之前,先来简单复习一下上一节进程的相关内容
一、认识线程
0.线程的引入
??引进进程的目的,就是为了能够"并发编程"
??虽然多进程已经能够解决并发的问题了,但是我们认为,还不够理想。
创建进程、销毁进程、调度进程开销有点大了
进程时系统资源分配的基本单位
创建进程,就需要分配资源 销毁进程,就需要释放资源
??于是程序员就发明了一个 “线程”(Thread)的概念,线程在有些系统上也被叫做"轻量级进程".
轻量: 创建线程比创建进程更高效 创建线程比销毁线程更高效 调度线程比调度进程更高效
1.线程的概念
一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
我们站在系统内核的角度,再来看进程和线程
一个系统之中可能有很多个PCB(进程控制块),各个PCB通过链表进行连接,如图代表系统中已有的4个进程 ( pid 分别代表着进程id )
在Linux系统中,线程同样是使用PCB来描述的
进程1,对应一个PCB,在这个进程1里创建一个线程,也是再加了一个PCB ??这就是当前我们看到的一个情况,那其实,站在操作系统内核的角度,不分“线程”还是“进程”,系统只认PCB
??我们用户在创建一个进程出来,系统内核方面就会有一个PCB插入到双向链表里面,如果我们在代码中再去创建一个新的线程,也就是再加一个PCB
??像上面的 进程2、进程3、进程4,他们看起来没有创建其他线程,但是进程创建之初,也会有一个PCB产生,我们可以把PCB视作里面的一个线程
我们可以得到一个结论:
??当我们创建了一个进程的时候,就是创建了一个PCB 出来,同时这个PCB也可以是当做是目前进程已经包含了一个线程了,所以一个进程中至少有一个线程。
??同一个进程的线程之间,是可以共用一份内存空间的,同时其他的进程(PCB)使用的是独立的内存
??也就是说上面的进程中,进程1和线程1 共用一份内存空间,进程2、进程3、进程4都有自己独立的内存空间。
??这就是我们站在系统内核的角度描述线程基本的情况
那我们又拐回来了,线程和代码有啥关系呢?
可以认为,一个线程就是代码中的一个执行流
执行流:按照一定的顺序来执行一组指令
2.进程与线程
??进程与线程之间本来就是容易搞混淆的,尤其是对于Linux系统来说,进程和线程之间又是存在着千丝万缕的联系,总之呢,我们得知道 进程和线程之间 的区别和联系
经典面试题
进程和线程之间的区别和联系[面试题]
1、进程是包含线程的。一个进程里可以有一个线程,同时也可以有多个线程。
2、每个进程都有独立的内存空间(虚拟地址空间),同一个进程的多个线程之间,共用一个虚拟地址空间。
3、进程是操作系统分配资源的基本单位,线程是操作系统调度执行的基本单位
??上节课所介绍的"进程调度",当时咱们是没有考虑线程的,实际上系统是以线程(PCB)为单位进行调度执行的.
咱们来画图说明:
3个进程4个线程
??我们先让CPU处理 第一个PCB块,执行一段时间之后,把PCB1 释放,再来执行PCB2,执行一段时间后,再进行释放,所以系统是根据PCB进行调度执行的.
??以上就是我们所讲的 线程和进程之间的区别与联系,上面的三点大家一定要有印象,是后面在面试的时候经常问到的问题。
例子
??刚才我们都一直在干巴巴的讲理论,可能是有点抽象了,那我们就再举一个例子进行说明一下吧(很形象)
主角:滑稽老铁 道具:封闭的房间与桌上的100只鸡
现在呢,房间里的桌上有100只鸡,如何提高滑稽老铁吃鸡的速度?
那么此时,我们就有两种方案:
1、多进程
那么多进程是怎么吃呢?
多进程吃鸡 现在有两个房间,两套桌子,把鸡平均分成两份,两个滑稽老铁同时再房间各自吃50只鸡
??这种分配的方法相比较于之前明显吃鸡的速度要高效好多。
??这就是我们所说的并发编程的效率,能够提升整体程序的效率
两个房间、两套桌子,说明每次再创建进程,都要给这个进程分配一些资源
这两个房间里的滑稽老铁,相互之间都看不见彼此,说明进程之间有独立的地址空间(进程的隔离性)
??这就是我们所说的多进程的吃鸡版本!
??当然了,两个房间、两套桌子总体来说成本还是有点高的,所以我们为了降低成本,那么我们还可以多线程吃鸡~
2、多线程
多线程吃鸡怎么吃?我们这样做~
还是一个房间,只不过多了一个滑稽老铁来一块吃鸡 多了一个滑稽老铁(多了一个吃鸡的执行流)
??每个滑稽吃50只鸡就行了 ~ 1个人吃50只肯定比1个人吃100只速度更快一些
这里还有一个很重要的问题:
??这两个滑稽老铁共用了同一个房间和桌子,一个进程的多个线程之间,共用一个虚拟地址空间.同时,这两个滑稽老铁是可以看到对方的情况的
只创建了一个滑稽,桌子和房间都没有新创建,创建这个线程的成本比创建进程的成本要更低.
??这就是多线程吃鸡的一个情况,那么接下来呢?
??我们多线程吃鸡吃着吃着觉得效率还不够高,还可以进一步怎么提高效率呢?
进一步提高效率:再多搞几个滑稽(线程)
??滑稽的数目(线程的数目)更多了,每个滑稽的任务就更少了,因此整体的效率就更高了~~
??就是说随着我们线程数目的增多,线程去完成同一个任务,我们的速度就会更快
??但是大家注意,这里的速度也不是说线程的数目越多越好!!如果线程的数目太多了,线程之间就会更加频繁的进行调度,调度的开销也就无法忽略了!!
就会出现下面的情况 我们增加了滑稽(线程)的数目,就可能出现有的滑稽抢不上位置(CPU),于是任务的执行速度反而会变慢,这什么意思呢?
??没有抢着位置的三个老铁,为了吃鸡,要往里面挤,于是已经围着桌子吃起来的滑稽老铁们就没有办法消停的吃鸡了,有的滑稽就可能本来吃的好好的,没一会被挤出来了,这样就会出现很多问题~
??所以线程也不是越多越好,线程的数目越多,就会引发更多的调度开销,反而可能让执行任务速度变得更慢~所以这一点呢,大家也要明确.
??还有一种情况,当我们很多滑稽老铁一起吃鸡的时候,可能有打架的行为~~
什么叫打架的情况呢?
还是刚才的饭局 两个滑稽(线程)同时看上一个鸡大腿(准备修改同一块内存的数据),这个时候就会起冲突.
??这种情况,我们称为"线程不安全",这同样也是多线程编程的重点问题,在后面的章节会着重介绍!!
还有一种情况,如果某个滑稽老铁不开心,某个滑稽(线程)一直抢不到桌子的位置 于是这个老铁一生气,把桌子给掀了!!
这说明什么呢?
一个进程里面如果某个线程抛出了异常,并且没有合理catch住的话,就可能导致整个进程都异常退出.其他线程也就玩完了
??所以一个线程不工作,其他的线程也全都不工作了,这一点,就对我们的多线程程序的安全性提了更高的呃要求
??多线程程序的编写,其实就提出了一个更高的要求,一定要保证线程的稳定
讲到这呢,那么我们滑稽的案例就告一段落了~ 希望通过这样的一个例子,让大家更好的了解进程与线程之间的关联关系
??以上就是我们所介绍的进程与线程的关联与特点,准确的来说是线程的一些特点,这些特点我们在以后写代码的时候也就会逐步的感受到了~好了,说了那么多,都是理论的知识,理论的知识大家有一个简单的认识就可以了,重点我们还是要落在代码上!!
??那么接下来,我们就介绍 使用Java来操作线程Thread类(创建线程)的相关方法
二、Java中的线程
??在Java当中,是使用Thread这个类的对象来表示一个操作系统中的线程
PCB是在操作系统内核中,描述线程的 而Thread类则是在Java的代码中 描述线程的.
接下来,我们就来写一下简单的代码来创建线程出来~
1.线程的创建
??首先我们得去创建Thread 的实例出来,但是常见的方式并不是直接new一个对象出来.
??Thread 是Java标准库中描述的一个关于线程的类.
??常见的方式就是自己定义一个类继承Thread,然后重写Thread中的 run 方法,run 方法就表示线程要执行的具体任务(代码)
start 方法,会在操作系统中真的创建一个线程出来(同时在内核中会创建PCB,加入到双向链表当中)
执行一下
这个新的线程,就会执行 run中所描述的代码
??看完这个线程的创建过程,有的同学不禁会问了,
??我们在 执行代码的时候 不用 t.start ,直接执行 t.run 行不行?咱们刚才不是把代码的逻辑定义到run方法里面了嘛,那我们直接调用t.run 不是一样会执行代码嘛??
执行一下
那么run 和 start 方法有什么区别呢?
(1)run 和 start
重点:经典面试题
run 和 start 的区别是非常非常大的,我们来给大家具体演示一下这个情况.
start 方法
当我们运行Java代码的时候,首先系统会创建一个进程,这个进程里面已经包含了一个线程了,这个线程执行的代码默认就是 main 方法 ,main方法调用t.start方法,在系统中又会创建一个线程(PCB)出来,然后这个PCB执行任务代码.
run 方法
run 方法没有创建新的PCB,没有创建新的线程.
t.run 这里并没有创建出一个新的线程,
而使用t.start 这个方法可以创建出新的线程,同时t.start 的两个线程之间是属于同一进程,属于并发的关系
例子
如果大家还是没有懂的话,给大家再举一个例子:
比如说老王想买一瓶酱油,start 方法就是 老王把儿子小王叫来说,你去楼下超市去买一瓶酱油,run方法就是 老王自己去买一瓶酱油。
这两种方式的区别,尤其是start,就是在派小王去买酱油的同时,老王自己同时想干嘛就干嘛,这就是并发的效果.而run方法只能老王只能去买酱油了,没法干其他别的事
这样的一个区别大家一定要区分请
让大家看一下程序并发的效果
??在上面的代码中,Mythread 中执行的是一个死循环,他会一直循环执行,在主线程Main里也会一直执行循环,那么都是死循环,这两个代码能同时执行吗?
??按照上面的讲解,MyThread 是一个执行流,Main 也是一个执行流,他们属于并发的关系,所以可以同时执行!
那么运行程序,观察结果~
??“hello main” 和 “hello thread” 进行交替打印,进一步验证了两个线程并发执行的效果。
??通过刚才这个代码,我们就可以看到,我们通过线程可以让两个死循环按照并发执行的方式,一起来执行,而不是单纯的说,一个执行完才去执行另外一个。好了,这是我们通过 start 创建线程来这样做的,如果我们改成 run呢?
执行代码,观察结果
??就会在MyThread 的死循环中转不出去,main的循环无法执行
??这里我们也可以看到 start 和 run之间很本质的区别,run 并没有创建出新的线程,它属于一个线程里面串行执行,而通过start 就可以创建新的线程,可以是两个线程以并发的方式同时来执行.
以上就是关于线程最基本的代码~
(2)创建线程的几种方式
??在上面的程序中,我们是通过新建一个类继承标准库中的Thread类来创建线程的,实际上,线程的创建是有很多种方式的,下面我们就来了解一下 Java当中创建线程的几种方式.
1.继承Thread,重写run
??我们自己建一个类继承Thread ,在这个类中重写run方法.
**加粗样式**
class Mythread1 extends Thread{
@Override
public void run() {
}
}
2.实现Runable接口,重写 run
Runable 是标准库提供的一个接口,这个接口主要用于描述"一个任务",里面也是有一个核心的run方法,通过run方法来描述具体要执行的任务代码是什么…
class MyRunable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Thread1 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunable());
t1.start();
}
}
注意: ??我们的Runable 并不是能够独立去使用,还要搭配我们的 Thread 类来进行使用,new MyRunable 的实例作为 Thread 的参数,这就是当前这种方式的写法.
本质上和刚才继承Thread重写Run的效果一样,都是具体告诉线程具体要执行的任务是什么…
只不过MyRunable只是用来描述一个具体的任务是什么,而真正线程的主体还是在于我们的Thread类本身.
??同时这种方式,还可以给当前创建的线程赋予名字,名字作为Thread 的第二个参数
3.继承Thread,重写run(匿名内部类)
内部类:在一个类里面定义类
所以匿名内部类就是一个没有类名的内部类,没有类名也没有关系,至少可以创建出一个实例来~
那么我们什么时候需要用到匿名内部类呢? 只需要这个实例,不需要用到其他实例了, 匿名内部类的方式写起来更加简洁一些
那么下面我们具体来看具体的代码应该怎么写…
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
}
};
}
??创建了一个匿名的类,这个类继承了 Thread,此处new 的实例其实是Thread类的子类的实例
4.实现Runnable 重写run,使用匿名内部类
??因为都是用的匿名内部类,所以这两种写法都很像,但是我们仔细观察会发现,写法不太相同.
我们把两组代码拿过来对比一下,可以看出区别来
第一种:我们创建匿名内部类是Thread 的子类,所以 {} 跟在 Thread 的后面
第二种:我们创建的是一个Runnable 这样的子类,new的 Runnable 子类作为 Thread 的一个参数。相当于创建了一个匿名内部类的实例,把这个实例作为 Thread的参数。
这两种写法还是有一定区别的.
??以上的这些创建线程的方式,本质上都相同
??只不过区别是,指定线程要执行的任务的方式不一样,此处的区别,其实都是单纯的Java语法层面的区别~ 所以这样的区别并不是很关键,这样的写法大家只需要多写两次去熟练就会了…
好了,写到这里,可能有同学说了
我们已经了解了创建线程的几种方式了,也知道如何并发执行了,那么有没有像任务管理器一样的东西让我们能够看到 Java创建的线程呢?
(3)jconsole 查看线程信息
??在 JDK 中内置了一个 jconsole 工具,就可以看到线程的信息.
我们先在Java运行一个线程
点击运行,看一看jconsole 里面的线程信息
jconsole 在哪里找呢? 先找到我们的jdk文件,bin目录下就有 jconsole.exe
打开jconsole 之后出现这样的界面
选择本地进程Main,然后点击连接
注意在这里,显示的进程只是Java相关的进程,非Java的进程显示不出来
??这些线程都是当前进程的线程,对于一个Java程序来说,启动的时候不仅启动了main这样一个线程(main这个线程是 main方法对应的线程, thread-0 这个就是我们自己创建的新的线程),还有很多其他的线程,这些线程都是JVM在运行的时候内置的一些线程…
我们可以通过 这个工具查看每个线程的具体情况 如果写的程序,发现程序挂了,就可以通过 jconsole 来查看程序里面每个线程的情况,对于分析解决问题就有很大帮助了
??以上就是用 jconsole 来查看 线程相关信息的具体操作,当然了我们还可以根据其他的信息来查看,我们就暂时不去介绍这么多了~
??好了,我们继续线程的另一块知识~
(4)多线程的优势-增加运行速度
??之前我们介绍并发编程能够提高程序的效率,我们呢就通过 Java 的代码来了解一下 并发编程的效率
这个代码我们要干什么呢?
首先我们有一个很大的数字,这个数字是10亿
首先是串行执行代码,a、b分别自增10亿次
我们来看一下执行结果:
然后是并发执行,让a、b分别在两个线程中并发执行自增操作,然后计时.
运行查看结果:
当前呢,使用并发的方式 确实比 串行的方式时间上 效率提高很多,
串行执行 600—700 ms 并发执行 300—400 ms 速度确实提高了好多
速度提高正好是提高一倍嘛? 不是~(不一定) 主要是因为线程调度自身也是有开销的~
串行执行: 一个线程执行了20亿次循环,中间可能调度若干次
并发执行:两个线程各自执行10亿次循环,中间可能调度若干次.
因为系统有调度,所以会对程序的运行时间有影响
2.Thread 的常见构造方法
方法 | 说明 |
---|
Thread() | 创建线程对象 | Thread(Runnable target) | 使用 Runnable 对象创建线程对象 | Thread(String name) | 创建线程对象,并命名 | Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 | 【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
具体代码使用:
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
3.Thread 的几个常见属性
属性 | 获取方法 |
---|
ID | getId() | 名称 | getName() | 状态 | getState() | 优先级 | getPriority() | 是否是后台线程 | isDaemon() | 是否存活 | isAlive | 是否被中断 | isInterrupted |
ID
ID 是线程的唯一标识,不同线程不会重复
名称
名称是各种调试工具会用到
状态
状态表示线程当前所处的一个情况,和上一节说的"进程的状态"是类似的效果,存在的意义都是辅助进行线程调度
优先级
优先级高的线程理论上来说更容易被调度到,和上节课"进程的优先级"是类似的效果
??此处的状态和优先级 ,和PCB中的状态优先级并不完全一致,Java线程在这个基础上有做了自己的丰富.
后台线程
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
我们在Java中创建的线程一般默认都是非后台线程,此时,如果main方法结束了,线程还没结束,JVM不会结束
如果当前线程是后台线程,此时如果main方法结束了,线程还没结束,那么JVM进程会直接结束,同时也把这个后台线程给带走了.
存活
是否存活,即简单的理解,为 run 方法是否运行结束了 存活是什么意思呢》我们来画一下
t中的代码执行完之后,Java中的线程PCB也会同时销毁吗?并不会.
Java 中PCB对象在JVM 垃圾回收机制下才会被销毁,而操作系统内核的 PCB 在代码执行完之后就销毁了
所以我们就可以通过 isAlive() 判断内核中的PCB是否存在
我们对当前程序中的线程查看属性
class MyRunable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello wolrd");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Thread1 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunable(),"线程01");
t1.run();
System.out.println("id: "+t1.getId());
System.out.println("name: "+t1.getName());
System.out.println("Priority: " +t1.getPriority());
System.out.println("State: "+t1.getState());
System.out.println("isAlive: "+t1.isAlive());
System.out.println("isDeamon: "+t1.isDaemon());
}
}
??好了,今天的线程就讲到这里,希望大家多多复习~
谢谢欣赏!!
下一篇 JavaWeb基础知识(三)——线程02 敬请期待~
未完待续…
|