多线程:创建线程和线程的常用方法

2022-08-02 13:15:15
分享一个朋友的人工智能教程(请以“右键”->"在新标签页中打开连接”的方式访问)。比较通俗易懂,风趣幽默,感兴趣的朋友可以去看看。

一:为什么要学多线程

  1. 应付面试 :多线程几乎是面试中必问的题,所以掌握一定的基础知识是必须的。
  2. 了解并发编程:实际工作中很少写多线程的代码,这部分代码一般都被人封装起来了,在业务中使用多线程的机会也不是很多(看具体项目),虽然代码中很少会自己去创建线程,但是实际环境中每行代码却都是并行执行的,同一时刻大量请求同一个接口,并发可能会产生一些问题,所以也需要掌握一定的并发知识

二:进程与线程

1. 进程

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

2. 线程

线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

一个正在运行的软件(如迅雷)就是一个进程,一个进程可以同时运行多个任务( 迅雷软件可以同时下载多个文件,每个下载任务就是一个线程), 可以简单的认为进程是线程的集合。

线程是一条可以执行的路径。

  • 对于单核CPU而言:多线程就是一个CPU在来回的切换,在交替执行。
  • 对于多核CPU而言:多线程就是同时有多条执行路径在同时(并行)执行,每个核执行一个线程,多个核就有可能是一块同时执行的。

3. 进程与线程的关系

一个程序就是一个进程,而一个程序中的多个任务则被称为线程。进程是表示资源分配的基本单位,又是调度运行的基本单位。,亦即执行处理机调度的基本单位。 进程和线程的关系:

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。

  • 处理机分给线程,即真正在处理机上运行的是线程。

  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

如果把上课的过程比作进程,把老师比作CPU,那么可以把每个学生比作每个线程,所有学生共享这个教室(也就是所有线程共享进程的资源),上课时学生A向老师提出问题,老师对A进行解答,此时可能会有学生B对老师的解答不懂会提出B的疑问(注意:此时可能老师还没有对A同学的问题解答完毕),此时老师又向学生B解惑,解释完之后又继续回答学生A的问题,同一时刻老师只能向一个学生回答问题(即:当多个线程在运行时,同一个CPU在某一个时刻只能服务于一个线程,可能一个线程分配一点时间,时间到了就轮到其它线程执行了,这样多个线程在来回的切换)

4. 为什么要使用多线程

多线程可以提高程序的效率。

实际生活案例:村长要求喜洋洋在一个小时内打100桶水,可以喜洋洋一个小时只能打25桶水,如果这样就需要4个小时才能完成任务,为了在一个小时能够完成,喜洋洋就请美洋洋、懒洋洋、沸洋洋,来帮忙,这样4只羊同时干活,在一小时内完成了任务。原本用4个小时完成的任务现在只需要1个小时就完成了,如果把每只羊看做一个线程,多只羊即多线程可以提高程序的效率。

5. 多线程应用场景

  • 一般线程之间比较独立,互不影响
  • 一个线程发生问题,一般不影响其它线程

三:多线程的实现方式

1. 顺序编程

顺序编程:程序从上往下的同步执行,即如果第一行代码执行没有结束,第二行代码就只能等待第一行执行结束后才能结束。

publicclassMain{// 顺序编程 吃喝示例:当吃饭吃不完的时候,是不能喝酒的,只能吃完晚才能喝酒publicstaticvoidmain(String[] args)throwsException{// 先吃饭再喝酒eat();drink();}privatestaticvoideat()throwsException{System.out.println("开始吃饭?...\t"+newDate());Thread.sleep(5000);System.out.println("结束吃饭?...\t"+newDate());}privatestaticvoiddrink()throwsException{System.out.println("开始喝酒?️...\t"+newDate());Thread.sleep(5000);System.out.println("结束喝酒?...\t"+newDate());}}

这里写图片描述

2. 并发编程

并发编程:多个任务可以同时做,常用与任务之间比较独立,互不影响。
线程上下文切换:

同一个时刻一个CPU只能做一件事情,即同一时刻只能一个线程中的部分代码,假如有两个线程,Thread-0和Thread-1,刚开始CPU说Thread-0你先执行,给你3毫秒时间,Thread-0执行了3毫秒时间,但是没有执行完,此时CPU会暂停Thread-0执行并记录Thread-0执行到哪行代码了,当时的变量的值是多少,然后CPU说Thread-1你可以执行了,给你2毫秒的时间,Thread-1执行了2毫秒也没执行完,此时CPU会暂停Thread-1执行并记录Thread-1执行到哪行代码了,当时的变量的值是多少,此时CPU又说Thread-0又该你,这次我给你5毫秒时间,去执行吧,此时CPU就找出上次Thread-0线程执行到哪行代码了,当时的变量值是多少,然后接着上次继续执行,结果用了2毫秒就Thread-0就执行完了,就终止了,然后CPU说Thread-1又轮到你,这次给你4毫秒,同样CPU也会先找出上次Thread-1线程执行到哪行代码了,当时的变量值是多少,然后接着上次继续开始执行,结果Thread-1在4毫秒内也执行结束了,Thread-1也结束了终止了。CPU在来回改变线程的执行机会称之为线程上下文切换。

publicclassMain{publicstaticvoidmain(String[] args){// 一边吃饭一边喝酒newEatThread().start();newDrinkThread().start();}}classEatThreadextendsThread{@Overridepublicvoidrun(){System.out.println("开始吃饭?...\t"+newDate());try{Thread.sleep(5000);}catch(InterruptedException e){
            e.printStackTrace();}System.out.println("结束吃饭?...\t"+newDate());}}classDrinkThreadextendsThread{@Overridepublicvoidrun(){System.out.println("开始喝酒?️...\t"+newDate());try{Thread.sleep(5000);}catch(InterruptedException e){
            e.printStackTrace();}System.out.println("结束喝酒?...\t"+newDate());}}

并发编程,一边吃饭一边喝酒总共用时5秒,比顺序编程更快,因为并发编程可以同时运行,而不必等前面的代码运行完之后才允许后面的代码

这里写图片描述

本示例主要启动3个线程,一个主线程main thread、一个吃饭线程(Thread-0)和一个喝酒线程(Thread-1),共三个线程, 三个线程并发切换着执行。main线程很快执行完,吃饭线程和喝酒线程会继续执行,直到所有线程(非守护线程)执行完毕,整个程序才会结束,main线程结束并不意味着整个程序结束。
这里写图片描述

  • 顺序:代码从上而下按照固定的顺序执行,只有上一件事情执行完毕,才能执行下一件事。就像物理电路中的串行,假如有十件事情,一个人来完成,这个人必须先做第一件事情,然后再做第二件事情,最后做第十件事情,按照顺序做。

  • 并行:多个操作同时处理,他们之间是并行的。假如十件事情,两个人来完成,每个人在某个时间点各自做各自的事情,互不影响

  • 并发:将一个操作分割成多个部分执行并且允许无序处理,假如有十件事情,如果有一个人在做,这个人可能做一会这个不想做了,再去做别的,做着做着可能也不想做了,又去干其它事情了,看他心情想干哪个就干哪个,最终把十件事情都做完。如果有两个人在做,他们俩先分一下,比如张三做4件,李四做6件,他们各做自己的,在做自己的事情过程中可以随意的切换到别的事情,不一定要把某件事情干完再去干其它事情,有可能一件事做了N次才做完。

通常一台电脑只有一个cpu,多个线程属于并发执行,如果有多个cpu,多线程并发执行有可能变成并行执行。
这里写图片描述

3. 多线程创建方式

  • 继承 Thread
  • 实现 Runable
  • 实现 Callable
①:继成java.lang.Thread, 重写run()方法
publicclassMain{publicstaticvoidmain(String[] args){newMyThread().start();}}classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"\t"+Thread.currentThread().getId());}}

