《JavaEE》----2.<多线程的简介&创建&Thread类>

前言:

      大家好,我目前在学习java。我准备利用这个暑假,来复习之前学过的内容,并整理好之前写过的博客进行发布。如果博客中有错误或者没有读懂的地方。热烈欢迎大家在评论区进行讨论!!!

      喜欢我文章的兄弟姐妹们可以点赞,收藏和评论。如果感觉有所收获可以关注我呦。我会持续更新滴,望支持!!!!!!一起加油呀!!!!

语言只是工具,决定你好不好找工作的是你的能力!!!!!

学历本科及以上就够用了!!!!!!!!!!


本篇博客会简单介绍线程、线程的特点、优点、线程的不安全问题、进程与线程的区别、Java中如何进程多线程编程、Thread类。

c++中会讲很多多进程编程,而在Java这样的生态中,并不是很鼓励多进程编程,更鼓励多线程编程。

引入多个进程,目的是为了实现并发编程=>多核cpu的时代。

多进程实现并发编程效果很好,但是多进程编程模型也有明显的缺点:

多进程编程模型的缺点

进程太重量,效率不高。

创建一个进程,销毁一个进程,调度一个进程消耗时间都比较多。

时间消耗在申请资源上。进程是资源分配的基本单位。分配内存操作是一个复杂的操作。

(操作系统内部有一定的数据结构,把空闲的内存分块管理好,当我们进行申请内存的时候,系统就会从这样的数据结构中找到一个大小适合的空闲内存。返回给对应的进程。这里虽然通过此处的数据结构,可以一定程度提高效率,整体来说,管理的空间比较多,相比之下,还是一个耗时操作。)

如果频繁创建/销毁进程时,这个耗时就不能忽视了

为了解决上述问题,就引入了“线程”(Thread)

一、线程也叫做“轻量级进程”

线程不能独立存在,而是要依附于进程,(进程包含线程,可以包含一个或多个)

一个进程最开始至少要有一个线程,这个线程负责完成执行代码的工作。

也可以根据需要,创建出更多的线程,从而使当前实现“并发编程”的效果

每个线程都可以独立执行一些代码。

之前提到的进程调度

是基于“一个进程里只有一个线程”的情况。

实际上,一个进程中,是可以有多个线程的~~每个线程都是可以独立的进行调度的~~

因此以后看到进程的调度,我们就知道,并不是把整个进程进行调度。而是去调度进程里面的每一个线程,每一个线程执行一些逻辑,每一个线程就可以分别在这上面进行调度。每一个线程也有

状态

优先级

上下文

记账信息....

一个进程,可能使用一个PCB表示,也可能使用多个PCB表示,每一个PCB对应到一个线程上,因此每一个线程都有自己的状态、优先级、上下文、记账信息....每一个线程都有这些信息进行辅助调度~~

除此之外,前面谈到的pid,是相同的。内存指针,文件描述符表也是相同的。共用同一份的

线程和线程之间共用同一份pid、内存指针、文件描述符表

二、线程的特点

1.每一个线程都可以独立的去cpu上调度执行

2.同一个进程的多个线程之间共用同一份内存空间和文件资源...

三、线程的优点

创建效率更高

(创建线程的时候,不需要重新申请资源了,直接复用之前已经分配给进程的资源,省去了资源分配的开销,于是创建效率就更高了)

总结:

进程中包含线程,

一个进程由多个PCB共同表示,

每个PCB就用来表示一个线程,

每个线程都有自己的状态、优先级、上下文、记账信息....

每个线程都可以独立去CPU上调度执行,

这些PCB共用了同样的pid、内存指针、文件描述符表

创建线程(PCB)的时候,不需要重新申请资源了,直接复用之前已经分配给进程的资源,省去了资源分配的开销,于是创建效率就更高了

进程是资源分配的基本单位

线程是调度执行的基本单位

一个系统中,可以有很多进程,每个进程都有自己的资源

一个进程中,可以有很多线程,每个线程都能独立调度,共享内存/硬盘资源

四、多线程方式

