多线程01

文章目录

一、相关概念

1.1程序.

1.2进程.

1.3线程.

1.4查看进程和线程.

1.5线程调度.

1.6多线程程序的优点.

1.7单核CPU和多核CPU.

1.8并行与并发.

1.8.1并行.

1.8.2并发.

1.8.3总结.

1.9线程分类.

二、创建线程的方式.

2.1概述.

2.2创建线程的几种方式.

3.继承Thread类方式.

3.1使用步骤.

3.2代码演示.

3.3变形写法.

3.3.1在同一个文件里面创建Thread子类.

3.3.2匿名内部类.

3.4注意事项.

4.实现Runable接口方式.

4.1使用步骤.

4.2代码演示.

4.3变形写法.

4.3.1在同一个文件里面创建Runnable接口实现类.

4.3.2匿名内部类方式.

4.4注意事项.

5.继承Thread类和实现Runnable接口两种方式对比.

6.相关方法.

6.1设置线程名.

6.2设置线程休眠- .

6.3设置&获取线程优先级.

6.4join方法.

6.5yield方法.

6.6stop方法.

6.7suspend&resume方法.

6.8wait¬ify¬ifyAll方法.

6.9isAlive方法.

6.9.1案例1-判断线程死亡之后是否还处于活动状态.

6.9.2案例2-判断线程sleep过程中是否处于活动状态.

6.9.3案例3-判断线程wait是否处于活动状态.

6.9.4案例4-判断线程被join后是否处于活动状态.

7.0 interrupt&interrupted&isinterrupted方法的区别.

7.0.1interrupt方法.

7.0.2interrupted方法.

7.0.isinterrupted方法.

7.线程的生命周期.

7.1JDK1.5之前.

7.2JDK1.5开始.


一、相关概念

