2024年2月

一、简介

在之前的文章中,我们简单的介绍了线程诞生的意义和基本概念,采用多线程的编程方式,能充分利用 CPU 资源,显著的提升程序的执行效率。

其中
java.lang.Thread
是 Java 实现多线程编程最核心的类,学习
Thread
类中的方法,是学习多线程的第一步。

下面我们就一起来看看,创建线程的几种方式以及
Thread
类中的常用方法。

二、创建线程的方式

在 JDK 1.8 版本中,创建线程总共有四种方式:

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 使用 Callable 和 Future 创建线程
  • 使用 JDK 8 的 Lambda 创建线程

2.1、通过继承 Thread 创建线程

通过继承
Thread
类来创建线程是最简单的一种方法,继承类重写
run()
方法,然后通过线程对象实例去调用
start()
方法即可启动线程。

public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "在运行!");
    }
}
MyThread thread = new MyThread();
thread.start();

2.2、通过实现 Runnable 接口创建线程

通过实现
Runnable
接口来创建线程也是最简单的一种方法,同时也是最常用的一种方式。

开发者只需要实现
Runnable
接口,然后通过一个
Thread
类来启动。

public class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "在运行!");
    }
}
Thread thread = new Thread(new MyThread());
thread.start();

2.3、使用 Callable 和 Future 创建线程

相比通过实现
Runnable
接口来创建线程,使用
Callable

Future
组合来创建线程可以实现获取子线程执行结果,弥补了调用线程没有返回值的情况,可以看做是
Runnable
的一个补充,
Callable

Future
是 JDK1.5 版本中加入的。

public class MyThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "在运行!");
        return Thread.currentThread().getName();
    }
}
Callable<String> callable = new MyThread();
FutureTask<String> ft = new FutureTask<>(callable);
new Thread(ft).start();
// 通过阻塞方式获取线程执行结果
System.out.println(ft.get());

2.4、使用 JDK 8 的 Lambda 创建线程

Lambda 表达式,是从 JDK1.8 版本开始加入的,可以看作成通过实现
Runnable
接口创建线程的一种简写。

new Thread(()-> System.out.println(Thread.currentThread().getName() + "在运行!")).start();

2.5、创建线程几种方式的对比

以上四种方式都可以创建线程,使用继承
Thread
类的方式创建线程时,编写简单,如果需要访问当前线程,无需使用
Thread.currentThread()
方法,直接使用
this
即可获得当前线程。

采用实现
Runnable

Callable
接口的方式创建线程时,线程类只是实现了
Runnable

Callable
接口,同时还可以继承其他类,最后通过
Thread
类来启动线程。它也是最常用的一种创建线程方式,通过接口方式来编程,可以实现代码更加统一。

其实通过继承
Thread
类创建线程的方式,本质上也可以看成实现了
Runnable
接口的一个实例,打开源码
Thread
,你会发现这一点。

public class Thread implements Runnable {
    
    //省略...
}

需要特别注意的地方是

真正启动线程的是
start()
方法而不是
run()
方法,单独调用
run()
方法和调用普通的成员方法一样,不能启动线程

三、Thread 常用方法介绍

Thread 类常用的方法主要有三大块:

  • 构造方法
  • 实例方法
  • 静态方法

3.1、构造方法

在 JDK 中,Thread 类提供了如下几个常用的构造方法来创建线程。

方法 描述
Thread() 创建一个默认设置的线程实例,线程名称采用自增ID命名
Thread(Runnable target) 创建一个包含可执行对象的线程实例
Thread(Runnable target, String name) 创建一个包含可执行对象,指定名称的线程实例
Thread(String name) 创建一个指定名称的线程实例
Thread(ThreadGroup group, String name) 创建一个指定线程组,线程名称的线程实例
Thread(ThreadGroup group, Runnable target) 创建一个指定线程组,包含可执行对象的线程实例
Thread(ThreadGroup group, Runnable target, String name) 创建一个指定线程组,包含可执行对象,指定线程名称的线程实例
Thread(ThreadGroup group, Runnable target, String name, long stackSize) 创建一个指定线程组,包含可执行对象,指定名称以及堆栈大小的线程实例

其中
Thread(Runnable target)
构造方法最常见。

Thread thread = new Thread(new Runnable() {
    
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
});
thread.start();

其次
Thread(Runnable target, String name)
构造方法,可以指定线程名称。

Thread thread = new Thread(new Runnable() {
    
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}, "thread-demo");
thread.start();

同时,还支持指定线程组来创建线程。

// 创建一个线程组实例
ThreadGroup tg = new ThreadGroup("线程组1");
// 创建一个线程实例
Thread thread = new Thread(tg,new Runnable() {
    
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getThreadGroup().getName() + ":" + Thread.currentThread().getName());
    }
}, "thread-demo");
thread.start();

如果不显式指定线程组,JVM 会将创建的线程归到当前线程所属的线程组中。

关于线程组的相关知识,我们会在后期的系列文章中进行讲解。

3.2、实例方法

在 Java 中,实例方法只有实例对象才能调用,也就是
new
出来的对象或者反射出来的对象,类是无法直接调用的。

在 JDK 中,Thread 类提供了如下几个常用的实例方法来操作线程。

方法 描述
public void start() 启动线程
public void run() 线程进入可运行状态时,jvm 会调用该线程的 run 方法;单独调用 run 方法,不能启动线程
public final void setName(String name) 设置线程名称
public final void setPriority(int priority) 设置线程优先级,默认5,取值1-10
public final void setDaemon(boolean on) 设置线程为守护线程或用户线程,默认是用户线程
public final void join(long millisec) 挂起线程 xx 毫秒,参数可以不传
public void interrupt() 当线程受到阻塞时,调用此方法会抛出一个中断信号,让线程退出阻塞状态
public final boolean isAlive() 测试线程是否处于活动状态

下面我们依次来看看它们之间的用法。

3.2.1、start()

start()
方法,简单的说就是启动线程,至于什么时候能运行,需要等待获取 CPU 时间片,然后调用线程对象的
run()
方法,产生一个异步执行的效果。

样例代码如下:

public class ThreadA extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",正在运行");
        }
    }
}
public class ThreadB extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",正在运行");
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();

        threadA.start();
        threadB.start();
    }
}

运行结果:

2023-08-30 15:51:43:331 当前线程:Thread-1,正在运行
2023-08-30 15:51:43:331 当前线程:Thread-1,正在运行
2023-08-30 15:51:43:332 当前线程:Thread-0,正在运行
2023-08-30 15:51:43:332 当前线程:Thread-1,正在运行
2023-08-30 15:51:43:332 当前线程:Thread-0,正在运行
2023-08-30 15:51:43:332 当前线程:Thread-1,正在运行
2023-08-30 15:51:43:332 当前线程:Thread-0,正在运行
2023-08-30 15:51:43:332 当前线程:Thread-1,正在运行
2023-08-30 15:51:43:333 当前线程:Thread-0,正在运行
2023-08-30 15:51:43:333 当前线程:Thread-0,正在运行

结果很明显,
CPU 什么时候执行线程的
run()
方法具有不确定,同时执行线程顺序也具有不确定性