刚开始创建第一个线程的时候,相当于和进程一起创建,还是需要有一定的开销去申请资源的(这个账是记在进程上面的)后面再创建线程,开销就省下了。

创建多线程方式可以提高效率,但是线程到达一定的数量,效率就无法进一步提升了,反而会因为需要调度的线程太多,使调度的开销更大,反而降低效率。线程多了,也容易产生一定的冲突。

五、线程不安全问题

如果一个线程抛出异常,如果没有妥善处理(要catch住),就容易使整个进程崩溃。此时其他线程也会随之消亡。

六、进程与线程的区别(经典面试题)

1.进程包含线程,一个进程里面可以有一个线程,也可以有多个线程

2.进程和线程,都是用来实现 并发编程 场景的,但是线程比进程更加轻量,更高效

3.同一个进程的线程之间,共用同一份的资源(内存+硬盘),省去了申请资源的开销

4.进程和进程之间,具有独立性,一个进程挂了,不会影响其他进程

   同一个进程的线程和线程之间,可能互相影响,(线程安全问题+线程出现异常)

5.进程是资源分配的基本单位,线程是调度执行的基本单位。

七、Java如何进行多线程编程 

7.1 基本的多线程编程

创建线程的方法

①继承Thread类,重写run方法

线程是操作系统的概念,操作系统提高了一些API,可以操作线程Java针对上述系统API进行了封装(实现跨平台)程序员只需要掌握这一套API就可以了。

Thread类,创建Thread类对象,进一步的就可以操作,系统内部的线程了。使用这个类,创建出一个线程出来。继承“Thread”是Java标准库内置的类,我们直接就能使用。

此处Thread不需要import也能使用,是因为Thread这个类在java.lang包下。

1.创建一个类继承Thread。再重写run方法。这个run方法就是线程的入口方法。入口方法就是代表线程一旦执行起来后,具体要执行哪些逻辑。类似于main方法。

:每个线程都是独立的执行流,每个线程都可以执行一系列的逻辑(代码)一个线程跑起来,就是从它的入口方法开始执行。

类比运行Java程序:就是跑起来一个java进程,这个进程里面至少会有一个线程,主线程的入口方法就是main方法。

2.2.创建一个主线程,也就是在main方法中创建一个Thread的实例,创建好了之后再去调用start方法。

start和run方法的区别的功能描述

//start和run都是Thread的成员

//run只是描述了线程的入口(线程要做什么任务)

//start则是真正调用了系统API,在系统中创建出线程,让线程再调用run

这里的创建线程,实在系统内核里面创建线程。涉及到创建PCB并且加入到内核链表里面,这样创建好的线程就会进一步执行我们的run方法。这样就可以将新的线程创建出来。

此时若在run中有System.out.println(“hello thread”) ;

那么运行程序,就会打印出hello thread


给打印代码加上while(true),死循环,在线程和主线程中一个打印hello thread,另一个打印hello main。运行代码,我们可以发现两边的日志都在交替打印

1.每个线程都是独立执行的逻辑,独立的执行流。

2.从t.start();代码之后,就会兵分两路,并发执行。达到了并发编程的效果,充分的使用了多核cpu资源。


把t.start()改成t.run()。并不会创建新的线程,只有一个主线程,这个主线程依次执行循环,执行完一个循环再执行另一个。

main这个线程是jvm自动创建的,和其他线程相比,没啥特殊的。

一个Java进程中,至少会有一个main线程。

 7.2 查看该进程里的多线程情况

多线程程序运行的时候,可以使用IDEA或者jconsole来观察到该进程里的多线程的情况

IDEA对新手不太友好,以调试模式启动程序,会有一个专门的窗口,查看方法的调用栈,在这里可以看到所有线程的信息。

jconsole来观察到该进程里的多线程的情况(jdk的bin目录中)

1.启动之前,确保idea中的程序已经跑起来了

2.若啥都不显示,可能需要使用管理员方式运行

它会列出当前机器上所运行的所有java进程

使用方法:

1.双击jconsole.exe,出现如下窗口,点击本地进程中你正用IDEA跑起来的进程。点击连接

 2.出现如下窗口,我们要查看线程情况,点击线程

