Java线程

参考:JavaGuide

线程

线程的实现

一个Java程序作为一个进程,运行时实际上是多线程并发的, main函数就是主线程。每个调用过start()方法,并且还未结束的java.lang.Thread类的实例就是一个线程。Java线程是Java程序的并发执行单位。

一对一线程模型✨

每个Java线程对应操作系统中的一个内核线程,充分利用多核处理器的优势。(现在Windows 和 Linux 都是这种)

JVM通过调用操作系统提供的线程库实现Java线程的创建和同步,Java线程的调度和管理由操作系统负责。

多对一线程模型

什么是用户线程?是完全在用户空间实现的线程,操作系统内核不知道它的存在。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持 规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。

多个用户线程映射到一个内核线程。线程切换非常快速,可以支持大量的用户线程。但是无法利用多核处理器,阻塞操作会导致整个进程挂起。

Java早期采用这种模式。

多对多线程模型

多个用户线程映射到少量内核线程。实现较复杂。goroutine采用这种模型。

创建线程

严格意义上讲,Java创建新线程的方式只有start()方法。

继承Thread类

override Thread类的run()方法即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的任务
        
    }
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.start();
    }
}

实现 Runnable 接口

需要把我的实现的类对象作为参数传递给 Thread 的创建函数。

1
2
3
4
5
6
7
public class MyRunnableLambda {
    public static void main(String[] args) {
        Runnable runnable = () -> {System.out.println("hi");};
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Runnable接口更灵活,可以更好地实现资源共享(多个线程可以共享同一个Runnable对象, 也更好地和线程池的高级 API 配合。

FutureTask

可以返回任务的执行结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class MyFutureTask {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(()->{
            System.out.println("hi");
            return 1;
        });
        new Thread(futureTask, "task1").start();
        Integer result=futureTask.get();
        System.out.println(result);
    }
}

可以直接调用Thread类的run()方法吗?

如果新建了一个线程,然后在主函数里执行thread.run(),实际上还是主线程(main方法的线程)来执行run()方法里的逻辑。

如果执行thread.start(),则是开启一个新线程,让它处于RUNNABLE状态,然后把run()里的逻辑交给新线程去执行。

线程的生命周期与状态

Java线程一共有六种状态。

  1. NEW, 线程被创建后的初始状态
  2. RUNNABLE,线程在运行 start()以后的状态,包含OS线程的READY和RUNNING状态。即要么在等待OS调度它执行,要么正在运行。
  3. WAITING,线程不会被CPU调度,直到它被其它线程显式唤醒。
  4. TIMED WAITING,线程不会被CPU调度,一段时间后会被系统唤醒。
  5. BLOCKED,线程没有获取锁而被阻塞,直到其它线程释放锁,JVM会随机挑选/按顺序选择一个被该锁BLOCKED的线程赋予锁。
  6. TERMINATE,线程已经结束执行。 Alt text

为什么不区分READY 和 RUNNING? 因为time slice切换得很快,可能一个线程执行很短的时间,就schedule到其它线程运行了。

sleep()和wait()

sleep()

Thread.sleep(2000); //让该线程睡眠 2 秒

sleep()是 Thread 类的静态native方法,让线程从 RUNNABLE 状态变成 TIMED WAITING 状态。这个操作不会释放锁。

wait()

wait()是 Object 类的实例 native 方法,必须在 synchronized 代码块/方法中调用,会释放锁。

1
2
3
4
5
6
synchronized (obj) {
    while (条件不满足) {  // 必须用循环检查条件,防止虚假唤醒
        obj.wait();      // 释放锁,进入等待队列
    }
    // 条件满足后的操作
}

wait()方法会让一个线程从RUNNABLE状态变成WAITING状态,这个线程会进入该对象的waiting set(等待队列)中。
另一个线程对同一个lock执行lock.notify()操作,那么随机唤醒该lock的等待队列中的一个线程,并把它加入entry set(同步队列)中,状态从(TIMED)WAIT变成BLOCKED。

同步队列:当线程想要去拿synchronized锁,却没有拿到的时候,它把自己加入该锁的同步队列中,状态被设置为BLOCKED。等锁释放的时候,JVM会随机(unfair)选择一个线程赋予锁。

wait()必须是instance method,因为释放锁这个操作实际上是让当前线程释放对象锁,操作的是锁那个对象。

多线程

Java线程调度

调度方式分为协同式(Cooperative Threads-Scheduling)和抢占式(Preemptive Threads-Scheduling。

协同式调度:线程的执行时间由线程本身控制。当线程执行完工作之后,主动通知系统切换到另一个线程上。优点是不会有同步性问题,缺点是可能某个线程的工作运行太久,阻塞其它线程。

抢占式调度:线程的执行时间是系统可控的。

Java语言提供了10个线程优先级,如果有两个线程都处于RUNNABLE状态,那么优先级高的线程会被先调度。由于Java线程是one-to-one映射到内核线程的,Java优先级部分对应内核线程的优先级。

单核CPU运行多线程

线程分为CPU密集型和IO密集型。对于CPU密集型线程,频繁的上下文切换会降低执行效率;对于IO密集型线程,由于在等待IO的时候切换到其它线程,它们可以做别的事,所以这样效率是合理的。

死锁

死锁产生的条件:mutual exclusion, hold and wait, no preemption, circular wait

1
cd /Users/ruoke/Library/Java/JavaVirtualMachines/corretto-17.0.13/Contents/Home/bin

运行jconsole,选想要查看的进程,点进去看它的线程 Alt text