Java 线程学习笔记

492 阅读8分钟

CPU 核心数和线程数的关系

目前主流 CPU 有双核、三核、四核、六核和八核等,增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下核心数和线程数是 1:1 的关系,也就是说 4 核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系,既 4 核 CPU 拥有 8 个线程

CPU 时间片轮转机制

我们在平时的开发中,可能并没有感受到 CPU 核心数的限制,想启动几个线程就启动几个线程,哪怕是在单核的 CPU 上也可以启动多个线程,这是因为操作系统提供了一种 CPU 时间片轮转机制。

时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RP 调度,每个进程被分配一个时间段,称作它为时间片,既该进程允许运行的时间。

通俗的讲,操作系统会对线程进行调度,加入系统一共启动了 100 个线程,操作系统会随机分配一个时间片去执行某个线程,由于系统切换速度非常快,所以用户会以为应用是并行运行的。线程切换需要保存线程状态(上下文),所以每次切换都有时间和性能消耗,当线程过多时,线程切换次数会增加,分配到单个线程的概率也会降低,这时程序就会发生卡顿。

进程和线程

  • 线程是 CPU 调度的最小单位,必须依赖进程而存在。线程是进程的一个实体。
  • 进程是程序运行资源分配的最小单位,资源包括 CPU、内存、磁盘等。一个进程可以包含多个线程。

Java 线程的实现方式

  • 继承 Thread 类

    class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("do something");
        }
    }
    

    启动线程

    MyThread myThread = new MyThread();
    myThread.start();
    
  • 实现 Runnable 接口

    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("do something");
        }
    }
    

    启动线程

    MyRunnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    thread.start();
    
  • 有返回值的线程(第二种实现方式的一种扩展)

    Callable<String> callable=new Callable<String>() {
        @Override
        public String call() throws Exception {
            System.out.println("so something");
            return "result";
        }
    };
    FutureTask<String> futureTask=new FutureTask<>(callable);
    Thread thread = new Thread(futureTask);
    thread.start();
    String result = null;
    try {
        //此处获取返回值("result")
        result = futureTask.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
    //直到thread线程执行完成后,才会执行此处代码
    System.out.println(result);
    

    PS:获取返回值时线程是等待状态,即如果线程没有完成,则获取返回值代码处于等待状态,直到线程执行完毕,才会继续执行后续代码。

停止线程

  • 调用 stop() 暴力停止,暴力停止线程是很危险的事,可能带来预料不到的问题,比如锁或者资源未释放(暴力停止方法已经过时,不建议使用)

  • 和谐停止:让 run 函数执行完毕,线程自然终止。
    使用 interrupt()发起中断信号(并不会真正停止,只是发送信号),此方法调用后,线程的 isInterrupted()方法将返回 true(默认返回 false),那么我们就可以根据 isInterrupted()的返回值在线程内部判断是否需要停止线程,如下:

    class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            while (!isInterrupted()) {
                System.out.println("线程正在执行");
            }
            System.out.println("线程终止");
        }
    }
    

    启动线程,并在一秒钟后发起中断信号,此时线程的 isInterrupted()方法返回 true,代码跳出 while 循环,run()方法执行完成,线程便自然终止。

    MyThread myThread = new MyThread();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    myThread.interrupt();
    

    注意

    当线程处于等待状态(如线程调用了 thread.sleep()、thread.join()、thread.wait()),则在线程检查中断标记为 true 时,则会在等待处抛出异常,并在抛出异常后立即清除中断标记,既 isInterrupted()重新返回 false。这种情况可以在等待处的异常代码块中再次调用 interrupt(),这时便可以成功发送中断信号。

    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        // 注:此示例线程实现Runnable接口实现,所有在run()方法内部无法调用到isInterrupted()方法,此时可使用Thread.currentThread()获取当前线程。
          while (!Thread.currentThread().isInterrupted()) {
              try {
                  Thread.sleep(5000);
                  System.out.println("running");
              } catch (InterruptedException e) {
                  // 此处再次发起中断信号,才可以成功让isInterrupted()返回true
                  Thread.currentThread.interrupt();
                  e.printStackTrace();
              }
          }
      }
    });
    
    thread.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 此时在线程启动一秒钟后,对thread线程发起中断信号,如果此时thread等待状态,则会在等待状态出抛出异常并将中断标记清除(isInterrupted()仍然返回false)
    thread.interrupt();
    