在jconsole,可以看到一个java进程,即使是最简单的,里面也包含了很多线程。

Thread = new MyThread();是自己动手创建的,其他的线程都是JVM自动创建的。一个java进程,启动之后JVM会在后面,默默帮我们做很多事情,比如垃圾回收、资源统计、远程方法调用...

我们只关注两个

1.main是主线程

2.Thread-0是我们创建的线程 ,点进去我们可以看到详细信息。

最主要,我们看堆栈跟踪(也就是线程的“调用栈”):描述了方法的调用关系。

功能:

未来写一些多线程程序的时候,就可以借助这个功能看到该程序实时的运行情况,比如你写的程序“卡死了”。

让while循环慢点(sleep)

在循环体里加上sleep,休眠

Thread.sleep();

输入参数的单位为毫秒。例如:

Thread.sleep(millis:1000);

此语句需要抛出异常,要么往上throws或者进行try,catch。

在当前我们写的线程中(重写run方法),我们必须进行try,catch,因为我们现在是方法重写,如果父类的run方法没有throws,那么子类这个方法也就没法去throws。

而在主线程(main方法中),我们可以进行throws。

sleep是Thread的类(静态)方法。

我们发现两线程都是休眠1000ms,当时间到了之后,这俩线程谁先执行,谁后执行不一定。这个过程可以视为“随机的”。

“对多线程调度顺序的“随机性””

因为操作系统对于多个线程的调度顺序,是不确定的,“随机的”(此处的随机不均等,可能优先级不一样,就算一样是不是均等也很难说,取决于操作系统对于线程调度的模块,调度器的实现),

类方法,类属性 VS(普通) 实例方法,实例属性 

类方法,类属性,直接用类名就可以调用

实例方法,实例属性,需要用类实例化对象,用对象名进行调用。

ps:static历史问题

c语言最初引入了static,以前的操作系统,运行的进程中,专门有一个内存区域,叫做“静态内存区”随着时间发展,静态内存区没有了,后来c就是用static表示其他含义了。

如果static修饰一个全局变量,或者修饰一个方法,表示它的作用域,就在当前.c文件里

如果修饰一个局部变量,那么就表示这个变量的生命周期是跟随整个程序的。

c++中把static又赋予了新的含义,c++引入了类和对象的概念,static就是类的成员,达到“类方法”“类属性”定义效果了(如果新加关键字来表示“类方法”“类属性”不合适,这会导致现有的代码可能产生冲突。如与之前变量名一样,c++要考虑和c兼容,有很多程序使用c,若引入关键字,可能导致现有的代码无法编译。)

java是从c++这边参考过来的,因此java这边也就用static表示类方法,类属性

Python没有这样的历史包袱,因此python直接使用@classmethod这样的方式来表示类方法。

7.3 创建线程的其他写法

②实现Runnable接口,重写run方法 

实现Runnable接口,重写run方法 

这里的内容和之前继承Thread类是一样的。也是描述了线程的入口。

不同的是,这里在main方法中,需要创建Runnable接口的实例化,描述一个任务。

再创建Thread的类的实例化,将Runnable的实例化交给Thread来执行。我们把这个任务放到线程里面去执行。通过t.start();通过这个操作,调用系统api来完成创建线程的工作~

Runnable 

Runnable本身并没有和线程进行联系,单纯的表示一个可运行的任务,这个任务是交给线程负责执行,还是交给其他实体来执行....Runnable本身并不关心。

ps:为什么总是向上转型(java)

Java这个圈子就爱这么写,

如果c++,这里的代码绝对不会写成向上转型。

c++是一个生态,这里的这群人不喜欢向上转型,他们觉得向上转型之后,触发多态,会有额外的运行时开销,不符合c++把性能追求到极致这样的初心。这边能不向上转型就不转型。

如果java,一定写成向上转型的方法。

java也是一个生态。这群人更鼓励使用向上转型,java程序员觉得性能问题不是问题,开发效率大于运行效率。使用向上转型,抽象层次高,代码的使用/理解成本更低。(能向上转型就向上转型。)

多态的本质

