0%

Java并发

1.多线程基础

1.1 线程和进程的区别:

进程 线程
定义 进程是程序执行时的一个实例,即它是程序已经执行到何种程度的数据结构的汇集。 线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
根本区别 进程是操作系统资源分配的基本单位 线程是处理器任务调度和执行的基本单位
资源开销 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销 可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计 数器(PC),线程之间切换的开销小。
包含关系 如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的 线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配 进程之间的地址空间和资源是相互独立的 同一进程的线程共享本进程的地址空间和资源
影响关系 一个进程崩溃后,在保护模式下不会对其他进程产生影响 一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程 每个独立的进程有程序运行的入口. 顺序执行序列和程序出口,可并发执行 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,可并发执行

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

  1. 继承Thread

    • 定义Thread类的子类,并重写该类的run方法。

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      package Thread;
      /*
      1.继承Thread类
      2.重写run()方法
      3.调用start()方法开启线程
      */
      public class TestThread extends Thread {
      @Override
      public void run() {
      //重写run()方法
      for (int i = 0; i < 10; i++) {
      System.out.println("小明在厨房做饭");
      }
      }

      public static void main(String[] args) {
      //创建Thread子类的实例
      TestThread testThread = new TestThread();
      //调用start()方法开启线程
      testThread.start();

      for (int i = 0; i < 200; i++) {
      System.out.println("小红在客厅打王者");
      }
      }
      }

    当多线程没有开启时,代码只执行主线程里面main()方法的代码,当开启线程后,经过cpu调度后开始执行(线程开启后不一定立即执行)

  2. 实现Runnable接口

    • 实现Runnable接口,并重写该接口的run()方法。
    • 创建Runnable实现类的实例,并用该实例作为Thread的target来创建Thread对象,Thread对象才是真正的线程对象。
    • 调用线程对象的start()方法来启动线程。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    package Thread;

    /*
    1.实现Runnable接口
    2.重写run()方法
    3.创建Runnable接口实现类实例,并将该实例放入Thread的target来创建Thread对象
    4.调用start()方法开启线程
    */
    public class TestRunnable implements Runnable {
    @Override
    public void run() {
    //重写run()方法
    for (int i = 0; i < 10; i++) {
    System.out.println("小明在厨房做饭");
    }
    }

    public static void main(String[] args) {
    //创建Runnable接口实现类实例
    TestRunnable testRunnable = new TestRunnable();
    //该实例放入Thread的target来创建Thread对象
    //调用start()方法开启线程
    // Thread thread = new Thread(testRunnable);
    // thread.start();
    new Thread(testRunnable).start();

    for (int i = 0; i < 200; i++) {
    System.out.println("小红在客厅打王者");
    }
    }
    }
  3. 实现Callable接口

    • 实现Callable接口并指定返回值,然后实现call()方法。
    • 创建Callable实现类的实例,使用FutureTask包装类来包装Callable对象。
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    package Thread;

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;

    /*
    1.实现Callable接口。
    2.重写call()方法。
    3.创建Callable接口实现类实例,使用FutureTask包装类来包装Callable实例对象。
    4.使用FutureTask对象作为Thread的target来创建Thread对象。
    5.调用start()方法开启线程。
    6.调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
    */
    public class TestCallable implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
    //重写call()方法
    for (int i = 0; i < 10; i++) {
    System.out.println("小明在厨房做饭");
    }
    return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    //创建Callable接口实现类实例
    TestCallable testCallable = new TestCallable();
    //使用FutureTask包装类来包装Callable实例对象
    FutureTask<Boolean> task = new FutureTask<>(testCallable);
    //使用FutureTask对象作为Thread的target来创建Thread对象
    //调用start()方法开启线程
    new Thread(task).start();

    for (int i = 0; i < 200; i++) {
    System.out.println("小红在客厅打王者");
    }
    //调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
    System.out.println(task.get()); //获取call()的返回值。若调用时call()方法未返回,则会阻塞线程等待返回值
    }
    }
  4. 采用实现RunnableCallable接口的方式创建多线程

    • 优势:

      线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
      在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    • 劣势:

      编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

  5. 使用继承Thread类的方式创建多线程

    • 优势:

      编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    • 劣势

      线程类已经继承了Thread类,所以不能再继承其他父类。

  6. RunnableCallable的区别

    • Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
    • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    • Call方法可以抛出异常,run方法不可以。
    • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

1.3 为什么要使用多线程

从计算机底层来说:

  • 单核时代: 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
  • 多核时代:多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个CPU 核心被利用到,这样就提高了 CPU 的利用率。