Thread 类

packagejava.lang;publicclassThreadimplementsRunnable{// 构造方法publicThread(Runnable target);publicThread(Runnable target,String name);publicsynchronizedvoidstart();}

Runnable 接口

packagejava.lang;@FunctionalInterfacepublicinterfaceRunnable{
    pubicabstractvoidrun();}

②:实现java.lang.Runnable接口,重写run()方法,然后使用Thread类来包装

publicclassMain{publicstaticvoidmain(String[] args){// 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类MyRunnable runnable=newMyRunnable();newThread(runnable).start();}}classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"\t"+Thread.currentThread().getId());}}

可以看到两种方式都是围绕着Thread和Runnable,继承Thread类把run()写到类中,实现Runnable接口是把run()方法写到接口中然后再用Thread类来包装, 两种方式最终都是调用Thread类的start()方法来启动线程的。
两种方式在本质上没有明显的区别,在外观上有很大的区别,第一种方式是继承Thread类,因Java是单继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,在继承上有一点受制,有一点不灵活,第二种方式就是为了解决第一种方式的单继承不灵活的问题,所以平常使用就使用第二种方式

其它变体写法:

publicclassMain{publicstaticvoidmain(String[] args){// 匿名内部类newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"\t"+Thread.currentThread().getId());}}).start();// 尾部代码块, 是对匿名内部类形式的语法糖newThread(){@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"\t"+Thread.currentThread().getId());}}.start();// Runnable是函数式接口,所以可以使用Lamda表达式形式Runnable runnable=()->{System.out.println(Thread.currentThread().getName()+"\t"+Thread.currentThread().getId());};newThread(runnable).start();}}

③:实现Callable接口,重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread

Callable:有返回值的线程,能取消线程,可以判断线程是否执行完毕

publicclassMain{publicstaticvoidmain(String[] args)throwsException{// 将Callable包装成FutureTask,FutureTask也是一种RunnableMyCallable callable=newMyCallable();FutureTask<Integer> futureTask=newFutureTask<>(callable);newThread(futureTask).start();// get方法会阻塞调用的线程Integer sum= futureTask.get();System.out.println(Thread.currentThread().getName()+Thread.currentThread().getId()+"="+ sum);}}classMyCallableimplementsCallable<Integer>{@OverridepublicIntegercall()throwsException{System.out.println(Thread.currentThread().getName()+"\t"+Thread.currentThread().getId()+"\t"+newDate()+" \tstarting...");int sum=0;for(int i=0; i<=100000; i++){
            sum+= i;}Thread.sleep(5000);System.out.println(Thread.currentThread().getName()+"\t"+Thread.currentThread().getId()+"\t"+newDate()+" \tover...");return sum;}}

Callable 也是一种函数式接口

@FunctionalInterfacepublicinterfaceCallable<V>{Vcall()throwsException;}

FutureTask

publicclassFutureTask<V>implementsRunnableFuture<V>{// 构造函数publicFutureTask(Callable<V> callable);// 取消线程publicbooleancancel(boolean mayInterruptIfRunning);// 判断线程publicbooleanisDone();// 获取线程执行结果publicVget()throwsInterruptedException,ExecutionException;}

RunnableFuture

publicinterfaceRunnableFuture<V>extendsRunnable,Future<V>{voidrun();}

三种方式比较:

  • Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
  • Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
  • Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
  • 当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
  • Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

四:线程的状态

  1. 创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
  2. 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
  3. 运行(running)状态: 执行run()方法
  4. 阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
  5. 死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)

这里写图片描述

这里写图片描述

这里写图片描述

五:Thread常用方法

Thread

publicclassThreadimplementsRunnable{// 线程名字privatevolatileString name;// 线程优先级(1~10)privateint priority;// 守护线程privateboolean daemon=false;// 线程idprivatelong tid;// 线程组privateThreadGroup group;// 预定义3个优先级publicfinalstaticint MIN_PRIORITY=1;publicfinalstaticint NORM_PRIORITY=5;publicfinalstaticint MAX_PRIORITY=10;// 构造函数publicThread();publicThread(String name);publicThread(Runnable target);publicThread(Runnable target,String name);// 线程组publicThread(ThreadGroup group,Runnable target);// 返回当前正在执行线程对象的引用publicstaticnativeThreadcurrentThread();// 启动一个新线程publicsynchronizedvoidstart();// 线程的方法体,和启动线程没毛关系publicvoidrun();// 让线程睡眠一会,由活跃状态改为挂起状态publicstaticnativevoidsleep(long millis)throwsInterruptedException;publicstaticvoidsleep(long millis,int nanos)throwsInterruptedException;// 打断线程 中断线程 用于停止线程// 调用该方法时并不需要获取Thread实例的锁。无论何时,任何线程都可以调用其它线程的interruptf方法publicvoidinterrupt();publicbooleanisInterrupted()// 线程是否处于活动状态publicfinalnativebooleanisAlive();// 交出CPU的使用权,从运行状态改为挂起状态publicstaticnativevoidyield();publicfinalvoidjoin()throwsInterruptedExceptionpublicfinalsynchronizedvoidjoin(long millis)publicfinalsynchronizedvoidjoin(long millis,int nanos)throwsInterruptedException// 设置线程优先级publicfinalvoidsetPriority(int newPriority);// 设置是否守护线程publicfinalvoidsetDaemon(boolean on);// 线程idpubliclonggetId(){returnthis.tid;}// 线程状态publicenumState{// new 创建
        NEW,// runnable 就绪
        RUNNABLE,// blocked 阻塞
        BLOCKED,// waiting 等待
        WAITING,// timed_waiting
        TIMED_WAITING,// terminated 结束
        TERMINATED;}}
publicstaticvoidma
  • 作者:vbirdbest
  • 原文链接:https://monday.blog.csdn.net/article/details/81282163
    更新时间:2022-08-02 13:15:15