1.1程序.

  • 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象(下载的软件里面的各种系统文件)。

  • 1.2进程.

  • 进程(process):

    • 程序的一次执行过程,或是正在内存中运行的应用程序。如:运行的Eclipse、QQ音乐等。

    • 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程(生命周期)。

    • 进程到运行不仅仅需要CPU,还需要很多其它资源,比如:内存、显卡、磁盘等。

    • 程序是静态的,进程是动态的。

    • 进程是操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位。一定要和线程区分开),系统在运行时会为每个进程分配不同的内存区域和时间片。操作系统调度器:拆分CPU为一段段时间片的运行片,轮流分配给不同的程序。

    • 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:一边打游戏,一边使用QQ聊天,一边使用录屏软件,同时还开着画图板,dos窗口等软件。

    • 不同进程之间是不共享运行时内存的,但是进程和进程之间是可以进行数据交互和通信,成本很高。如:在京东里面通过微信付款、在淘宝里面通过支付宝付款。

    • 1.3线程.

    • 线程(thread):

      • 进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。

      • 一个进程同一时间若并行执行多个线程,就是支持多线程的。

      • 线程作为CPU调度和分配的最小单位(一定要和进程区分开)。有句话说CPU只能看到线程,可以这么理解,假设我是CPU,我闭着眼,操作系统调度器将一个进程分配给我之后,我拿到进程睁开眼,我看到的是什么?我看到的是进程中的很多线程,那么我现在能调度和分配的是什么?进程?不行,因为我看不到其他进程,何来调度分配,只能调度我看到的那些线程,如果我是4核的话,把线程ABCD分配到核心1234,其他的线程依然要等待分配,至于等待多久,如何分配,暂不在本文讨论范围。于是线程是CPU调度和分配的基本单位。

      • 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

      • 下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。

    • 1.4查看进程和线程.

    • 电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程。

    • 1. 每个运行的应用程序都是一个进程:

      2. 一个应用程序的多次运行,就是多个进程:

      3. 一个进程中包含多个线程:


      1.5线程调度.

    • 分时调度:所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。

    • 抢占式调度:让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。


    • 1.6多线程程序的优点.

    • 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。

    • 提高计算机系统CPU的利用率。

    • 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。


    • 1.7单核CPU和多核CPU.

    • 单核CPU,在一个时间单元内,只能执行一个线程的任务。例如,可以把CPU看成是医院的医生诊室,在一定时间内只能给一个病人诊断治疗。所以单核CPU就是,代码经过前面一系列的前导操作(类似于医院挂号,比如有10个窗口挂号),然后到cpu处执行时发现,就只有一个CPU(对应一个医生),大家排队执行。这时候想要提升系统性能,只有两个办法,要么提升CPU性能(让医生看病快点),要么多加几个CPU(多整几个医生),即为多核的CPU。

    • 问题:多核的效率是单核的倍数吗?譬如4核A53的cpu,性能是单核A53的4倍吗?理论上是,但是实际不可能,至少有两方面的损耗。

      1. 一个是多个核心的其他共用资源限制。譬如,4核CPU对应的内存、cache、寄存器并没有同步扩充4倍。这就好像医院一样,1个医生换4个医生,但是做B超检查的还是一台机器,性能瓶颈就从医生转到B超检查了。

      2. 另一个是多核CPU之间的协调管理损耗。譬如多个核心同时运行两个相关的任务,需要考虑任务同步,这也需要消耗额外性能。好比公司工作,一个人的时候至少不用开会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以工作效率绝对不可能达到2倍。

    • 参考资料:多核CPU和多个CPU的区别:https://www.zhihu.com/question/20998226。


    • 1.8并行与并发.

      1.8.1并行.

    • 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个CPU上同时执行。比如:多个人同时做不同的事。

    • 比如,A、B、C三个指令同时执行:


    • 1.8.2并发.

    • 并发(concurrency):只是一种现象,指两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令在单个CPU上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。

    • 比如,A、B、C三个指令在同一时间段内快速切换执行:

    • 解释一:

      多线程并发只是表面和感觉上的并发,并不是实质上的并发。一个线程要运行,它必须占有CPU,而我们目前用的计算机大多都是单CPU的,所以一次最多只能有一个线程获取CPU并运行。

      多线程的实质是“最大限度地利用CPU资源”,当某一个线程的处理不需要占用CPU而只需要和I/O等资源打交道时,让其他线程有机会获得CPU资源。这有点类似于“统筹方法”,例如让你打扫房子和烧水,要在最短的时间内做好这两件事,你一定会想到先把水烧上,然后在等水烧开的空闲时间中去打扫房子,而不是先打扫好了再去烧水,也不是先烧好了再去打扫,这个例子里面,你是那个唯一的CPU,而烧水和打扫就是两个线程。

      虽然CPU只有一个,但是它在多个线程之间频繁切换,当切换的频率高到一定程度时,我们就感觉所有的线程在同时运行,于是感觉这多个线程是并发的。因此,并发并不是真的指多个线程同时运行,它仅仅只是形容一种现象。

      解释二:

      在操作系统中,启动了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单核 CPU 系统中,每一时刻只能有一个程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。宏观并行,微观其实是串行(排队)。 而在多核 CPU 系统中,则这些可以并发执行的程序便可以分配到多个CPU上,实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序才可以真正的同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。


      1.8.3总结.

    • 并发只是一种程序在运行时的现象效果(宏观并行,微观串行),和串行、并行没有可比性。串行即排队,并行才是真正的多个程序同时执行。

    • 单CPU中进程只能是并发,多CPU计算机中进程可以并行也可以并发。

    • 单CPU单核中线程只能并发,单CPU多核中线程可以并行也可以并发。

    • 无论是并发还是并行,使用者来看,看到的是多进程,多线程。


    • 1.9线程分类.

    • JAVA中的线程主要分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。

      1. 用户线程:AVA语言中无论是线程还是线程池,默认都是用户线程,用户线程也被称为普通线程。

        • main线程也是用户线程,又叫主线程,是由JVM创建的。

        • 用户线程创建的子线程默认是用户线程,可以设置为守护线程:setDaemon(true)。比如在main()方法(主线程)里面创建的线程就是子线程。

      2. 守护线程:也被称之为后台线程、服务线程或精灵线程。

        • 守护线程是为用户线程服务的,当线程中的用户线程都执行结束后,守护线程也会跟随结束。

        • 守护线程具有自动结束生命周期的特性,而非守护线程则不具备该特性。

        • 守护线程的优先级和用户线程优先级一致;

        • 守护线程的子线程也是守护线程。

        • 垃圾回收线程就是典型的守护线程,随主线程结束而结束。

    • 设置用户线程为守护线程方法:setDaemon(true) ,该方法必须在 start()方法之前设置, 如果设置在 start() 之后,程序执行会报错,守护线程也不会生效。

    • 判断线程是否是守护线程方法:isDaemon();

    • JVM必须确保所有的用户线程执行结束之后才能退出;如果只还有守护线程在运行,JVM不用等待守护线程执行结束,会直接退出。验证代码如下:

二、创建线程的方式.

2.1概述.

  • Java语言的JVM允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。

  • Thread类的特性:

    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为线程执行体。

    • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run(),要想实现多线程,必须在主线程中创建新的线程对象。


2.2创建线程的几种方式.

  1. 继承Thread类;

  2. 实现Runnable接口;

  3. 实现Callable接口(JDK1.5出现的);

  4. 使用线程池(JDK1.5出现的)。


3.继承Thread类方式.