封装本质上是让调用者不再了解类实现的细节,从而降低了学习和使用成本。

多态则是在封装基础上更进一步,更是让你不知道当前是啥类。更不用说类的实现细节,你只需要关心它的父类。

就如这里,

只需要看到Runnable runnable(runnable是Thread类型)。而不需要关心后面new 了一个怎么样的runnable

只需要看到Thread t(t是Thread类型),而不需要关心后面new 了 一个怎样的Thread。属于将封装程度更加提高了。学习成本更降低了。

未来在公司中,接触到的各种代码,向上转型,也会非常普遍。

代码比较复杂,容易体会到封装/多态 的意义,

两次创建线程的差别

解耦合。

使用Runnable的写法,和直接继承Thread之间的区别主要是 解耦合。

相互影响越大,我们认为是耦合越高

创建线程,需要两个关键操作:

1.明确线程要执行的任务,

2.调用系统api,创建出线程。

任务本身,不一定和线程 概念 强相关,

这个任务只是单纯的执行一段代码,这个任务是使用单线程,还是多线程执行,还是通过其他方法(信号处理函数 / 协程 / 线程池)都没啥区别。

因此我们可以把任务本身给提取出来~我们将任务和线程之间进行解耦合,解耦合之后,我们随时就可以把任务改成其他方式来去执行。


匿名内部类

在数据结构课程的优先级队列中,我们学到过,能够按照优先级高低,来决定谁先出队列。

如果存的对象,那么谁优先级算高,谁算低呢,此时我们使用Compareble或者Comparator来定义,比较规则.

使用这个的时候,我们就可以使用匿名内部类来进行定义了

③.继承Thread重写run,但是使用匿名内部类.

public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(){

        };
    }
}

先创建出新的类,这个类的名字是啥,不知道

只知道这个类,是Thread的子类

同时又把这个子类的实例给创建出来了

(不知道这个类名,不影响,因为这个类本身就是只使用一次) 

package thread;

public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

这样看起来更直观。通过匿名内部类创建线程,这个方法本质上和方法①是一样的。只是换了一种写法。匿名内部类这种写法,在当前java中是比较常见的

④.实现Runnable,重写run,也是使用匿名内部类。

package thread;

public class Demo41 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        Thread t = new Thread(runnable);
        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

可以看到这样创建所执行的效果和之前也都是一样的,使用这种方式也是匿名内部类的写法。甚至我们可以连Runnable的变量名都可以不要。如下:

package thread;

public class Demo42 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

总之匿名内部类在java中很常见,要重点掌握哦!

⑤.基于lambda表达式(最推荐写法)

package thread;

