首页 > 技术文章 > Java 多线程 - 总结概述

gltou 2021-11-16 09:36 原文

概述

菜鸟教程:

  • Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
  • 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
  • 这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
  • 多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

大白话:

  • 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程式CPU调度和执行的单位
  • 说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
  • 进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位
  • 注意:很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉

知识点:

  • 线程就是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程main(),gc()垃圾回收线程;
  • main()称之为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排执行,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如cpu调度事件,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

 

一个线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

下图显示了一个线程完整的生命周期。

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 终止状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

 

 

线程的优先级

  • 每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
  • 线程开启不一定立即执行,由cpu调度
  • Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
  • 默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
  • 具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
  • 在下面的线程状态节点具体讲述

 

创建一个线程

Java 提供了三种创建线程的方法:

  • 通过继承 Thread 类本身;(重点)
  • 通过实现 Runnable 接口;(重点)
  • 通过 Callable 和 Future 创建线程;(重点)

继承 Thread 类创建线程

  • 创建一个线程的第一种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
  • 继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
  • 该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
 1 //创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
 2 public class TestThread01 extends Thread {
 3     @Override
 4     public void run() {
 5         //run方法线程体
 6         for (int i = 0; i < 3; i++) {
 7             System.out.println("好好学习");
 8         }
 9     }
10 
11     public static void main(String[] args) {
12         //main线程,主线程
13 
14         //创建一个线程对象
15         TestThread01 testThread01 = new TestThread01();
16 
17         //testThread01.run();执行run方法,没有开启线程,依旧是自上而下执行代码
18 
19         //调用start()方法,开启线程,run方法体线程和main()主线程根据调度交叉执行
20         testThread01.start();
21         for (int i = 0; i <3 ; i++) {
22             System.out.println("天天向上");
23         }
24     }
25 }

Thread线程练习-网图下载

前置条件:

  • 下载好之后放到项目的lib目录下,没有lib目录的,自己新增一个。

  • 右键lib目录,选择Add as Library,点击OK即可

 1 //练习Thread,实现多线程下载图片
 2 public class TestThread2 extends Thread{
 3     private String url;
 4     private String name;
 5 
 6     public TestThread2(String url,String name){
 7         this.url=url;
 8         this.name=name;
 9     }
10 
11     //下载图片线程的执行体
12     @Override
13     public void run() {
14         WebDownloader webDownloader = new WebDownloader();
15         webDownloader.downloader(url,name);
16         System.out.println("下载了文件名为:"+name);
17     }
18 
19     public static void main(String[] args) {
20         TestThread2 t1 = new TestThread2("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-1.jpg");
21         TestThread2 t2 = new TestThread2("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-2.jpg");
22         TestThread2 t3 = new TestThread2("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-3.jpg");
23 
24         t1.start();
25         t2.start();
26         t3.start();
27     }
28 }
29 
30 //下载器
31 class WebDownloader{
32     //下载方法
33     public void downloader(String url,String name){
34         try {
35             FileUtils.copyURLToFile(new URL(url),new File(name));
36         } catch (IOException e) {
37             e.printStackTrace();
38             System.out.println("IO异常,download方法出现问题");
39         }
40     }
41 }

 


实现 Runnable 接口创建线程

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。

为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:

public void run()

可以重写该方法,重要的是理解的 run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。

在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。

Thread 定义了几个构造方法,下面的这个是我们经常使用的:

Thread(Runnable threadOb,String threadName);

这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。

新线程创建之后,你调用它的 start() 方法它才会运行。

void start();
 1 //创建线程方式二:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法启动线程
 2 public class TestThread03 implements Runnable{
 3     @Override
 4     public void run() {
 5         for (int i = 0; i < 200; i++) {
 6             System.out.println("好好学习"+i);
 7         }
 8     }
 9 
10     public static void main(String[] args) {
11         //创建runnable接口的实现类对象
12         TestThread03 testThread03 = new TestThread03();
13         //创建线程对象,通过线程对象来开启我们的线程,代理
14         new Thread(testThread03).start();
15 
16         for (int i = 0; i < 200; i++) {
17             System.out.println("天天向上"+i);
18         }
19     }
20 }