3.1使用步骤.

  • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务;

  • 创建Thread子类的实例,即创建了线程对象;

  • 调用线程对象的start()方法来启动该线程。


3.2代码演示.

  • 创建 Thread类的子类:

  • 测试类:

  • 线程运行效果图:


3.3变形写法.

  • 说明:在有时候我们只是临时使用一次线程,此时就没必要专门定义线程类,浪费资源。那么我们就可以通过以下两种写法实现多线程。

3.3.1在同一个文件里面创建Thread子类.


3.3.2匿名内部类.

补充lambda表达式写法。


3.4注意事项.

  1. 子线程之间的运行时随机的,到底运行谁,由CPU分配时间片决定。

  2. 不是main线程执行结束后才执行子线程。程序是先从main方法开始执行的(即从main线程开始执行),如果main方法里执行了其他线程的start方法,其它线程就会进入就绪状态,这时就看CPU分配时间片给谁,谁就执行。

  3. 如果自己手动通过线程对象调用run()方法,那么就只是调用一个普通方法,没有启动多线程模式。  

  4. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。   

  5. 想要启动多线程,必须调用start方法。   

  6. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出异常“IllegalThreadStateException”


4.实现Runable接口方式.

  • Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法。

4.1使用步骤.

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

  2. 创建Runnable实现类的实例,并以此实例作为Thread的target参数(线程要执行的内容)来创建Thread对象,该Thread对象才是真正的线程对象。

  3. 调用线程对象的start()方法,启动线程。调用Runnable接口实现类的run方法。


4.2代码演示.

  • 定义Runnable接口实现类.

  • 测试:


4.3变形写法.

4.3.1在同一个文件里面创建Runnable接口实现类.


4.3.2匿名内部类方式.

  • 写法一:未使用Lambda表达式。

  • 写法二:使用Lambda表达式。


4.4注意事项.

  1. 通过实现Runnable接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在run方法里面。

  2. 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

  3. 实际上,所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

  4. 说明:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。可以理解为 Runnable对象就是一个数据体,里面包含了线程要执行的内容。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。


5.继承Thread类和实现Runnable接口两种方式对比.

  1. Thread类实际上也是实现了Runnable接口的类。

  2. 继承Thread方式适合线程之间处理各自任务(不共享资源)情况。

  3. 实现Runnable接口比继承Thread类所具有的优势:

    1. 避免了单继承的局限性;

    2. 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源;

    3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。


6.相关方法.

6.1设置线程名.

  • 不设置线程名,默认是:Thread-0、Thread-1、Thread-2....这样命名。可以在线程的 run方法里面获取当前线程名测试 :Thread.currentThread().getName()

  • 手动设置线程名:

    • 继承Thread类方式:

      • 通过创建的线程对象调用:setName("线程名");方法进行设置;

      • 在线程类里面定义带参构造函数,然后调用父类构造函数。如:

    • 实现Runnable接口方式:


6.2设置线程休眠- .

  1. Thread.sleep():是Thread类的一个静态方法。作用:使当前线程休眠,进入阻塞状态(暂停执行),简单理解就是“线程暂停方法”。该方法可接收长整型毫秒和整型纳秒,可读性差(因为需要根据毫秒数或者纳秒数换算成其它时间单位)。

  2. TimeUnit.SECONDS.sleep():是 concurrent 包下的一个类,提供了可读性更好的线程暂停操作。里面提供了很多枚举类,如:

枚举

单位

DAYS

HOURS

小时

MINUTES

分钟

SECONDS

MILLISECONDS

毫秒(千分之一秒)

MICROSECONDS

微秒(一百万分之一秒(就是毫秒/1000))

NANOSECONDS

毫微秒(十亿分之一秒(就是微秒/1000))

  • 使用方式如下:

  • 除了枚举之外可以进行时间转换:


6.3设置&获取线程优先级.

  • getPriority() :返回线程优先级;

  • setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。


6.4join方法.

  • join方法的作用是:在当前 线程执行的某个阶段,加入并执行其它线程。

  • 案例:


6.5yield方法.

  • yield方法的作用是当前正在运行的线程放弃CPU资源,重新回到就绪状态.

  • 案例:


6.6stop方法.

  • stop():已过时,不建议使用。强行杀死执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。比如转账转了一半,突然停止。

  • 案例演示:

  • 运行结果:

  • 当线程2运行到第10次的时候就会暴力停止线程1的运行。但是从结果上发现,当线程2运行完第10次后,线程1又运行了一次,然后才被停止的,这是因为当线程2执行完第10次的输出语句后,还没来得及执行暴力停止线程1的代码,CPU马上给线程1分配时间片,线程1运行,然后CPU再给线程2分配时间片,线程2运行,马上暴力停止线程1.

  • 这样会导致线程1在停止的时候没有来得及处理后事,不安全。就好比,你在工厂里面生成产品,别人突然叫你停止生产,连给你关闭设备的时间都没有留。正确的应该是先提前通知你要停止生产了,给你时间关闭设备。