,这是采用多线程异步执行程序的一个主要特征。

3.2.2、run()

如果单独调用
run()
方法,不能启动线程,会像调用普通的成员方法一样,我们可以将上面例子中的
threadA.start()
改成
threadA.run()
,再看看结果如何。

public class ThreadTest {

    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();

        threadA.run();
        threadB.run();
    }
}

运行结果:

2023-08-30 16:14:50:983 当前线程:main,正在运行
2023-08-30 16:14:50:984 当前线程:main,正在运行
2023-08-30 16:14:50:985 当前线程:main,正在运行
2023-08-30 16:14:50:985 当前线程:main,正在运行
2023-08-30 16:14:50:985 当前线程:main,正在运行
2023-08-30 16:14:50:986 当前线程:main,正在运行
2023-08-30 16:14:50:986 当前线程:main,正在运行
2023-08-30 16:14:50:986 当前线程:main,正在运行
2023-08-30 16:14:50:987 当前线程:main,正在运行
2023-08-30 16:14:50:987 当前线程:main,正在运行

结果很明显,单独调用
Thread
类实例
run()
方法,是没有任何异步效果的,全部被主线程执行。

3.2.3、setName()

setName()
方法,简而言之就是设置线程名称,如果不手动设置,创建线程的时候 JDK 会给一个默认的线程名称,从 0 开始依次自增。

开发者可以通过
getName()
方法获取线程名称,也可以通过
getId()
获取当前线程的唯一标记,这个值用户无法手动设置,由
Thread
类自动生成。

样例代码如下:

public class ThreadA extends Thread {

    @Override
    public void run() {
        long threadId = Thread.currentThread().getId();
        String threadName = Thread.currentThread().getName();
        System.out.println("threadId:" + threadId + ",threadName:" + threadName);
    }
}
public class ThreadTest {

    public static void main(String[] args)  {
        ThreadA threadA = new ThreadA();
        threadA.setName("thread-a");

        threadA.start();
    }
}

运行结果:

threadId:10,threadName:thread-a
3.2.4、setPriority()

setPriority()
方法的作用是设置线程的优先级,取值范围:1~ 10,与此对应的还有
getPriority()
方法,用于获取线程的优先级。优先级越高,拥有优先获取 CPU 执行的优势。

换句话说,当有两个线程在等待 CPU 执行时,优先级高的线程越容易被 CPU 选择执行。

样例代码如下:

public class ThreadA extends Thread {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        int priority = Thread.currentThread().getPriority();
        System.out.println("threadName:" + threadName + ",priority:" +  priority);
    }
}
public class ThreadB extends Thread {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        int priority = Thread.currentThread().getPriority();
        System.out.println("threadName:" + threadName + ",priority:" +  priority);
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            ThreadA threadA = new ThreadA();
            ThreadB threadB = new ThreadB();

            threadA.start();
            threadB.start();
        }
    }
}

运行结果:

threadName:Thread-0,priority:5
threadName:Thread-1,priority:5
threadName:Thread-2,priority:5
threadName:Thread-3,priority:5
threadName:Thread-4,priority:5
threadName:Thread-5,priority:5
threadName:Thread-6,priority:5
threadName:Thread-7,priority:5
threadName:Thread-8,priority:5
threadName:Thread-9,priority:5

线程默认优先级为 5,如果不手动指定,那么线程优先级具有继承性
,比如线程 A 启动线程 B,那么线程 B 的优先级和线程 A 的优先级相同。

如果我们手动设置优先级,再看看结果如何。

public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            ThreadA threadA = new ThreadA();
            ThreadB threadB = new ThreadB();

            threadA.setPriority(10);
            threadA.start();

            threadB.setPriority(1);
            threadB.start();
        }
    }
}

运行结果:

threadName:Thread-0,priority:10
threadName:Thread-1,priority:10
threadName:Thread-2,priority:10
threadName:Thread-3,priority:10
threadName:Thread-4,priority:1
threadName:Thread-5,priority:10
threadName:Thread-6,priority:1
threadName:Thread-7,priority:1
threadName:Thread-8,priority:1
threadName:Thread-9,priority:1

将线程实例
threadB
的优先级调整到最高,拥有优先被 CPU 执行的优势。

在实测过程中,可能有的同学感觉效果并不明显,如果你的电脑 CPU 是多核的,线程数量较少的情况,可能会被多个 CPU 并行执行,具体执行环境取决于 CPU 。

需要特别注意的是:
设置优先级只是很大程度上让某个线程尽可能获得比较多的执行机会,操作系统不能保证设置了优先级高的线程就一定会先运行或得到更多的 CPU 时间,具体执行哪一个线程,最终还是由 CPU 来决定

另外有些 linux 操作系统是不区分优先级的,它把所有优先级都视为 5。

setPriority()
方法在实际的开发中,使用的并不多见。

3.2.5、setDaemon()

在 Java 中线程分为两种,一种是用户线程,一种是守护线程。

守护线程是一种特殊的线程,它的作用是为其他线程的运行提供便利的服务,比如垃圾回收线程,就是最典型的守护线程。

当 JVM 检测到应用程序中的所有线程都只有守护线程时,它将退出应用程序,因为没有存在的必要,服务的对象都没了,当然就需要销毁了。

开发者可以通过使用
setDaemon()
方法,传递
true
作为参数,使线程成为一个守护线程,同时可以使用
isDaemon()
方法来检查线程是否是守护线程。

样例代码如下:

public class ThreadA extends Thread {

    @Override
    public void run() {
        try {
            while (true){
                String threadName = Thread.currentThread().getName();
                boolean isDaemon = Thread.currentThread().isDaemon();
                System.out.println("threadName:" + threadName + ",isDaemon:" + isDaemon);
                Thread.sleep(500);
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.setDaemon(true);
        threadA.start();

        Thread.sleep(3000);
        System.out.println("主线程方法执行完毕!");
    }
}

运行结果:

threadName:Thread-0,isDaemon:true
threadName:Thread-0,isDaemon:true
threadName:Thread-0,isDaemon:true
threadName:Thread-0,isDaemon:true
threadName:Thread-0,isDaemon:true
threadName:Thread-0,isDaemon:true
主线程方法执行完毕!

需要特别注意的是:创建守护线程时,
setDaemon(true)
方法必须在线程
start()
方法之前,否则会抛异常。

3.2.6、join()

join()
方法的作用是让调用此方法的主线程被阻塞,仅当该方法执行完成以后,才能继续运行。

从概念上感觉很抽象,我们看一下例子!

public class ThreadA extends Thread {

    @Override
    public void run() {
        try {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
            System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",正在运行");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();

        // 让执行这个方法的线程阻塞(指的是主线程,不是threadA线程)
        threadA.join();

        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        System.out.println(time + " 主线程方法执行完毕!");
    }
}

运行结果:

2023-08-31 12:46:06 当前线程:Thread-0,正在运行
2023-08-31 12:46:09 主线程方法执行完毕!

从运行结果可以得出一个结论,主线程
main
调用
threadA.join()
方法时,会进入阻塞状态,直到线程实例
threadA

run()
方法执行完毕,主线程
main
从阻塞状态变成可运行状态。

此例中主线程
main
会无限期阻塞直到
threadA.run()
方法执行完毕。

比如某个业务场景下,主线程
main
的执行时间是 1s,子线程的执行时间是 10s,同时主线程依赖子线程执行完的结果,此时让主线程执行
join()
方法进行适度阻塞,可以实现此目标。

3.2.7、interrupt()

interrupt()
方法的作用是当线程受到阻塞时,调用此方法会抛出一个中断信号,让线程退出阻塞状态,如果当前线程没有阻塞,是无法中断线程的。

与此对应的还有
isInterrupted()
方法,用于检查线程是否已经中断,但不清除状态标识。

我们先看一个例子!

public class ThreadA extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",count:" + i);
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();