Runnable 接口创建线程练习-下载网图

 1 public class TestThread04 implements Runnable{
 2 
 3     private String url;
 4     private String name;
 5 
 6     public TestThread04(String url,String name){
 7         this.url=url;
 8         this.name= name;
 9     }
10 
11     @Override
12     public void run() {
13         new WebDownLoad01(url,name);
14         System.out.println("下载了文件名:"+name);
15     }
16 
17     public static void main(String[] args) {
18         TestThread04 t1 = new TestThread04("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-1.jpg");
19         TestThread04 t2 = new TestThread04("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-2.jpg");
20         TestThread04 t3 = new TestThread04("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-3.jpg");
21 
22         new Thread(t1).start();
23         new Thread(t2).start();
24         new Thread(t3).start();
25     }
26 }
27 //下载器
28 class WebDownLoad01{
29     public WebDownLoad01(String url,String name){
30         try {
31             FileUtils.copyURLToFile(new URL(url),new File(name));
32         } catch (IOException e) {
33             e.printStackTrace();
34             System.out.println("IO异常,下载失败");
35         }
36     }
37 }

 Runnable 接口创建线程练习-多个线程同时操作同一个对象

买火车票的例子

 1 //多个线程同时操作同一个对象
 2 //买火车票的例子
 3 public class TestThread05 implements Runnable{
 4 
 5     //站台总票数
 6     private  int ticketNums=10;
 7 
 8     @Override
 9     public void run() {
10         while (true){
11             //没有票时退出
12             if(ticketNums<=0){
13                 break;
14             }
15 
16             //模拟延时
17             try {
18                 Thread.sleep(200);
19             } catch (InterruptedException e) {
20                 e.printStackTrace();
21             }
22 
23             System.out.println(Thread.currentThread().getName()+"-->拿到了第:"+ticketNums--+"票");      //Thread.currentThread().getName()获得当前执行线程的名字
24         }
25     }
26 
27     public static void main(String[] args) {
28         TestThread05 testThread05 = new TestThread05();
29 
30         new Thread(testThread05,"小明").start();//小明:线程的名字
31         new Thread(testThread05,"小张").start();
32         new Thread(testThread05,"黄牛").start();
33     }
34 }

输出结果,发现同一张票被两个人买走了,发现了线程的问题:多个线程操作同一个资源的时候,线程不安全,数据紊乱,后面线程几个重要概念中的线程安全会有具体讲述

Runnable 接口创建线程练习-龟兔赛跑

 1 //模拟龟兔赛跑
 2 public class Race implements Runnable{
 3 
 4     // 胜利者
 5     private static String winner;
 6 
 7     //线程运行体
 8     @Override
 9     public void run() {
10         for (int i = 0; i <= 100; i++) {
11             //判断比赛是否结束
12             boolean b = gameOver(i);
13             //如果有胜利者了,就停止程序
14             if (b){
15                 break;
16             }
17             System.out.println(Thread.currentThread().getName()+"跑了"+i+"米");
18         }
19     }
20 
21     //判断是否完成比赛
22     private boolean gameOver(int steps){
23         //判断是否有胜利者
24         if (winner!=null){
25             return true;
26         }{
27           if (steps>=100) {
28               winner=Thread.currentThread().getName();
29               System.out.println("winner is :"+winner);
30               return true;
31           }
32         }
33         return false;
34     }
35 
36     public static void main(String[] args) {
37         Race race = new Race();
38 
39         new Thread(race,"乌龟").start();
40         new Thread(race,"兔子").start();
41     }
42 }

 


实现Callable接口创建线程

了解即可

  1. 实现Callable接口,需要返回值类型;
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务ExecutorService ser = Executors.newFixedThreadPool();
  5. 提交执行Future<Boolean> r1 = ser.submit(t1);
  6. 获得结果Boolean rs1 = r1.get();
  7. 关闭服务ser.shutdown();
 1 //线程创建方式三:实现Callable接口
 2 public class TestCallable implements Callable {
 3     private String url;
 4     private String name;
 5 
 6     public TestCallable(String url,String name){
 7         this.url=url;
 8         this.name=name;
 9     }
10 
11     @Override
12     public Object call() throws Exception {
13         WebDownload webDownload = new WebDownload(url,name);
14         System.out.println(name+"文件已下载");
15         return true;
16     }
17 
18     public static void main(String[] args) throws ExecutionException, InterruptedException {
19         TestCallable t1 = new TestCallable("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-1.jpg");
20         TestCallable t2 = new TestCallable("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-2.jpg");
21         TestCallable t3 = new TestCallable("https://www.baidu.com/s?wd=%E7%99%BE%E5%BA%A6%E7%83%AD%E6%90%9C&sa=ire_dl_gh_logo_texing&rsv_dl=igh_logo_pc","百度图片-3.jpg");
22 
23         //创建执行服务
24         ExecutorService ser = Executors.newFixedThreadPool(3);
25 
26         //提交执行
27         Future<Boolean> r1 = ser.submit(t1);
28         Future<Boolean> r2 = ser.submit(t2);
29         Future<Boolean> r3 = ser.submit(t3);
30 
31         //获取结果
32         Boolean rs1 = r1.get();
33         Boolean rs2 = r2.get();
34         Boolean rs3 = r3.get();
35 
36         //关闭服务
37         ser.shutdown();
38     }
39 }
40 
41 //下载器
42 class WebDownload{
43     public WebDownload(String url,String name){
44         try {
45             FileUtils.copyURLToFile(new URL(url),new File(name));
46         } catch (IOException e) {
47             e.printStackTrace();
48             System.out.println("IO异常,下载失败");
49         }
50     }
51 }

