多线程学习,看这一篇就够了

多线程作为面试必问点,必须系统的深入学习才能经受住面试官的拷打。

笔者认为不管学什么,都是先要有个整体的认知,再逐步的分解学习,以达到化难为简的目的。下表是我们接下来要学习的技能点, *** 最重要,先整体了解,之后再逐步学习。

技能点名称 难易程度 认知程度 重要程度
进程和线程 理解 **
线程的创建和启动 应用 ***
线程生命周期 理解 ***
线程控制 理解 *
线程同步 应用 ***
同步代码块 应用 ***
同步方法 应用 ***
Reentrant Lock锁 应用 ***
线程通信 应用 **
线程组 了解 *
线程池 理解 **

1.线程基础内容

1.1程序、进程与线程

程序: 程序是一段静态的代码,一般存储在硬盘

进程: 进程是指一种正在运行程序,一般运行在内存

  • 进程的特点

    • 动态性 :运行中的程序
    • 并发性:进程同时运行
    • 独立性:独立,互不干扰
  • 并发和并行区别

并行(parallel): 指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

在这里插入图片描述

并发(concurrency): 指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

在这里插入图片描述

  • 线程Thread

    • 进程内部的一个执行单元,它是程序中一个单一的顺序控制流程。
    • 线程又被称为轻量级进程(Iight weight process)
    • 如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程
  • 线程特点

    • 轻量级进程
    • 独立调度的基本单位
    • 可并发执行
    • 共享进程资源

    进程与线程的区别

    面试常问点,注意根本区别的理解,必答点。

    区别 进程 线程
    根本区别 作为资源分配的单位 cpu调度和执行的单位
    开销 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。 线程可以看成是轻量级的进程,同一类进程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小
    所处环境 在操作系统中能同时运行多个任务(程序) 在同一应用程序中有多个顺序流同时执行
    分配内存 系统在运行的时候会为每个进程分配不同的内存区域 除CPU外,不会为线程分配内存(线程使用的线程是它所属进程的资源),线程组只能共享资源
    包含关系 没有线程的进程可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。 线程是进程一部分,所以线程有时被称为轻量级进程

    举个例子帮助理解CPU,进程,线程之间的关系:

  • 班级:501、502、503

  • 小组:1、2、3、4、5… (真正做事的,看作线程)

  • 完成一件事份:大扫除

    • 总负责:班主任
    • 步骤1:以班级为单位领取大扫除工具 (班级是资源分配的单位) ,本班级的所有小组都使用该班级领取的资源
    • 步骤2:以小组为单位开始大扫除 (小组是调度和执行的单位)
    • 步骤3:班主任亲自监任。如果发现不台格,直接要求该小组重新打扫;如果某个小组打扫完毕,班主任可以直接给该小组安排其他任务
  • 对比

    • CPU:校长
    • 进程:班级(一个班级可以有多个小组,班级是资源分配的单位)
    • 线程:小组(班主任直接指挥小组进行工作)

1.2线程的创建和启动

1.2.1线程的创建

面试常问,线程创建有几种方式?必须掌握,下面将以龟兔赛跑的例子帮助大家理解

方式1:继承Java.lang.Thread类,并覆run()方法

举个龟兔赛跑的栗子

  1. 创建Test类
/**
 * 功能:龟兔赛跑
 * 技能:线程的创建和启动
 */
public class Test {

    public static void main(String[] args) {
        //创建线程对象
        Thread thread= new TortoiseThread();
        //启动线程
        //thread.run();//错误
        thread.start();
        while(true){
            System.out.println("兔子暂时领先	"+"线程名:"+Thread.currentThread().getName()+"线程优先级:"+Thread.currentThread().getPriority());
        }
    }
}
  1. 创建TortoiseThread类

public class TortoiseThread extends Thread{    //继承
    /*
    *方法体,线程执行的代码,线程要完成的任务
     */
    @Override
    public void run() {
        while (true){
            System.out.println("乌龟领先了,add oil..."+this.getName()+"      "+this.getPriority());
        }
    }

}

运行结果:

乌龟领先了,add oil... 线程名:Thread-0  线程优先级:5
乌龟领先了,add oil... 线程名:Thread-0  线程优先级:5
乌龟领先了,add oil... 线程名:Thread-0  线程优先级:5
兔子领先了,add oil... 线程名:main 线程优先级:5
兔子领先了,add oil... 线程名:main 线程优先级:5

总结

  1. 如何定义线程类
 public class TortoiseThread extends Thread{
      public void run(){ //线程体
       //线程执行的代码
      }
   }
  1. 如何创建线程对象
  Thread thread= new TortoiseThread();//子类对象赋给父类的引用
  1. 如何启动线程