        Thread.sleep(50);

        // 检查线程是否中断,没有尝试终止线程
        if(!threadA.isInterrupted()){
            threadA.interrupt();
        }
    }
}

运行结果:

2023-08-31 14:46:55:053 当前线程:Thread-0,count:0
2023-08-31 14:46:55:054 当前线程:Thread-0,count:1
...
2023-08-31 14:46:55:839 当前线程:Thread-0,count:9999

如果当前线程没有阻塞,调用
interrupt()
起不到任何效果。

下面我们对
ThreadA
类在尝试改造一下,让它每执行一次停顿 1 秒,内容如下:

public class ThreadA extends Thread {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
                System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + ",count:" + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();

        Thread.sleep(2000);

        // 检查线程是否中断,没有尝试终止线程
        if(!threadA.isInterrupted()){
            threadA.interrupt();
        }
    }
}

运行结果:

2023-08-31 14:51:19:792 当前线程:Thread-0,count:0
2023-08-31 14:51:20:798 当前线程:Thread-0,count:1
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.example.thread.ThreadA.run(ThreadA.java:22)

很明显,当线程处于阻塞状态时,调用
interrupt()
方法,可以让线程退出阻塞,起到终止线程的效果。

3.2.8、isAlive()

isAlive()
方法的作用是检查线程是否处于活动状态,只要线程启动且没有终止,方法返回的就是
true

看一下例子!

public class ThreadA extends Thread {

    @Override
    public void run() {
        System.out.println("当前线程:" + Thread.currentThread().getName() + ",isAlive:" + Thread.currentThread().isAlive());
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        System.out.println("begin == " + threadA.isAlive());

        threadA.start();

        Thread.sleep(1000);
        System.out.println("end == " + threadA.isAlive());
    }
}

运行结果:

begin == false
当前线程:Thread-0,isAlive:true
end == false

从运行结果上可以看出,线程启动前
isAlive=false
,线程运行中
isAlive=true
,线程运行完成
isAlive=false

3.3、静态方法

在 JDK 中,Thread 类还提供了如下几个常用的静态方法来操作线程。

方法 描述
public static Thread currentThread() 返回对当前正在执行的线程对象的引用
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程
public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响
public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true
public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流

下面我们依次来看看它们之间的用法。

3.3.1、currentThread()

currentThread()
方法的作用是返回当前正在执行线程对象的引用,在上文中有所介绍。

下面我们再来看看几个例子!

public class ThreadA extends Thread {

    static {
        System.out.println("静态块打印的线程名称:" + Thread.currentThread().getName());
    }

    public ThreadA() {
        System.out.println("构造方法打印的线程名称:" + Thread.currentThread().getName());
    }

    @Override
    public void run() {
        System.out.println("run()方法打印的线程名称:" + Thread.currentThread().getName());
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();
    }
}

运行结果:

静态块打印的线程名称:main
构造方法打印的线程名称:main
run()方法打印的线程名称:Thread-0

从运行结果可以看出,线程类的构造方法、静态块是被主线程
main
调用的,而线程类的
run()
方法才是用户线程自己调用的。

再来看看另一个例子!

public class ThreadA extends Thread {

    public ThreadA() {
        System.out.println("构造方法打印 Begin...");
        System.out.println("Thread.currentThread打印的线程名称:" + Thread.currentThread().getName());
        System.out.println("this.getName打印的线程名称:" + this.getName());
        System.out.println("构造方法打印 end...");
    }

    @Override
    public void run() {
        System.out.println("run()方法打印 Begin...");
        System.out.println("Thread.currentThread打印的线程名称:" + Thread.currentThread().getName());
        System.out.println("this.getName打印的线程名称:" + this.getName());
        System.out.println("run()方法打印 end...");
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        System.out.println("===============");
        threadA.start();
    }
}

运行结果如下:

构造方法打印 Begin...
Thread.currentThread打印的线程名称:main
this.getName打印的线程名称:Thread-0
构造方法打印 end...
===============
run()方法打印 Begin...
Thread.currentThread打印的线程名称:Thread-0
this.getName打印的线程名称:Thread-0
run()方法打印 end...

从运行结果可以看出,
Thread.currentThread
方法返回的未必是
Thread
本身,而是当前正在执行线程对象的引用,这和通过
this.XXX()
返回的对象是有区别的。

3.3.2、yield()

yield()
方法的作用是暂停当前执行的线程对象,并执行其他线程。这个暂停会放弃 CPU 资源,并且放弃 CPU 的时间不确定,有可能刚放弃,就获得 CPU 资源了,也有可能放弃好一会儿,才会被 CPU 执行。

相关例子如下。

public class ThreadA extends Thread {

    private String name;

    public ThreadA(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name  + ":" + i);
            if ("t1".equals(name)) {
                System.out.println(name  + ":" + i +"......yield.............");
                Thread.yield();
            }
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA1 = new ThreadA("t1");
        ThreadA threadA2 = new ThreadA("t2");

        threadA1.start();
        threadA2.start();
    }
}

运行结果:

t2:0
t1:0
t2:1
t2:2
t2:3
t2:4
t1:0......yield.............
t1:1
t1:1......yield.............
t1:2
t1:2......yield.............
t1:3
t1:3......yield.............
t1:4
t1:4......yield.............

从运行结果上可以看出,调用
yield()
方法可以让线程放弃 CPU 资源,循环次数越多,越明显。

3.3.3、sleep()

sleep()
方法的作用是在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。这个
正在执行的线程
指的是
Thread.currentThread()
返回的线程。

根据 JDK API 的说法,该线程不丢失任何监视器的所属权,换句话说就是不会释放锁,如果
sleep()
代码上下文被加锁了,锁依然在,只是 CPU 资源会让出给其他线程。

相关例子如下。

public class ThreadA extends Thread {

    @Override
    public void run() {
        try {
            String begin = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(begin + " 当前线程:" + Thread.currentThread().getName());

            Thread.sleep(3000);

            String end = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(end + " 当前线程:" + Thread.currentThread().getName());

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();
    }
}

运行结果如下:

2023-08-31 18:06:41:459 当前线程:Thread-0
2023-08-31 18:06:44:464 当前线程:Thread-0
3.3.4、holdsLock()

holdsLock()
方法表示当且仅当
当前线程在指定的对象上保持监视器锁时,才返回 true
,简单的说就是检测一个线程是否拥有锁。

相关例子如下。

public class ThreadA extends Thread {

    private String lock = "lock";

