JavaWeb 基础知识——线程01

Posted RAIN 7

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaWeb 基础知识——线程01相关的知识,希望对你有一定的参考价值。


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 的几个常见属性


属性获取方法
IDgetId()
名称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 敬请期待~



未完待续…

以上是关于JavaWeb 基础知识——线程01的主要内容,如果未能解决你的问题,请参考以下文章

动态SQL基础概念复习(Javaweb作业5)

JavaWeb 基础知识 --多线程(阻塞队列+生产消费者模型)

JavaWeb 基础知识 --多线程(阻塞队列+生产消费者模型)

线程学习知识点总结

多线程 Thread 线程同步 synchronized

Java面试:javaweb框架代码