6.7suspend&resume方法.

  • suspend() & resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。已过时,不建议使用。取而代之的是wait()、notify()、notifyAll().


6.8wait&notify&notifyAll方法.

  • wait():线程等待,可以传入参数指定等待时间。线程等待的时候会释放当前占用的锁,这也是和suspend方法最大的区别。

  • notify:唤醒当前线程;

  • notifyAll:唤醒所有线程。


6.9isAlive方法.

  • isAlive方法是判断线程是不是处于活动状态。

  • 注,如果线程处于阻塞状态也属于活动状态。

6.9.1案例1-判断线程死亡之后是否还处于活动状态.

  • 案例如下:

  • 一开始线程处于活动状态,当2秒过后线程已经运行结束,就处于未活动状态(死亡了)。


6.9.2案例2-判断线程sleep过程中是否处于活动状态.

  • 一开始线程肯定处于活动状态,当线程运行一次,然后休眠10秒钟(sleep),再休眠未结束期间线程仍处于活动状态。


6.9.3案例3-判断线程wait是否处于活动状态.

  • 线程运行到第五秒的时候处于等待状态(wait),但仍是活动状态。


6.9.4案例4-判断线程被join后是否处于活动状态.

  • 一开始线2程肯定处于活动状态,当线程1加入执行的时候,线程2处于阻塞状态(join),但仍然是活动状态。


7.0 interrupt&interrupted&isinterrupted方法的区别.

7.0.1interrupt方法.

  • interrupt 方法是打断活动状态的线程。

  • 详解:要中断一个Java线程,可调用线程类(Thread)对象的实例方法:interrupte();然而interrupte()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。

    • 情况1:如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:

      1. 如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException;

      2. 如果是java.nio.channels.InterruptibleChannel进行的io操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException;(待验证);

      3. 如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常。(待验证)。

    • 情况2:如果线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true,则此后,一旦线程调用了wait、jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false。

  • 通过上面的分析,我们可以总结,调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。

  • 补充:interrupte方法的调用,该方法可在需要中断的线程本身中调用,也可在其他线程中调用需要中断的线程对象的该方法。


  • 案例演示1-打断非阻塞状态线程,然后在该线程内部通过判断打断标记停止线程,并处理后事和获取打断标记.

  • 执行效果如下:

  • 线程被打断后打断标记变为 true,这就是打断非休眠状态下的线程效果。


  • 案例演示2-打断阻塞状态线程,并获取打断过后的打断 .

  • 运行效果如下:

  • 从结果上可以发现,线程在休眠前和休眠被断后的打断标记都是false,这是因为 interrupt方法在打断休眠状态的线程后又会把线程的打断标记重置为false。


7.0.2interrupted方法.

  • interrupted()方法是 Thread类里面的静态方法,判断当前执行线程是否停止,会获取先的打断标记,获取完之后又会重置该线程的打断标记为:false。

  • 举例:当一条线程被打断后,如果执行两次interrupted()方法获取线程的打断标记,第一次输出的是 true,第二次输出的则是 false。


7.0.isinterrupted方法.

  • 是对象方法,判断该对象是否已经停止,会获取线程的打断标记,但不会重置线程的打断标记。

  • 该方法和interrupted()方法的区别就是该方法不会重置线程的打断标记为false,而interrupted()方法则会重置为false。


7.线程的生命周期.

7.1JDK1.5之前.

  • 新建: 刚new出来在内存中

  • 就绪: start表示该线程有资格抢CPU!

  • 运行: 抢到了CPU,会执行run方法

  • 阻塞: 调用了sleep方法或其他阻塞的方法

  • 死亡: 执行完run方法


7.2JDK1.5开始.

  • 6个阶段


8.例子

  1. 设计两条线程,分别打印1-100之间的偶数和奇数.


  1. 例子2

    • 声明一个匿名内部类继承Thread类,重写run方法,实现打印[1,100]之间的偶数,要求每隔1秒打印1个偶数。

    • 声明一个匿名内部类继承Thread类,重写run方法,实现打印[1,100]之间的奇数,

      • 当奇数线程打印到5时,让奇数线程暂停一下,再继续。

      • 当奇数线程打印到5时,让奇数线程停下来,让偶数线程执行完再打印。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

瑶大头*^_^*

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

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

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

打赏作者

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

抵扣说明:

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

余额充值