    @Override
    public void run() {
        System.out.println("当前线程:" + Thread.currentThread().getName() + ",Holds Lock = " + Thread.holdsLock(lock));

        synchronized (lock){
            System.out.println("当前线程:" + Thread.currentThread().getName() + ",Holds Lock = " + Thread.holdsLock(lock));
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();
    }
}

运行结果如下:

当前线程:Thread-0,Holds Lock = false
当前线程:Thread-0,Holds Lock = true

关于线程锁,我们会在后期的文章中进行分享介绍。

3.3.5、dumpStack()

dumpStack()
方法的作用是将当前线程的堆栈跟踪打印至标准错误流。此方法仅用于调试。

相关例子如下。

public class ThreadA extends Thread {

    @Override
    public void run() {
        System.out.println("当前线程:" + Thread.currentThread().getName());
        Thread.dumpStack();
    }
}
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();
    }
}

运行结果如下:

当前线程:Thread-0
java.lang.Exception: Stack trace
	at java.lang.Thread.dumpStack(Thread.java:1336)
	at com.example.thread.ThreadA.run(ThreadA.java:16)

Thread.dumpStack
会将当前线程的堆栈跟踪信息打印出控制台。

四、小结

本文主要围绕线程类
Thread
相关的常用方法进行详解,内容难免有所遗漏,欢迎网友留言指出。

五、参考

1、
五月的仓颉 - Thread中的实例方法介绍

2、
菜鸟教程 - Java 多线程编程

Java 8 引入的Stream API提供了一种新的数据处理方式,它以声明式、函数式的编程模型,极大地简化了对集合、数组或其他支持数据源的操作。Stream可以被看作是一系列元素的流水线。允许你高效地对大量数据执行复杂的过滤、映射、排序、聚合等操作,而无需显式地使用循环或者临时变量。Stream API的设计理念主要包括两个方面:
链式调用

惰性求值
。链式调用允许我们将多个操作连接在一起,形成一个流水线,而惰性求值意味着只有在真正需要结果的时候才执行计算,从而避免了不必要的计算开销。

接下来我们就来盘点一下日常开发中常用的一些Stream API。

创建Stream

  • 集合创建
List<String> list = new ArrayList<>(); 
// 串行流
Stream<String> stream = list.stream();
// 并行流
Stream<String> parallelStream = list.parallelStream();
  • 数组创建
String[] strs = new String[3];  
Stream<String> stream = Arrays.stream(strs);
  • 使用
    Stream.of(T...values)
    创建
Stream<String> stream = Stream.of("Apple", "Orange", "Banana");
  • 使用Stream.generate()创建流
// 生成一个无限流,通过limit()限制元素个数  
Stream<Double> randomStream = Stream.generate(Math::random).limit(5);
  • 使用Stream.iterate()创建流
// 生成一个等差数列,通过limit()限制元素个数 
Stream<Integer> integerStream = Stream.iterate(0, n -> n + 2).limit(5);
  • 使用IntStream、LongStream、DoubleStream创建原始类型流
// 使用IntStream创建  
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]  
  
// 使用LongStream创建  
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

IntStream我们使用的地方还是比较多的,比如我们按照下标遍历一个集合时,同常的做法是:for(int i = 0; i < list.size(); i++){},我们可以使用IntStream去改造一下,IntStream.rangeClosed(0, list.size()).forEach();

中间操作

中间操作是构建流水线的一部分,用于对流进行转换和处理,但它们并不会触发实际的计算。

  • 过滤操作(filter)
    过滤操作用于筛选流中的元素,保留满足指定条件的元素。
    Stream<T> filter(Predicate<? super T> predicate)

    filter
    接受一个谓词Predicate,我们可以通过这个谓词定义筛选条件,
    Predicate
    是一个函数式接口,其包含一个
    test(T t)
    方法,该方法返回boolean。
private static void filterTest(){  
    List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");  
    // 过滤长度大于5的水果  
    List<String> filteredFruits = fruits.stream().filter(fruit -> fruit.length() > 5).collect(Collectors.toList());  
    System.out.println("长度大于5的水果: "+ filteredFruits);  
}

private static void filterTest(List<Student> students){  
    List<Student> filterStudents = students.stream()  
            .filter(student -> Objects.equals("武汉大学", student.getSchool()))  
            .collect(Collectors.toList());  
  
    filterStudents.forEach(System.out::println);  
}

打印结果:
image.png

  • 映射操作(map/flatMap)
    映射操作用于对流中的每个元素进行转换。他有map以及flatMap两种操作。map就是基本的映射操作,对每个元素进行提取转换。
// 将实体层映射成学生姓名字符串  
List<String> names = students.stream()  
        .map(Student::getName)  
        .collect(Collectors.toList());

// 将字符串转大写。
List<String> upperList = Lists.newArrayList("hello", "world", "stream", "api").stream().map(String::toUpperCase).collect(Collectors.toList());

日常开发中map操作我们用的非常多,比如数据库中查询出来的DO实体,我们需要转换为VO返回给前端页面展示,这时候我们可以使用map进行转换操作:

List<StudentDO> studentDOList = studentMapper.listStudents();

List<StudentVO> studentVOList = studentDOList.stream().map(studentDO -> {
	StudentVO studentVO = StudentVO.builder().studentNo(studentDO.getId())
	.studentName(studentDO.getName()).build();
	return studentVO;
}).collect(Collectors.toList());

而flatMap的作用略微特殊,它用于将一个元素映射为一个流,然后将所有流连接成一个流。这在处理嵌套结构或集合中的元素是另一个集合的情况下非常有用。

List<List<String>> nestedWords = Arrays.asList(
    Arrays.asList("Java", "Kotlin"),
    Arrays.asList("Python", "Ruby"),
    Arrays.asList("JavaScript", "TypeScript")
);

// 使用 flatMap 将嵌套的 List<String> 转换为一个扁平的 List<String>, 结果将是包含所有单词的扁平流
List<String> wordList = nestedWords.stream()  
        .flatMap(List::stream).collect(Collectors.toList());

System.out.println(wordList);

// 打印结果: [Java, Kotlin, Python, Ruby, JavaScript, TypeScript]

flatMap
在使用时,通常会涉及到处理复杂的数据结构,比如处理嵌套的对象集合或者进行数据的扁平化。

@Data
@Builder
class Student {  
    private String name;  
    private List<Integer> grades;  
}

@Data
@Builder
class ClassRoom {  
    private List<Student> studentList;  
}

@Data
@Builder
class School {  
    private List<ClassRoom> classRoomList;  
}

School school = School.builder()  
        .classRoomList(Lists.newArrayList(  
                ClassRoom.builder().studentList(Lists.newArrayList(  
                        Student.builder().name("Alice").gradeList(Lists.newArrayList(90, 85, 88)).build(),  
                                  Student.builder().name("Bob").gradeList(Lists.newArrayList(78, 92, 80)).build()  
                )).build(),  
                ClassRoom.builder().studentList(Lists.newArrayList(  
                        Student.builder().name("Charlie").gradeList(Lists.newArrayList(95, 89, 91)).build(),  
                        Student.builder().name("David").gradeList(Lists.newArrayList(82, 87, 79)).build()  
                )).build()  
        ))  
        .build();  
  