创建线程的三种方式的对比

  • 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

  • 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

 


拓展

静态代理

  • 真实对象和代理对象都要实现同一个接口
  • 代理对象要代理真实角色
  • #好处
  • 代理对象可以做很多真实对象做不了的事情
  • 真实对象专注做自己的事情
 1 //以结婚找婚庆公司举例
 2 public class StaticProxy {
 3     public static void main(String[] args) {
 4         WeddingCompany weddingCompany = new WeddingCompany(new You());
 5         weddingCompany.HappyMarry();
 6     }
 7 }
 8 
 9 //同一个接口———结婚这件事情
10 interface Marry{
11     void HappyMarry();
12 }
13 
14 //真实对象————结婚的主人公
15 class You implements Marry{
16     @Override
17     public void HappyMarry() {
18         System.out.println("张三要结婚了");
19     }
20 }
21 
22 //代理对象————结婚场景布置找的婚庆公司,代理角色
23 class WeddingCompany implements Marry{
24 
25     //代理的主人公,结婚的主人公,给谁帮忙的
26     private Marry target;
27 
28     public WeddingCompany(Marry target){
29         this.target= target;
30     }
31 
32     @Override
33     public void HappyMarry() {
34         before();
35         this.target.HappyMarry();
36         after();
37     }
38 
39     private void before() {
40         System.out.println("结婚之前布置现场");
41     }
42 
43     private void after() {
44         System.out.println("结婚之后收尾款");
45     }
46 
47 
48 }
  • 反观线程Thread和Runable,Thread类其实是实现Runable接口的,Thread类相当于就是一个静态代理,完成Runable许多完成不了的事情,比如使用Runable创建线程,最后运行线程使用的是代理Thread的start方法;Runnabe接口启动线程就是是用了静态代理的方式
  • Runnable接口和Thread代理都有run方法,最后调用的是Thread的start方法,但实际执行的还是Runnable中的run方法中的方法体

Lambda表达式

好处:

  • 避免匿名内部类定义过多
  • 其实质属于函数式编程的概念
  • 例子:new Thread(()-> System.out.println("多线程学习")).start();

函数式接口:

  • 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口,比如

  • 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象

推导lambda表达式

正常写法

 1 /*
 2 推导lambda表达式--正常写法
 3  */
 4 public class TestLambda01 {
 5     public static void main(String[] args) {
 6         ILike like = new Like();
 7         like.lambda();
 8     }
 9 }
10 
11 //1.定义一个函数式接口
12 interface ILike{
13     void lambda();
14 }
15 
16 //2.实现类
17 class Like implements ILike{
18     @Override
19     public void lambda() {
20         System.out.println("i like lambda");
21     }
22 }

静态内部类

 1 /*
 2 推导lambda表达式--优化方法:静态内部类
 3  */
 4 public class TestLambda01 {
 5 
 6     public static void main(String[] args) {
 7         ILike like = new Like();
 8         like.lambda();
 9 
10         like = new Like1();
11         like.lambda();
12     }
13 
14     //3.静态内部类
15     static class Like1 implements ILike{
16         @Override
17         public void lambda() {
18             System.out.println("i like lambda1-静态内部类");
19         }
20     }
21 }
22 
23 //1.定义一个函数式接口
24 interface ILike{
25     void lambda();
26 }
27 
28 //2.实现类
29 class Like implements ILike{
30     @Override
31     public void lambda() {
32         System.out.println("i like lambda");
33     }
34 }