thread.run();//错误,可以理解为直接调用run()方法里的代码,并非真正开启线程
thread.start();//正确
  1. 其它
    1. 之前的线程都是单线程,启动mian()方法,自动创建一个线程,名称mian
    2. 控制台交替出现乌龟和兔子领先,本质上交替获的cpu执行代码并输出结果
方式2:实现java.lang.Runnable接口,并实现run()方法

学习编程查看源代码是非常重要的

先看看Runnable接口的源码,发现它只有一个方法run(),功能并不完善,并不是真正意义上的线程。
在这里插入图片描述Thread源码中的构造方法,说明我们通过Runnable创建线程实际还是通过Thread类创建的。
在这里插入图片描述

举个龟兔赛跑的栗子

  1. 创建Test类
/**
 * 功能:龟兔赛跑
 * 技能:定义线程类方法2
 */
public class Test {

    public static void main(String[] args) {
        //创建线程对象
        Runnable runnable= new TortoiseRunnable(); 
        Thread thread = new Thread(runnable);
        //启动线程
       thread.start();
       
         while(true){
            System.out.println("兔子领先了,add oil..."+" 线程名:"+Thread.currentThread().getName()+" 线程优先级:"+Thread.currentThread().getPriority());
        }
    }
}
  1. 创建TortoiseThread类

public class TortoiseRunnable implements Runnable{
    /*
    *方法体,线程执行的代码,线程要完成的任务
     */
    @Override
    public void run() {
         while (true){
            System.out.println("乌龟领先了,add oil..."+" 线程名:"+this.getName()+"  线程优先级:"+this.getPriority());
        }
    }

}

思考:如果多个同种乌龟和一个兔子赛跑呢?,假设这种乌龟的体力值为100,在TortoiseThread类中定义体力值Physicalstrength

  1. 创建Test类
public class Test {

    public static void main(String[] args) {
        //创建线程对象
        Runnable runnable= new TortoiseRunnable();
		//多个线程使用同一种资源,即同一种体力值的乌龟,共享成员变量
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable); 
        
        //启动线程
        thread1.start();
        thread2.start();
        thread3.start();

        while(true){
            System.out.println("兔子领先了,add oil..."+" 线程名:"+Thread.currentThread().getName()+" 线程优先级:"+Thread.currentThread().getPriority());
        }
    }
}
  1. 创建TortoiseThread类

public class TortoiseRunnable implements Runnable{
	private int Physicalstrength = 100;
    /*
    *方法体,线程执行的代码,线程要完成的任务
     */
    @Override
    public void run() {
         while (true){
            System.out.println("乌龟领先了,add oil..."+" 线程名:"+this.getName()+"  线程优先级:"+this.getPriority());
        }
    }
}

再次思考,如果某个Runnable的实现类只使用一次,能否用优雅的代码实现?
tip:使用匿名内部类
此时只需要一个类即可

public class Test2 {

    public static void main(String[] args) {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("乌龟领先了,add oil..."+" 线程名:"+Thread.currentThread().getName()+"  线程优先级:"+Thread.currentThread().getPriority());
                }
            }
        };
		//Runnable()如果只传入
		一次,推荐使用此方法
        Thread thread = new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        while (true){
                            System.out.println("乌龟领先了,add oil..."+" 线程名:"+Thread.currentThread().getName()+"  线程优先级:"+Thread.currentThread().getPriority());
                        }
                    }
                }
        );
        //多个线程使用同一种资源,即同一种体力值的乌龟,共享成员变量
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable);

        //启动线程
        thread.start();

        thread1.start();
        thread2.start();
        thread3.start();

        while(true){
            System.out.println("兔子领先了,add oil..."+" 线程名:"+Thread.currentThread().getName()+" 线程优先级:"+Thread.currentThread().getPriority());
        }
    }
}

优雅代码的编写是程序员必须考虑得事情,思考?在龟兔赛跑的比赛中,他们身份都是赛跑选手,那能否根据这些共同点用其它的方式创建这些选手?

  1. 创建RunnerThread类

public class RunnerThread extends Thread {

    private String runnerName;//参赛选手的名字
    public RunnerThread() {
    }

    public RunnerThread(String name) {//线程得名称,默认得名称 thread-0 thread-1 thread-2
        super(name);
    }
    public RunnerThread(String name1 , String name2){//构造方法创建线程,传入线程名称和选手名称
        super(name1);
        this.runnerName = name2;
    }

    @Override
    public void run() {//共同行为代码
        while (true){
            System.out.println(this.runnerName+"领先了,add oil..."+" 线程名:"+this.getName()+"  线程优先级:"+this.getPriority());
        }
    }
}