// 使用 flatMap 扁平化处理获取所有学生的所有课程成绩  
List<Integer> allGrades = school.getClassRoomList().stream()  
        .flatMap(classroom -> classroom.getStudentList().stream())  
        .flatMap(student -> student.getGradeList().stream())  
        .collect(Collectors.toList());  
  
System.out.println(allGrades);
// 打印结果:[90, 85, 88, 78, 92, 80, 95, 89, 91, 82, 87, 79]
  • mapToInt操作
    mapToInt
    是 Stream API 中的一种映射操作,专门用于将元素映射为
    IntStream
    。通过
    mapToInt
    ,你可以将流中的元素映射为
    int
    类型,从而进行更专门化的操作,例如数值计算。
int totalAge2 = students.stream().mapToInt(Student::getAge).sum();

类似的还有
mapToLong

mapToDouble
操作,这两个操作类似于
mapToInt
,分别用于将流中的元素映射为
LongStream

DoubleStream

  • 排序操作(sorted)
    排序操作用于对流中的元素进行排序。
List<String> cities = Lists.newArrayList("New York", "Tokyo", "London", "Paris");

// 对城市按字母顺序排序
List<String> sortedStream = cities.stream().sorted().collect(Collectors.toList());  

对于集合中对象的排序,sorted要求待比较的元素必须实现Comparable接口。

@Data  
@Builder  
static class Student implements Comparable<Student>{  
    private String name;  
    private Integer age;  
      
    @Override  
    public int compareTo(Student other) {  
        return other.getAge()-this.getAge();  
    }  
}

List<String> sortedList = students.stream()  
        .sorted()  
		.map(Student::getName()) 
        .collect(Collectors.toList());    

如果没有实现,就需要将比较器作为参数传递给
sorted(Comparator<? super T> comparator)

@Data  
@Builder  
static class Student {  
    private String name;  
    private Integer age;
}

List<String> sortedList = students.stream()  
        .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
        .map(Student::getName()) 
        .collect(Collectors.toList());    
  • 去重操作(distinct)
    去重操作用于去除流中的重复元素。distinct基于Object.equals(Object)实现。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6);  
// 去除重复的数字  
List<Integer> distinctList = numbers.stream().distinct().collect(Collectors.toList());

// 或者去除学生中姓名相同的
List<String> studentNameList = students.stream()
								.map(Student::getName()) 
								.distinct()
						        .collect(Collectors.toList());    

  • 截断操作(limit)
    截断操作用于限制流中元素的数量。limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6); 
// 只取前三个数字 
List<Integer> limitedList = numbers.stream().limit(3).collect(Collectors.toList());

// 取土工工程专业的年龄最小的前两名学生
List<Student> limitStu = students.stream()  
        .filter(student -> Objects.equals("土木工程", student.getMajor())) 
        .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
        .limit(2)  
        .collect(Collectors.toList());
  • 跳过操作(skip)
    跳过操作用于跳过流中的前几个元素,返回由后面所有元素构造的流,如果n大于满足条件的集合的长度,则会返回一个空的集合。作用上跟limit相反。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6); 
// 跳过前三个数字,返回后面的数字 
List<Integer> limitedList = numbers.stream().skip(3).collect(Collectors.toList());

// 跳过土工工程专业的年龄最小的前两名学生,取后面的学生
List<Student> limitStu = students.stream()  
        .filter(student -> Objects.equals("土木工程", student.getMajor())) 
        .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
        .skip(2)  
        .collect(Collectors.toList());
  • peek操作
    peek
    方法对每个元素执行操作并返回一个新的 Stream。
    peek
    的主要目的是用于调试和观察流中的元素,通常用于打印调试信息、记录日志或其他类似的目的,而不会改变流中元素的结构。
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");  
  
List<String> modifiedWords = words.stream()  
        .filter(word -> word.length() > 5)  
        .peek(word -> System.out.println("Filtered Word: " + word))  
        .map(String::toUpperCase)  
        .peek(word -> System.out.println("Uppercase Word: " + word))  
        .collect(Collectors.toList());

Stream的终端操作

终端操作是对流进行最终计算的操作,执行终端操作后,流将被消耗,不能再被使用。

  • 迭代forEach操作
    forEach
    迭代操作,用于对流中的每个元素执行指定的操作。
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");

// 使用 forEach 输出每个水果
fruits.stream().forEach(fruit -> System.out.println(fruit));
// 执行forEach时可省略 stream(),即
fruits.forEach(fruit -> System.out.println(fruit));
// 或
fruits.stream().forEach(System.out::println);
  • 收集操作(collect)
    通过
    collect()
    方法结合
    java.util.stream.Collectors
    工具类将Stream转换为另一种形式,例如列表、集合(toList, toSet, toMap)、映射或归约结果。如上述示例中的:
  1. 收集到List
    使用
    Collectors.toList()
// 跳过土工工程专业的年龄最小的前两名学生,取后面的学生
List<Student> limitStu = students.stream()  
        .filter(student -> Objects.equals("土木工程", student.getMajor())) 
        .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
        .skip(2)  
        .collect(Collectors.toList());
  1. 收集到Set
    使用
    Collectors.toSet()
// 将学生姓名收集到Set
Set<String> studentNameSet = students.stream().map(Student::getName)
		.collect(Collectors.toSet());
  1. List转Map
    使用
    Collectors.toMap
    。日常开发中使用很多。
// 转换为年龄对应的学生信息  
Map<Integer, Student> studentMap = students.stream().collect(Collectors.toMap(
											Student::getAge, 
											Function.identity(), 
											(e1,e2) -> e1));

这段代码代表,我们使用年龄作为Map的key,对应学生信息作为value。
Function.identity()
:这是一个提取元素自身的映射函数。
(e1, e2) -> e1
:这是一个合并冲突的操作。如果在流中存在相同的年龄(相同的键),这个函数定义了当出现重复键时应该如何处理。在这里,我们选择保留第一个出现的元素,即保留先出现的
Student
对象。当然我们还可以这样
(e1, e2) -> {...}
自定义合并冲突策略,例如:

// 转换为年龄对应的学生信息,如果年龄相同,则取名字较长的  
Map<Integer, Student> studentMap = students.stream().collect(Collectors.toMap(Student::getAge, Function.identity(), (e1,e2) -> {  
    return e1.getName().length() > e2.getName().length() ? e1 : e2;  
}));

如果value的值是一些number,我们也可以做一些加减乘除之类的合并。

日常开发中,这个用法很频繁。

  1. 字符串拼接:
    使用
    Collectors.joining(拼接符)
List<Student> students  = Lists.newArrayList(  
        Student.builder().name("Alice").gradeList(Lists.newArrayList(90, 85, 88)).build(),  
        Student.builder().name("Bob").gradeList(Lists.newArrayList(78, 92, 80)).build()  
);  
  
String studentName = students.stream().map(Student::getName).collect(Collectors.joining(","));

// 打印出来:Alice,Bob
  1. 分组
    即按照集合中的元素的某个属性进行分组,转换为
    Map<Object, List<Object>>
    :
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");  
Map<Integer, List<String>> lengthToNamesMap = fruits.stream()  
                    .collect(Collectors.groupingBy(String::length));

