文章

Java 多线程详解

基本概念

线程和进程

线程和进程分别是什么? 常见的一个概念是

进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。

但是显然这并不方便理解。 要解决这个问题可能需要从操作系统开始说起:

程序

计算机程序是一组计算机能识别和执行的指令

也就是我们写的代码或者编译出来的代码

进程

当程序运行起来就成为了一个进程,操作系统为了避免进程操作到为了避免进程A写入进程B的情况发生,会使用一个叫进程隔离的技术,进程间的通信可以使用进程间通信 (Inter-Process Communication,简称 IPC)完成。

进程隔离的实现,使用了虚拟空间地址(虚拟内存空间),进程A的虚拟地址和进程B的虚拟地址不同,这样就防止进程A将数据信息写入进程B。

就像井底之蛙以为自己看到的天空就是全部的天空一样,每个进程都以为自己独占了所有内存,但其实只是内存中的一部分。

程序并行

由于 CPU 的执行速度是远非常快的,配合时间片轮转调度,我们可以认为 CPU 是可以同时处理很多程序运行。再加上现在 CPU 也都有很多核心,也就是说如果我们的程序想要运行得更快,就可以让尽可能多个代码片段一起并行运行。

线程

前面我们说 当程序运行起来就成为了一个进程,因为并行的程序需要做同一件事情,那么有没有一种可以相互不隔离的“进程”呢?

这种相互不管理的“进程”就叫做线程,线程们共享进程的资源,同时又可以分别处理各自的程序片段。

井底之蛙

如果说进程是只有自己一片天的井底之蛙,那线程就是进程井里的一只青蛙了。

Linux 底层

创建进程和线程最终都调用了一个名为 do_dork() 的方法,只是一些关于资源(内存,文件等)的参数不同。

宏观看看

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

创建一个线程

Thread 类

public class MyThreadTest {
  public static void main(String[] args) {
    Thread1 thread1 = new Thread1();
    thread1.start();
  }
}

class Thread1 extends Thread {
  @Override
  public void run() {
    System.out.println("Thread1");
  }
}

Java 中新建一个线程的方法就是继承 Thread 类然后重写 run() 方法。

Runnable 接口

可以看到Thread 类实现了Runnable 接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

当实现接口 Runnable 的对象用于创建线程时,启动线程会导致在单独执行的线程中调用对象的 run 方法。

函数式接口

Runnable 接口又是一个 函数式接口(@FunctionalInterface)

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

函数式接口可以被隐式转换为 lambda 表达式。

[ 挖一个函数式接口的坑 ]

Thread.start()

    public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* 什么也不做。如果start0抛出了一个Throwable,
				   那么它将向上传递到调用堆栈 */
            }
        }
    }

    private native void start0();

Java 本身并不能创建线程,所以 star() 方法中调用了原生方法 start0() 来创建线程。

线程的状态

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;

  • Runnable:运行中的线程,正在执行run()方法的Java代码;

  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;

  • Waiting:运行中的线程,因为某些操作在等待中;

  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;

  • Terminated:线程已终止,因为run()方法执行完毕。

用一个状态转移图表示如下:

         ┌─────────────┐
         │     New     │
         └─────────────┘
                │
                ▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
 ┌─────────────┐ ┌─────────────┐
││  Runnable   │ │   Blocked   ││
 └─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
 │   Waiting   │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
                │
                ▼
         ┌─────────────┐
         │ Terminated  │
         └─────────────┘

线程同步(锁)

引子

让我们写一个 100 个线程自增的程序

public class LockP {
  private static int num = 0;
  public static void main(String[] args) throws InterruptedException {
      for(int i = 0; i < 100; i++){
          new Thread(()->{
                for(int j = 0; j < 100; j++)
                    num++;
            }).start();
      }
      sleep(1000); // 等待所有线程执行完毕
      System.out.println(num);
  }
}

执行三次结果分别是:

  • 9862

  • 9946

  • 9936

可以看到程序并没有如我们所愿将num自增到10000 ,这是因为如果多个线程同时读写共享变量,会出现数据不一致的问题。

实际上++ 操作并不是一个原子性的操作,所以其他线程有可能在某个线程++ 的同时操作变量。

原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。

传统方式

解决线程同步的问题就需要线程同步了,传统方式我们使用synchronized 关键字。

public class LockP { 
  private static int num = 0;
  public static void main(String[] args) throws InterruptedException {
      for(int i = 0; i < 100; i++){
          new Thread(()->{
                for(int j = 0; j < 100; j++)
                    synchronized (LockP.class) {
                        num++;
                    }
            }).start();
      }
      sleep(1000); // 等待所有线程执行完毕
      System.out.println(num);
  }
}

它表示用LockP.class作为一把锁,在执行synchronized (LockP.class) { .... } 代码块的时候只能有一个线程操作。

JUC

什么是JUC

新建线程的方法

生产者消费者

传统

虚假唤醒

JUC

集合安全

List

常用辅助类

CountDownLatch

CyclicBarrier

Semaphore

读写锁

队列

4组API

线程池

3大方法

7大参数

四种拒绝策略

JMM

Volatile

  1. 保证可见性

  2. 不保证原子性

  3. 禁止指令重排

    CAS

License:  CC BY 4.0