sleep() 和 wait()的异同

  • sleep()和 wait()都能使线程进入等待状态

  • wait()是 Object 的方法,sleep()是 Thread 的方法

  • sleep()可以在任何地方调用,wait() 只能在同步方法或同步代码块中调用

  • Thread.sleep()方法会让当前线程主动让出 CPU(这时 CPU 可以去执行其他任务),而到达 sleep 指定时间后,CPU 再回到该线程继续往下执行。sleep 方法不会释放同步锁。

  • Object.wait()是让当前线程暂时退出同步锁,让其他正在等待的资源运行,只有调用了 notify()或 notifyAll()方法后,该线程才会结束 wait 状态,可以重新参与同步锁的竞争。

线程优先级

setPriority(int priority)
此方法可以设置线程的优先级,priority 优先级取值为 1-10,优先级越高,线程就会有更高的几率被执行。由于操作系统一般只有两三个线程优先级,Java 线程优先级和操作系统优先级无法一一对应,所以 Java 线程优先级并不稳定,甚至有可能不起作用。Java 优先级开发中一般很少使用。

并发和并行

  • 并行
    并行是指同时运行,既同时运行多个应用(注:此处的同时运行时指真正的同时运行,而不是依靠 CPU 时间片轮转机制造成的错觉)。
    例如,一条 4 车道的公路的并行车辆最大是 4。
  • 并发
    并发是指单位时间内可以处理的次数。
    例如,服务器一秒可以处理 1000 个请求,服务器的并发能力就是 1000 次/s。
    例如,一条 4 车道的公路,一分钟最多可通行 1000 辆车,这条公路的并发能力就是 100 辆/分钟。

线程 start()和 run()方法的区别

  • 调用 start()方法是启动线程,jdk 源码中会调用到 ndk 方法,最终由 CPU 去调度这个线程,最终执行到 run 方法。
  • 调用 run()方法只是普通的方法调用,和线程没有任何关系,也不会启动线程。

join() 使用

线程的 join() 方法会使此线程的启动线程(也有说法叫做“父线程”)处于等待状态,知道此线程执行完成后才会继续执行“父线程”。

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程1");
    }
});
thread.start();
//此处thread线程调用join()方法后,当前线程将等待thread线程执行完毕后再执行
thread.join();
//此处打印语句会在thread线程执行完成后再执行
System.out.println("继续执行");

强制执行某个线程

Java 无法做到控制 CPU 去执行某个线程,但是可以使用 C 语言调用内核的 API 做到

守护线程

线程可以通过 setDaemon(true)方法设置该线程为守护线程。当此线程的“父线程”(启动该线程的线程)结束时,该线程的守护线程也随之结束。如下代码如果不设置 thread 线程为守护线程,thread 线程将会一直存在。将 thread 线程设置为 main 线程的守护线程,main 线程结束时,thread 线程跟着结束。

public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                System.out.println("我是守护线程,我还在运行");
            }
        }
    });
    //设置thread线程为当前线程的守护线程(当前在main线程中),当main线程结束时,thread线程会跟着结束
    thread.setDaemon(true);
    thread.start();
    System.out.println("执行完此行代码程序就应该结束了");
}

使用 yield()让出线程执行权

根据 CPU 时间片轮转机制,每个线程随机获取一段时间的执行机会。当线程调用了 yield()方法后,此线程会让出本次的执行机会(本次不再执行)。

public static void main(String[] args) {
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("thread1:" + i);
                if (i == 20) {
                    Thread.yield();
                }
            }
        }
    });
    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("thread2:" + i);
            }
        }
    });
    thread1.start();
    thread2.start();
}

执行结果截取,线程 1 在第 20 此打印后,让出了本次执行机会,接着线程 2 执行了打印代码。注意:此处并不一定是 yield()使线程 1 结束执行,也有可能线程 1 的时间片正好在此时用完。

thread1:20
thread2:30
thread2:31
thread2:32
thread2:33
thread2:34
thread2:35