// 按照年龄分组  
Map<Integer, List<Student>> studentMap = students.stream().collect(Collectors.groupingBy(Student::getAge));

// 连续进行分组
Map<String,Map<String,List<Student>>> groupsStudent = students.stream()  
        // 先按照学校分组  
        .collect(Collectors.groupingBy(Student::getSchool  
        // 再按照专业分组  
        ,Collectors.groupingBy(Student::getMajor)));
  1. counting()
    counting()
    收集器用于计算流中元素的数量。等同于Stream的
    count()
    操作。
long studentCount = students.stream().collect(Collectors.counting());
// 效果同等于
long studentCount = students.stream().count();
  1. maxBy()
    maxBy()
    基于指定的比较器,用于找到流中的最大的元素。等同于Stream的
    max
    操作
// 年龄最大的学生
Student olderStudent = students.stream()  
        .collect(Collectors.maxBy((s1,s2) -> s1.getAge()- s2.getAge())).orElse(null);

Student olderStudent2 = students.stream()  
    .collect(Collectors.maxBy(Comparator.comparing(Student::getAge))).orElse(null);

// 等价于stram的max
Student olderStudent = students.stream()
	.max(Comparator.comparing(Student::getAge)).orElse(null);    
  1. minBy()
    minBy()
    基于指定的比较器,用于找到流中的最小的元素。等同于Stream的
    min
    操作。
// 年龄最小的学生
Student youngStudent = students.stream()  
    .collect(Collectors.minBy(Comparator.comparing(Student::getAge))).orElse(null); 

Student youngStudent = students.stream()
	.min(Comparator.comparing(Student::getAge)).orElse(null);
  1. averagingInt
    averagingInt()
    收集器用于计算流中元素的平均值。
// 求学生平均年龄
double avgAge = students.stream()  
        .collect(Collectors.averagingInt(Student::getAge));
  1. summarizingInt()
    summarizingInt()
    收集器用于计算流中元素的汇总统计信息,包括总数、平均值、最大值和最小值。
// 一次性得到元素个数、总和、均值、最大值、最小值
IntSummaryStatistics summaryStatistics = students.stream().collect(Collectors.summarizingInt(Student::getAge));

System.out.println("总数:" + summaryStatistics.getCount()); 
System.out.println("平均值:" + summaryStatistics.getAverage()); 
System.out.println("最大值:" + summaryStatistics.getMax()); 
System.out.println("最小值:" + summaryStatistics.getMin());
  • partitioningBy()
    将流中的元素按照指定的条件分成两个部分。在分区中key只有两种情况:true或false,目的是将待分区集合按照条件一分为二,分区相对分组的优势在于,我们可以同时得到两类结果,在一些应用场景下可以一步得到我们需要的所有结果,比如将数组分为奇数和偶数。
// 分为武汉大学学生,非武汉大学学生
Map<Boolean,List<Student>> partStudent = students.stream()  
        .collect(Collectors.partitioningBy(student -> Objects.equals("武汉大学",student.getSchool())));
  • count操作
    count
    用于计算流中的元素个数。效果等同于
    Collectors.counting()
long studentCount = students.stream().count();
// 效果同等于
long studentCount = students.stream().collect(Collectors.counting());

  • max操作
    基于指定比较器,max用于找到流中最大的元素。效果等同于
    Collectors.maxBy()
Student olderStudent = students.stream()
	.max(Comparator.comparing(Student::getAge)).orElse(null);  

Student olderStudent2 = students.stream()  
    .collect(Collectors.maxBy(Comparator.comparing(Student::getAge))).orElse(null);

  • min操作
    基于指定比较器,min用于找到流中最小的元素。效果等同于
    Collectors.minBy()
Student youngStudent = students.stream()
	.min(Comparator.comparing(Student::getAge)).orElse(null);
	
// 年龄最小的学生
Student youngStudent = students.stream()  
    .collect(Collectors.minBy(Comparator.comparing(Student::getAge))).orElse(null); 

  • reduce操作
    reduce
    用于对流中的元素进行归约操作,得到一个最终的结果。
// 计算学生的总年龄
int totalAge1 = students.stream()  
        .map(Student::getAge)  
        .reduce(0, (a,b) -> a+b);

// 也可以使用Integer.sum
int totalAge2 = students.stream() 
        .map(Student::getAge)  
        .reduce(0, Integer::sum);

// 也可以不设置初始值0,直接Integer.sum,但是返回的是Optional
int totalAge3 = students.stream()  
       .map(Student::getAge)  
       .reduce(Integer::sum).orElse(0);
  • findFirst操作
    findFirst
    用于查找流中的第一个元素。也即
    list.get(0)
Student firstStu = students.stream()  
        .filter(student -> Objects.equals("土木工程", student.getMajor()))  
        .findFirst().orElse(null);
        



曾经有个小兄弟问我,他有一段代码类似 Student firstStu = students.get(0)。他们组长让他优化优化,然后就用了这种方式优化的。

热点随笔:

·
2024年,我又开始用Linux桌面作为主力系统了~
(
程序设计实验室
)
·
.NET 团队公布.NET 9开发目标 并发布.NET9的首个预览版
(
张善友
)
·
糟糕,接口被刷了,怎么办?
(
苏三说技术
)
·
C# 实现刘谦春晚魔术
(
柴油飞机
)
·
【开工大吉】推荐4款开源、美观的WPF UI组件库
(
追逐时光者
)
·
到什么程度才叫精通 Linux?
(
陶朱公Boy
)
·
你的编程能力从什么时候开始突飞猛进的?
(
陶朱公Boy
)
·
架构设计:千万级流量下的数据强依赖降级
(
Hello-Brand
)
·
2024初三集训模拟测试1
(
xrlong
)
·
程序员们有什么好的编程习惯?
(
陶朱公Boy
)
·
2023年终总结——从零到一,从自己,到世界
(
.N1nEmAn
)
·
【译】我为 .NET 开发人员准备的 2023 年 Visual Studio 10 大新功能
(
郑子铭
)

热点新闻:

·
OpenAI首个视频生成模型Sora发布,网友:整个行业RIP
·
.NET 9 首个预览版发布 —— 打造面向云原生 & AI 的开发平台
·
40多岁学编程!扎根一线60余年,这位“蓝领院士”拼了
·
字节跳动CEO张一鸣为家乡捐款266万 被立功德碑!已经拆除
·
突发!OpenAI联创Karpathy又双叒离职了,自称「懂我的都懂」
·
体验比想象中更好?过年开小鹏G6回家,我有了这些感悟
·
Windows 11 24H2六大重磅新功能首曝:基于新一代Windows平台打造
·
大厂失业的中年人:我花一年时间找到了新方向⑩|在春天许一个愿望
·
大厂互联网黑话卷到了国外,网友:过年了,请说点人话吧
·
何小鹏内部信曝光,3 年 30 款新车!
·
Rust是否会替代C#?解析微软最新招聘需求引发的热议
·
大厂年终奖疯狂不再?打工人集体怀念过去:想念那个靠年终奖一夜暴富的时代