局部内部类

 1 /*
 2 推导lambda表达式--优化方法:局部内部类
 3  */
 4 public class TestLambda01 {
 5 
 6     public static void main(String[] args) {
 7         ILike like = new Like();
 8         like.lambda();
 9 
10         //3.静态内部类
11         like = new Like1();
12         like.lambda();
13 
14         //4.局部内部类
15         class Like2 implements ILike{
16             @Override
17             public void lambda() {
18                 System.out.println("i like lambda2-局部内部类");
19             }
20         }
21 
22         //4.局部内部类
23         like = new Like2();
24         like.lambda();
25     }
26 
27     //3.静态内部类
28     static class Like1 implements ILike{
29         @Override
30         public void lambda() {
31             System.out.println("i like lambda1-静态内部类");
32         }
33     }
34 }
35 
36 //1.定义一个函数式接口
37 interface ILike{
38     void lambda();
39 }
40 
41 //2.实现类
42 class Like implements ILike{
43     @Override
44     public void lambda() {
45         System.out.println("i like lambda");
46     }
47 }

匿名内部类

 1 /*
 2 推导lambda表达式--优化方法:匿名内部类
 3  */
 4 public class TestLambda01 {
 5 
 6     public static void main(String[] args) {
 7         ILike like = new Like();
 8         like.lambda();
 9 
10         //3.静态内部类
11         like = new Like1();
12         like.lambda();
13 
14         //4.局部内部类
15         class Like2 implements ILike{
16             @Override
17             public void lambda() {
18                 System.out.println("i like lambda2-局部内部类");
19             }
20         }
21 
22         //4.局部内部类
23         like = new Like2();
24         like.lambda();
25 
26         //5.匿名内部类,没有类的名称,必须借助接口或者父类
27         like = new ILike() {
28             @Override
29             public void lambda() {
30                 System.out.println("i like lambda3-匿名内部类");
31             }
32         };
33         //5.匿名内部类
34         like.lambda();
35     }
36 
37     //3.静态内部类
38     static class Like1 implements ILike{
39         @Override
40         public void lambda() {
41             System.out.println("i like lambda1-静态内部类");
42         }
43     }
44 }
45 
46 //1.定义一个函数式接口
47 interface ILike{
48     void lambda();
49 }
50 
51 //2.实现类
52 class Like implements ILike{
53     @Override
54     public void lambda() {
55         System.out.println("i like lambda");
56     }
57 }

思考:怎么还能将它再简化呢,于是乎,JDK1.8出了个lambda表达式

 1 /*
 2 推导lambda表达式--优化方法:lambda表达式
 3  */
 4 public class TestLambda01 {
 5 
 6     public static void main(String[] args) {
 7         ILike like = new Like();
 8         like.lambda();
 9 
10         //3.静态内部类
11         like = new Like1();
12         like.lambda();
13 
14         //4.局部内部类
15         class Like2 implements ILike{
16             @Override
17             public void lambda() {
18                 System.out.println("i like lambda2-局部内部类");
19             }
20         }
21 
22         //4.局部内部类
23         like = new Like2();
24         like.lambda();
25 
26         //5.匿名内部类,没有类的名称,必须借助接口或者父类
27         like = new ILike() {
28             @Override
29             public void lambda() {
30                 System.out.println("i like lambda3-匿名内部类");
31             }
32         };
33         //5.匿名内部类
34         like.lambda();
35 
36         //6.lambda简化
37         like = ()->{
38             System.out.println("i like lambda4-lambda简化");
39         };
40         //6.lambda简化
41         like.lambda();
42     }
43 
44     //3.静态内部类
45     static class Like1 implements ILike{
46         @Override
47         public void lambda() {
48             System.out.println("i like lambda1-静态内部类");
49         }
50     }
51 }
52 
53 //1.定义一个函数式接口
54 interface ILike{
55     void lambda();
56 }
57 
58 //2.实现类
59 class Like implements ILike{
60     @Override
61     public void lambda() {
62         System.out.println("i like lambda");
63     }
64 }

lambda例子

 1 //例子-带参数的lambda
 2 public class TestLambda02 {
 3     public static void main(String[] args) {
 4 
 5         LunchEat lunchEat = (String name)->{
 6             System.out.println("中午吃"+name);
 7         };
 8         
 9         lunchEat.eat("牛肉");
10     }
11 }
12 
13 interface LunchEat {
14     void eat(String name);
15 }

疑问:上述的lambda还能不能再简化?可以,请看下面代码

推荐阅读