创建Test类

package RunnerThread;

public class Test {
    public static void main(String[] args) {
    	//这样无论多少选手参加比赛,我们只要重复下面代码即可
        Thread thread1 = new RunnerThread("刘翔线程","刘翔");
        Thread thread2 = new RunnerThread("博尔特线程","博尔特");

        thread1.start();
        thread2.start();
    }
}

总结

  1. 如何定义线程类
  public class TortoiseRunnable implements Runnable{
        public void run() {
         }
  }
  1. 如何创建线程对象
  Runnable runnable= new TortoiseRunnable();
  Thread thread = new Thread(runnable);
  1. 如何启动线程
  thread.start();
  1. 两种定义线程类的优略

    1. 方法1:继承Thread类
      优点:编程较简单
      缺单:无法继承其它类
    2. 方法2: 实现Runnable接口
      缺点:编程稍微复杂
      优点:可以继承其它类,更方便多个线程共享同一个资源
  2. Thread常用的方法

    1. run() :线程体
    2. getName()
    3. setName()
    4. getPriority()
    5. setPriority()
    6. start():启动线程
    7. currentThread():获取当前线程
  3. 匿名内部类的使用
    如果某个Runnable的实现类只使用一次,使用匿名内部类即可

方式3:实现Callable接口,并实现call()方法

先查看源代码,我们可以发现几点和之前runnable不一样的地方,支持泛型,可以抛出各种异常,等等,不过我们可以通过源码判断Callable接口也并非真正意义上的线程。
在这里插入图片描述
我们通过一个例子思考它的特点,与用法

/**
 * 定义线程类的第三种方式
 *public class RandomThread implements Callable<Integer> {
 *     @Override
 *     public Integer call() throws Exception {
 *     }
 * }
 * 特点:
 * 1.有返回值
 * 2.抛出异常(检查异常和运行时异常)之前的Runnable只能抛出运行时异常
 * 3.支持泛型
 * 4.jdk1.5提供的,功能强大
 * 5.如果没有返回值,如果没有抛出检查异常,建议使用方式一和方式二
 * 6.线程体方法名不是run,是call
 *
 * public class FutureTask<V> implements RunnableFuture<V>
 *     public interface RunnableFuture<V> extends Runnable, Future<V>
 */
//使用Callable注意有返回值,确定泛型种类
public class RandomThread implements Callable<Integer> {


    @Override
    public Integer call() throws Exception {//注意线程体是call
        Thread.sleep(3000);//暂停3秒
        return new Random().nextInt(10);
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个线程对象
        Callable callable = new RandomThread();
        FutureTask<Integer> task = new FutureTask<>(callable);//FutureTask可以理解为拥有了Runnable, Future的功能,建议查看源码
        Thread thread = new Thread(task);

        //启动线程
        thread.start();

        //得到返回值
        System.out.println(task.isDone());
        Integer result =  task.get();//必须等线程执行完毕才能得到返回值,之前再次阻塞。
        System.out.println(task.isDone());
        System.out.println(result);
    }
}

线程的启动

  • 新建的线程不会自动开始运行,必须通过start()方法启动
  • 不能直接调用run()来启动线程,这样run()将作为一个普通方法立执行,执行完毕前其它线程无法并发执行
  • Java程序启动时,会立刻创建主线程,main就是在这个线程上运行。当不再产生新线程时,程序是单线程的

线程的生命周期

在这里插入图片描述

  1. 新生状态
  • 用new关键字建立一个线程对象后,该线程对象就处于新生状态。

  • 处于新生状态的线程有自己的内存空间,通过调用 start()进入就绪状态

  1. 就绪状态
  • 处于就绪状态线程具备了运行条件,但是还没有分配到内存,处于线程就绪队列,等待系统为其分配CPU

  • 当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称之为“cpu调度“。

  1. 运行状态
  • 在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任务而死亡

  • 如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。

  1. 阻塞状态
  • 处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待IO设备等資源,将让出CPU并暂时停止自己的运行,进入阻塞状态

  • 在阻壅状态的线程不能进入就绪队列。只有当引起胆壅的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位詈开始继续运行

  1. 死亡状态
  • 死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个。一个是正常运行的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过执行stop方法来终止一个线程【不推荐使用】,三是线程抛出末捕获的异常

更完整的生命周期
在这里插入图片描述

线程控制

线程同步

线程同步的必要性

线程同步的实现

死锁

线程间通信

线程间通信的必要性

线程间通信的实现

其他

线程组

线程池

后续补充

猜你喜欢

转载自blog.csdn.net/qq_41620020/article/details/106679196