随着 JavaScript 开发变得越来越广泛,命名空间和依赖关系变得越来越难以处理。人们已经开发出不同的解决方案以模块系统的形式来解决这个问题。

CommonJS(CJS)

CommonJS 是一种同步加载模块的规范,主要用于服务器端的 Node.js 环境。

// 模块导出
module.exports = {
  // 模块内容
};

// 模块导入
const module = require('module');

top:CommonJS
加载的是一个对象(即
module.exports
属性),该对象只有在脚本运行完才会生成。

AMD(Asynchronous Module Definition)

AMD
是一种_异步_加载模块的规范,主要用于浏览器端的 JavaScript 开发。它允许模块在加载完成后立即执行,而不会阻塞页面加载。

// 模块定义
define(['dependency1', 'dependency2'], function (dependency1, dependency2) {
  // 模块内容
  return {
    // 模块导出内容
  };
});

// 模块加载
require(['module'], function (module) {
  // 模块加载完成后执行的逻辑
});

UMD(Universal Module Definition)

UMD
是一种通用的模块定义格式,旨在兼容
CommonJS

AMD
以及全局变量导出的方式。

实现原理为:先判断是否支持
node.js
的模块,存在就使用
node.js
;再判断是否支持
AMD

define
是否存在),存在则使用*
*AMD
的方式加载。这就是所谓的
UMD**。

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['exports', 'module'], factory);
  } else if (typeof exports === 'object' && typeof module === 'object') {
    // CommonJS
    factory(exports, module);
  } else {
    // 浏览器环境下暴露到全局变量
    factory((root.myModule = {}), root.myModule);
  }
})(this, function (exports, module) {
  // 模块内容
});

UMD
实质上创建了一种使用两者之一的方法,同时还支持全局变量定义。因此,
UMD
模块能够在客户端和服务器上工作。

ESM(ES Module)

ESM

ECMAScript
官方提供的模块标准,支持异步加载,具有静态导入和导出,使得代码更具可靠性和可预测性。

// 模块导出
export const module = {
  // 模块内容
};

// 模块导入
import {module} from 'module';

概括

不同的模块规范各有优劣,选择合适的规范取决于项目的需求和目标平台。

  • CJS 为同步加载,主要用于服务器端编程,因为在服务器端加载速度不太重要,而且同步加载更容易理解和管理。
  • AMD 为异步加载,适用于浏览器环境中的异步模块加载,尤其是在 Web 应用中采用模块化开发时使用。
  • UMD 为通用形,通常用作 ESM 不起作用时的后备方案,适用于同时在浏览器和 Node.js 环境中使用的代码,使得代码具有更大的通用性。
  • ESM 适用于现代浏览器以及支持 ES6 模块的 Node.js 版本,使得代码更具可维护性和可移植性。

引言

最初考虑引用“ DevOps 已死,平台工程才是未来”作为标题,但这样的表达可能太过于绝对。最终,决定用了“扯淡的”这个词来描述 DevOps,但这并不是一种文明的表达方式。 文章旨在重新审视 DevOps 和平台工程,将分别探讨 DevOps 和平台工程的概念,并重点分析平台工程所倡导的一些核心内容。同时,希望通过本文能够给从事内部开发平台(IDP)工作的同学们带来一些思考。

DevOps的目标

在 2009 年,DevOps 这一概念就被提出,重点强调团队协作、自动化工具和流程改进,旨在提高软件开发和部署的速度和质量。然而,提出之后有近 15 年了,发现这一方法并未如预期完美实现了目标。在我们公司内部,我们也会发现软件交付成本仍然还是较高,从部署发布工具的角度来看,无论是 J-ONE、JDOS 还是目前的行云部署,对于研发人员日常部署发布仍存在一定的成本,但这种现象好像不仅仅是工具层面的问题。

DevOps 本身是一种理念,强调团队协作,使开发团队和运维团队能够紧密合作。尽管强调了自动化和工具的重要性,但它并没有明确指出具体的发展方向。因此,出现了平台工程(Platform Engineering)这一理念。虽然最早是谁提出的已无法考证,但在 2022 年 7 月份,一条Twitter上的消息“DevOps is dead, long live Platform Engineering” 在国内外的 DevOps 圈子迅速传播开来,并得到了广泛的回应。

平台工程(Platform Engineering)是一种新的运维理念,强调内部开发平台应该提供技术研发人员自服务的能力。其核心观点之一是通过屏蔽基础设施的复杂性,为技术研发人员提供灵活的工具链和工作流程。这样,可以利用平台的基本能力,自主解决问题,无需依赖平台层的参与,使得开发团队能够更加高效地开展工作,提高软件交付的速度和质量。

平台工程的定义

平台工程是设计和构建工具链和工作流的学科,可在云原生时代为软件工程组织提供自助服务功能。平台工程师提供的集成产品通常被称为“内部开发人员平台”,涵盖了应用程序整个生命周期的运营需求。 --定义来自 platformengineering.org (关于平台工程的定义较多,但大部分意思较一致:主要是倡导自助服务减少底层基础支撑工具的复杂性和不确定性,减化工作流程,减少最终用户在使用过程中的认知成本,从而改善了最终用户的体验,和提高生产效率)

平台工程和 DevOps 都是软件开发和运维领域的概念,它们共同关注提高软件开发和部署的效率和质量,但它们的重点和方法有所不同。平台工程着重于构建可重用的平台架构,提供场景化的能力,提供自助化的体验。而 DevOps 则侧重于团队协作、自动化工具和流程改进,以提高软件开发和部署的速度和质量。

在 2023 年,Gartner 已将平台工程列为顶级战略趋势之一。最近发布的 2024 年十大技术趋势中,Gartner 再次提到了平台工程,并且将其提升了一个级别,这表明平台工程在业界的认可度得到进一步提升。

在过去的几年中,人们一直追求 DevOps,并从能力成熟度的角度推动提升。然而,对于投入和产出的量化评估却相对模糊。平台工程提出了一些衡量其价值产出的方式,包括自助式体验和尽可能减少人力投入。通过致力于建设自助化、场景化的能力,提供有价值的平台。

回到本文的标题,我们来谈谈为什么开发人员不愿意承担运维的工作。

开发为什么不想做运维

DevOps 强调团队协作,并鼓励开发人员承担一定的运维工作。然而,在现实中,为什么这一点往往难以实现?我认为主要有以下几个方面的理由:


专注于核心开发任务:
开发人员通常更倾向于日常软件开发任务,他们可能没有太多时间和精力在其他方面,否则会影响日常任务的工作进展。

不熟悉或不感兴趣:
开发人员可能没有足够的经验来处理运维的工作,或者他们对运维工作不感兴趣,导致在运维方面缺乏积极性。

运维的锅太重、事太杂:
运维工作涉及到生产环境,因此其责任和影响范围较大。任何运维失误都可能导致系统故障、服务中断或数据丢失等严重后果。因此,对于开发人员来说,承担运维工作可能带来额外的压力和责任。此外,运维工作通常包括各种琐碎而繁杂的任务,包括7*24值班。

缺乏好用的工具和平台支持:
缺乏易用且高效的自动化工具和平台,运维工作就会更加依赖手工操作,从而增加了运维的成本和复杂性。