public class Demo5 {
    public static void main(String[] args) {

        Thread t = new Thread(()->{
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

lambda(枚举,反射),是一种更简化的语法表示方式,我们称之为“语法糖”

例如for each遍历数组。

for(int i = 0; i<arr.length; i++) 这是一种遍历方式

for(int x : arr)           这也是一种遍历方式,这种更简洁,可以称之为语法糖

相当于匿名内部类的替换写法

       Thread t = new Thread(()->{
            
        });

lambda表达式:

这样的写法称之为lambda表达式,其本质上是一个匿名方法。匿名函数(用一次就不用了)。主要用来实现这种“回调函数”的效果。

Java中不允许函数独立存在,必须依托于一个类,(其他语言叫函数function,java这里叫做方法method)

因此这里个代码里面,看起来像是一个单独的函数,本质上是一个函数式接口,还是一个类或者是一个对象。lambda 本质是一个函数式接口(本质上还是没脱离类)。

函数指针:

是指向内存空间的,函数怎么跑到内存中,原因是操作系统,加载 一个可执行程序,创建进程的过程。当写的代码都是一个一个的文件,我们将他们预编译,得到一个exe.文件。还是一个文件,这个时候,函数在文件里,但是当我们双击exe文件,操作系统就会 加载这个exe文件,将exe文件中的指令和数据加载到内存中,构建成一个进程,这个时候,我们写的函数,对应的二进制指令就进入到内存中,这个时候拿指针指向它。 

作用:

1.使用函数指针实现转移表,降低代码的圈复杂度(减少 if else 分支数目)

2.使用函数指针作为回调函数(qsort)

回调函数:

回调函数,不是你主动调用,也不是现在就立即调用,而是把调用的机会交给别人(通常是操作系统,库,框架,别人写的代码)来进行使用,别人会在合适的时机来调用这个函数。

java中可以使用,lambda表达式和匿名内部类来描述这个回调函数。

八、Thread类的其他使用方法

        Thread t1 = new Thread();
        Thread t2 = new Thread(new MyRunnable());
        Thread t3 = new Thread("这是我的名字");
        Thread t4 = new Thread(new MyRunnable(),"这是我的名字");

前两个我们已经见过了,第三个

8.1 Thread(String name)方法

name,在创建线程的时候,我们可以去指定一个name,name不影响线程执行,只是给线程起个名字,后续在调试的时候,比较方便区分。

使用示例如下:

package thread;

public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"这是新线程");
        t.start();
    }
}

运行后,我们在JDK目录中,运行 jconsole.exe文件。

我们发现main线程咋不见了?

因为main执行完了,所以就没有了!!

线程的入口方法执行结束,则这个线程就自动销毁了

对于主线程来说,入口就是main方法

t.start();这个方法,一瞬间就执行完了。当start方法执行完毕之后,紧接着main方法执行结束,那么这个主线程自然没有了,销毁了。线程不是创建了就一直存在,而是执行完了自然就销毁了。

8.2 Tread的几个常见属性

属性                        获取方法

ID                             getId()       ID是线程唯一身份标识,不同线程不会重复。(这个id是                                                          Java给你这个线程分配的,不是系统api提供的线程id,更不                                                    是pcb中的id)

名称                         getName()    

状态                         getState()     就绪,阻塞...等等许多后面我们再去讨论。

优先级                      getPriority()     虽然提供了api可以设置/获取优先级,但是没什么大用,

                                                         从应用程序角度出发,很难察觉出来,优先级带来的差                                                          异,优先级影响到的是系统在微观上进行的调度。

                                                         后面我们再去详细讨论。

是否后台线程           isDaemon()  重点介绍:这个也叫做守护线程(后台线程) ,相对的有                                                       前台线程,如果前台线程没有执行结束,此时整个进程是                                                         一定不会结束的。而后台进程没有执行结束,并不影响整                                                         个进程的结束。默认情况下,一个线程是前台线程,除非                                                         把他手动定义成setDaemon(true) 。

是否存活                  isAlive()      Thread对象的生命周期,要比系统内核中的线程更长一些

                                                    Thread对象还在,内核中的线程已经销毁了这样的情况

                                                    我们可以通过isAlive判定内核线程是不是已经没了

                                                    回调方法执行完毕,线程就没了

是否被中断              isInterrupted()

是否后台线程           isDaemon() 

示例:

package thread;

public class Demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"这是新线程");

        //设置t为后台线程
        t.setDaemon(true);

        t.start();

    }
}

运行程序,我们发现什么都没有打印,

改成后台线程之后,主线程飞快执行完了,于是进程结束,t线程还没来得及执行,就完了。

是否存活                  isAlive() 

package thread;

public class Demo7 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            System.out.println("线程开始");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程结束");
        });

        t.start();
        System.out.println(t.isAlive());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(t.isAlive());

    }

}

我们让主线程sleep3000 ,所以当sleep3000完成之后,那么这个线程执行完毕。我们打印       System.out.println(t.isAlive());

 如果线程正在运行,我们调用isAlive() 那么就打印true

如果线程结束,我们调用isAlive() 那么就打印false

true和线程开始,这两条日志,谁先打印,谁后打印不一定,因为线程是并发执行的,并发调度顺序不确定,取决于系统的调度器,(自己尝试,大概率先打印true,因为调用start之后,新的线程被创建也是有一定开销的,创建线程过程中,主线程就执行println)

但是无法排除极端情况,比如主线程正好卡了下,使新线程的日志先打印......

如果在t.start前调用isLive这个时候线程没被创建出来,自然也会打印false

  • 29
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祁思妙想

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值