以上可能是开发人员不太愿意承担运维工作的一些可能的理由。我接下来看下运维的本质是什么?

运维工作的本质

运维工作重点是保障系统的安全和稳定运行。它不仅需要 7x24小时监控线上环境的稳定性,还需要处理各种日常的运维任务。这些任务可能包括资源管理、日常巡检、故障排查与修复、工单处理等。

最近,一些大厂经历了重大的线上稳定性故障,这给业界带来了很大的关注。

最近的这些线上故障对整个行业产生了极大的警示,所有企业都一样面临着线上稳定性挑战。

带来的一些思考

安全生产,警钟长鸣:
面对线上问题,我们绝不能单纯地追求速度和省事,对于任何线上操作,都必须保持敬畏之心。

安全生产,人人有责:
无论是开发人员编写的错误代码逻辑,还是运维人员错误的升级操作,最终都有可能给公司带来无法估量的损失。

生产环境的稳定性,最难得不是技术,而是依赖无数细节的落地,稳定性的保障需要大量的投入,然而这个事最大的问题就是,难被认可,以及怎么来衡量做的好呢?网上曾经一个段子,大概意思就是“那些代码写的没有 Bug 的人,往往默默无闻,甚至可能被干掉;相反,那些经常写 Bug 的同学,因为日常忙碌于修复 Bug ,反而能风生水起”,当然,开发不愿意承担运维的原因,确实是因为线上稳定性的责任重大,同时运维工作的负担也很重,并且缺乏适用的工具和平台来支持。

然而,平台工程被提出,作为一种新的理念,旨在解决这些问题,并提高软件交付流程。接下来聊一聊,与 DevOps 相比【平台工程】的成功关键因素有哪些。

平台工程成功的关键因素

如何在公司内推动平台工程

作为一个相对新颖的概念,平台工程已连续两年获得 Gartner 的认可,将其推向了我们不得不关注的重要地位。要在公司内推动平台工程,我认为需明确以下几个方面:


平台范围:
内部有许多工具,首先要确立权威或认证的工具,进行持续投入与迭代,而非各自发展,以免造成重复建设和成本的浪费。

平台文化:
平台到底是为谁而做,为谁服务,技术研发人员是我们的上帝,建立以服务技术研发人员为主的平台文化,同时满足公司管理角度。

平台目标:
核心目标是构建场景化的工具,使技术人员能在平台中自助化使用,把场景化、自助化作为核心目标。

平台Owner:
企业内的IPD不可能集中在同一个部门,因此确定特定场景的 Owner 至关重要,可以消除职责边界不清晰的问题。

需求来源:
一切以研发需求为主,要兼顾研发人员的使用体验,避免大而全的版本升级改动,导致研发迁移系统,迁移资源,从而带来的额外使用成本。

平台API:
内部平台天然就应具备丰富API,满足内部研发的需求,并也应提供详细的文档让技术人员使用。

综上,从全局的维度讨论了如何在内部推动平台工程。接下来,我们探讨一下平台工程下的工具应具备哪些特质:

平台工程下建设什么样的工具

个人认为,相较于面向消费者的产品,内部工具更为重要。因为消费者产品用户有选择权,但内部人员并无太多选择余地,最多只是抱怨几句,却仍需继续使用。要打造令内部人员满意的工具,个人觉得至少需要以下关键属性:


产品化:
内部的工具平台一定要产品化,定位于服务全集团,而非局限自己所在部门的几个人,或者几十人使用,一定要把目标用户定位是集团内所有研发同学,只有这样才能把工具做好。

用户体验:
重视用户体验,除了提供基础的GUI界面,API等能力之外,另外也要注意屏蔽复杂的后端逻辑,降低用户的使用成本。

集成化:
这里讲集成,不仅是像目前行云/泰山一样通过工具市场把各个工具集成到平台上就行了,这些仅完成了第一步,还是以研发使用场景为目标,以应用为视角工作区,例如在发布时,整合监控、日志、预案、告警等可观测的视图,让用户在一个地方满足所有该场景的需求。

自助化:
用户无需平台同学的协助,能满足一切功能,这里举个例子,我们去银行取钱,在柜台人工可以取,但需银行人员的协助,但我们通过ATM,一样也可以完全自助的取钱。

平台工程下的内部开发团队

在平台工程背景下,内开发团队可能会有以下共性情况,例如这四方面:


产品化:
内部工具再需求把控方面特别容易被定制,经过一段时间后,可能会演变成了某个人或者某个小部门的定制产品。

优先级:
经常接到或面临“某C-x老板”的高优先级需求。

认可性:
由于与业务脱节,难以衡量价值,长期下来,对产出的价值认可可能会产生疑虑。

重复建设
:内部工具和平台的重复建设问题较为严重。

个人觉得内部平台团队应要坚持做以下几件事:

•持续收集用户需求,并对平台长远规划路线图。
•完善用户使用手册和最佳实践,提升用户体验。
•保持开放心态,一定要提供API。
•持续推广和运营所负责的平台。(生孩子和养孩子)
•针对重复建设问题,加强合作共建,避免陷入小范围的自嗨式“个人/部门工具”建设

如何衡量平台工程的成功呢?

主要在于要从一些指标维度进行衡量评估。如果一个平台或者工具,在做了一年后,对于自身的使用情况一无所知,而仅专注在做功能开发,那么怎么来衡量这个平台带来的价值呢?我觉得最关键的在于要寻找一个关键的指标,这个指标可以是业务维度,也可以是产品维度或组织维度,我抛出几个维度仅供参考:


用户维度
(第一个就是要用户维度,提升用户体验)
◦周活跃用户数
◦赋能业务数
◦NPS净推荐值

产品维度
◦访问效率
◦执行效率
◦执行成功率

组织维度
◦XX周期
◦XX用时

平台工程的未来

针对平台工程的未来发展,目前国内外的情况如下:

国外的情况

当前,国外各大厂像Google、Spotify、Netflix、Walmart等一大批公司均在积极推动平台工程在企业内部的实施,11月份,CNCF正式发布了
平台工程的能力成熟度模型
,分别从5个维度上划分了4个级别,CNCF发布的成熟度模型维度比较粗粒度,主要从团队/人员、平台应用、用户体验、自服务以及平台迭代等方面进行评估,并未对平台功能维度进行详细划分。

国内的情况

在国内,目前平台工程也逐渐受到大家的关注,特别是原来就负责DevOps工具相关的人员,更加关注平台工程带来的新的概念和倡导方向。中国信息通信研究院也正在组织行业内的专家,共同梳理一份符合国内现状的平台工程能力要求标准,会明确平台工程功能维度。我目前也参与了部分工作,如有对此感兴趣的同事,欢迎联系我一同参与。

最后,放个一个Gartner预测的数据,Gartner预测,到 2026 年, 80% 的软件工程组织将建立平台团队,其中 75% 将包含开发者自助服务门户。80%的软件工程组织将建立平台团队作为可重复使用的服务、组件和工具的内部提供者,用于应用程序交付。

可见,
平台工程不仅仅是一种趋势,它是软件交付的未来

作者:京东零售 井亮亮

来源:京东云开发者社区 转载请注明来源