夫战,勇气也,一鼓作气,再而衰,三而竭,彼竭我盈,故克之。——左传 · 曹秽论战

机器学习概述

机器学习的真实含义是,利用计算机来彰显数据背后的真实含义。

机器学习就是把无序的数据转换成有用的信息。如何从移动计算和传感器产生的海量数据中抽取有价值的信息是一个重要课题,大量的经济活动都依赖于信息。

机器学习的主要任务就是分类(数据划分)、回归(数值预测)。

机器学习中的常用术语:特征,目标变量,训练集,测试集,知识表示。

特征是所采集的某几项属性值,大量已知结果的数据就是训练集目标变量是机器学习算法的预测结果。在分类算法中目标变量通常是标称型的,回归算法中通常是连续型的。分类问题中的目标变量也被称为类别。并假定分类问题只存在有限个数的类别。

测试集是用来检测机器学习算法的效果的。一个效果达到预期的机器学习算法可以称之为某一个问题的知识表示

监督学习:分类、回归等知道预测什么的问题,属于监督学习。

无监督学习:即数据没有目标变量,不给定目标值。在无监督学习中,将数据按照相似程度划分为多个类的过程称为聚类,将寻找描述数据统计值的过程称为密度估计。

image-20220315162916575.png

选择机器学习算法需要清楚的两个问题:

  1. 算法的目的是什么,完成何种任务?

    有预测目标值:监督学习算法;否则无监督学习算法。

    目标变量为离散型:分类器算法;

    目标变量为连续型:回归算法;

    将数据分组:聚类算法;

    估计数据与分组的相似程度:密度估计算法。

  2. 需要分析或收集的数据是什么?

    特征值是离散型还是连续型,特征值是否存在缺失的值,数据中是否存在异常值,某个特征发生的频率如何。

一般说来发现最好算法的关键环节是反复试错的迭代过程。

开发机器学习应用程序的步骤

  1. 收集数据:爬虫、传感器、公开数据。
  2. 准备输入数据:保证数据格式符合算法要求。
  3. 分析输入数据:识别异常值、空值,可以通过图像图形来展示数据。这一步是确保数据集中没有垃圾数据。
  4. 训练算法:将数据输入算法,从中抽取知识或信息。
  5. 测试算法:使用测试集测试效果,若不满意则需要回到第4步甚至第1步。
  6. 使用算法:在实际环境中使用。

为什么使用Python

语法清晰;易于操作纯文本文件;使用广泛。

夫宠而不骄,骄而能降,降而不憾,憾而能眕者,鲜矣。——————《古文观止 · 石碏谏宠州吁 》《左传 · 隐公三年》

起因

最开始接触springboot是因为公司开始接触微服务架构,而使用最普遍的项目是SpringCloud,SpringCloud中的每个微服务都是基于springboot来开发的,也因此接触到springboot。

不过后来因为种种原因,微服务技术路线没有完全执行起来,springboot的接触也仅仅停留在表面。

最近,因为学习vue前端,接触到element-ui,又接触到一个前后台整体框架若依系统ruoyi,其使用的框架也是springboot,因此也重新燃起学习springboot的热情。

今天我们来看看springboot中如何做单元测试。

Junit各个版本区别

我们知道Java有一个非常遍历的单元测试中间件junit,目前版本已经到了junit5了,springboot目前最新版中使用的是Junit Jupiter。

junit4, junit5 jupiter之间的关系:

Junit 5 包括 Junit Platform, Junit Jupiter, Junit Vintage三部分。

Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,也可以接入其他测试引擎。

Junit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。

Junit Vintage是为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x, Junit3.x的测试引擎。

Junit4以及Junit5的区别如下图所示。注:该图原文地址

image-20220315150459244

springboot中一个基础的接口测试样例

首先我们创建springboot以及一个基础的接口。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class TestController {

@Autowired
MyService myService;

@GetMapping("test")
public String sayHello() {
return myService.show();
}
}


@Component
public class MyService {
public String show() {
return "我在MyService中";
}
}

我们在测试类中编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ExtendWith(SpringExtension.class)
@WebMvcTest(TestController.class)
class TestControllerTest {

@MockBean
MyService myService;

@Autowired
MockMvc mockMvc;

@Test
public void sayHello() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/test").accept(MediaType.ALL_VALUE))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
}

}

需要注意的是,在测试类中,同样需要使用@Atuowired引入相关的服务对象,以及mock模拟对象。

在springboot项目中,有一个非常常用的注解:@Autowired,它表示什么含义内,又是如何使用的呢?我们来小小的研究一下。

autowire,英文含义为”自动装配”,在 SpringBoot中,自动装配指的是将 Spring 容器中的 bean 自动的和我们需要这个 bean 的类组装在一起。

使用实例

首先我们创建基础的springboot项目,并创建一个接口/test以及一个组件类MyService,并在接口Controller层引入MyService,如下代码所示:

1
2
3
4
5
6
7
8
9
import org.springframework.stereotype.Component;

@Component
public class MyService {

public String show() {
return "我在MyService中";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.springcloud.security.demo.service.MyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

@Autowired
MyService myService;

@GetMapping("test")
public String sayHello() {
return myService.show();
}
}

如上代码所示,我们使用@Autowired注解,初次之外,并没有编写其他任何代码来实例化MyService,我们运行程序,并请求接口来看看效果:

image-20220314184505646

如上图,可以看到程序运行正常,并且MyService中的内容被成功的打印了出来。那么该MyService对象是如何实例化的呢?

这就引入了一个较大的概念:依赖注入

关于依赖注入,某乎上有一个非常有意思的例子,大家可以看一下,有助于理解依赖注入要解决的是什么问题,大体策略是什么。

降低耦合,解决程序难以维护的问题。

通常的方式有:构造函数,属性或者工厂模式。

下面跟着代码介绍几种依赖注入的实现方式:构造方法注入,setter方法注入,接口注入。

构造方法注入

image-20220315085624873

setter方法注入

image-20220315085726037

接口注入

image-20220315085830657

springboot中@Autowired使用方法

上述方法使用时,在idea中会有一个黄色波浪线提示:

image-20220315091313729

那么,什么才是推荐的注入方式呢?

springboot中推荐使用构造函数,setter方式来设置。

如下所示:

image-20220315091457268

image-20220315091919837

缓存的本质

缓存的本质是解决数据两端的处理速度差异问题,从而尽量避免处理速度快的等着处理速度慢的。

例如,我们都知道CPU的处理速度比内存的存储速度快,这种速度差异巨大,为了避免CPU在从内存读取数据时等待空转,因此才有CPU内部的三级缓存L1, L2, L3,从而避免内存成为CPU的性能瓶颈。

另外,本文所阐述的缓存只针对后台服务,针对web前端的浏览器级别,CDN,或者手机端本地缓存不做过多叙述。

通常服务中的几层缓存

在Web服务中,数据大多数永久的存储在mysql等关系型数据库中,客户端通过web或者移动端,通过网络请求Web后台服务器,后台服务器请求数据库,并通过一定程序逻辑将数据通过网络返回给客户终端。这一系列操作,数据的流向大致是如下图这样:

image-20220314175232702

那么,在这个过程中,那个过程最缓慢呢?即哪个过程最可能是系统瓶颈呢?

答案就是:数据库接受到数据请求后查询数据,并返回数据的过程。

因此,我们需要针对这一系统瓶颈提出解决方案,那便是缓存。

Redis/memcache

最常见的,也是使用最普遍的缓存中间件就是Redis、memcache。以Redis为例,通过key-value的存储结构,其value可以存储字符串,列表,集合等内容。例如,下图是我们使用redis管理工具Another Redis DeskTop Manager查看数据库的情况:

image-20220314180501402

image-20220314180509279

Redis通常是部署在一个单独的内存较大的服务器上,通过在内存中存储数据从而加速数据的查找与读取。

而且,目前Redis与Memcache已支持做分布式部署,或者部署为缓存集群,功能强大。

Caffine/Guava Cache

Caffine/Guava Cache是本地内存缓存中间件,换句话说,他们与应用程序是紧密结合的。其通过将本地程序的部分数据放在内存中,从而减少外部数据请求。

我们画一幅图来理解上述的两层缓存:

image-20220314181331621

一个数据请求流程,大致是先访问Caffine存储,若为命中,则访问Redis缓存,仍然未命中,则访问数据库。

多行不义必自毙。

————————《左传 · 郑伯克段于鄢》

问题起源

最近工作中需要和数个第三方进行服务对接,都涉及到同一个问题,如何进行服务安全认证,并进一步考虑到当前流行的多服务模式下,如何在多个服务间,即分布式服务间进行分布式的服务安全认证,或身份认证与操作授权。

第三方常用的身份认证与授权方式

就目前所对接的第三方来说,大多数的身份认证为access_token认证,即通过约定的key+secret以及指定的加密方式,通过指定接口获取一个特定的token以及token的有效期或者有效截止时间,后续业务接口访问时,每次均需要带上该token即可。

这种方式目前的专业术语叫OAuth 2.0。

推荐阅读阮一峰大佬的博客:OAuth 2.0 的四种方式OAuth 的核心就是向第三方应用颁发令牌。在文章里面提到了4中身份认证方式,分别是授权码,隐藏式,密码式,客户端凭证。

例如,我们上文提到的与第三方交互的token方式就对应阮一峰博客中的凭证式的访问方式。

如下图是我绘制的一个简单的认证请求方式,以便于读者理解。

image-20220314161456380

多服务模式下的身份认证与授权

在目前的情况下,多服务通常指的是微服务,而微服务又以SpringCloud为代表,其使用广泛,组件全面。

SpringCloud中,负责身份认证与授权的有Spring Cloud Security,Netflix Zuul等。

我们今天来看一下SpringCloud Security的基础使用。

首先按照创建springboot的流程,创建SpringCloud Security项目。创建完成后,需要在pom文件中引入以下内容,以便服务启动。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

然后修改application.properties配置文件,设置端口号:

1
server.port=8083

并创建一个测试的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.springcloud.security.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

@GetMapping("test")
public String sayHello() {
return "test hello world";
}
}

然后,我们启动项目:

image-20220314171013328

在浏览器中输入地址:

1
localhost:8083/test

发现,浏览器自动跳转到地址localhost:8083/login,其页面如下:

image-20220314171123503

我们接着配置项目。在配置文件中输入以下内容:

1
2
3
# Security
spring.security.user.name=admin
spring.security.user.password=123456

在浏览器输入框中输入对应内容,重试,点击”Sign in”按钮,页面自动跳转到了地址:localhost:8083/test,如下图所示:

image-20220314171640322

扩容的含义

该扩容针对的是服务,通常情况下,随着业务的发展,用户数量的增加,原本的服务,受限于硬件基础有限,无法满足增加的用户请求,或者受限与软件能力,需要重新编程或修改数据结构,以便扩大服务的支撑能力。

例如,原本的单机应用受限于单机的网络带宽,磁盘IO,所能够支撑的用户量是有限的,当响应速度慢到一定程度时,就需要对服务进行扩容,扩大带宽也好,增加磁盘IO也好。又比如,原本的用户数据使用mysql单表存储,当用户量达到一定程度,单表读取

扩容的难点

扩容的难点在于扩容的便捷性与服务能否宕机与宕机时间,能否做到很方便的从服务支撑100用户扩容到服务支持1000用户甚至1万用户,扩容的成本,扩容操作所耗费的整体时间,以及扩容时是否可以保证服务的可用性。

常见的服务扩容方法

nginx服务分发

nginx是一个高性能的HTTP web服务器,同时也是一个反向代理服务器,通过配置nginx可以实现将API请求按照指定规则分发到多态服务器上,我们可以通过nginx的配置文件来很简单的实现单机项目多服务器部署。

1
2
3
4
upstream my_server {
server ip1 weight=12;
server ip2 weight=15;
}

微服务化

另外,目前比较火热的开发方式,微服务,天然的支持服务多部署,通过服务注册中心注册服务,可以简单的实现将服务分发到指定的服务上,这方面的资料可以检索SpringCloud,重点关注其中的服务发现Eureka,Zookeeper,通过注册同一个服务,可以实现服务分发。

上云服务

换句话说,将扩容的任务交付出去,购买弹性云计算服务器,弹性数据库服务等等。例如数据库成为瓶颈了就扩容云数据库,当然这是针对不差钱的企业来说的,对技术人员也没什么要求。但是我相信这会是未来的普遍趋势。

分库分表

针对数据库扩容来说,如果是个人服务,而又收到资金等等限制,当数据库达到瓶颈时,需要考虑做分库分表来拆分数据库压力。

1

在前几篇文章

如何保证线程按照指定次序执行-Thread.join

如何保证线程按照指定次序执行-newSingleThreadExecutor线程池

如何保证线程按照指定次序执行-Join函数的另一种用法》中,

我们阐述了如何使用Thread.join()函数,单容量的线程池来保证多个线程的执行顺序,今天我们来尝试用另一种方式,通过CountDownLatch来实现。

CountDownLatch的官方文档如下所述:

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。

其中,主要的两个方法是awaitcountDown

await()
导致当前线程等到锁存器计数到零

countDown()
减少锁存器的计数,如果计数达到零,释放所有等待的线程。

即按照CountDownLatch的思路来实现线程顺序执行,需要阐述为:

在C线程之前前,要先执行B线程;在B线程执行前,先执行A线程。

C线程中调用await方法等待B线程执行完毕,B线程执行完毕后调用countDown从而唤起C线程继续执行。

同样的,B线程中调用await方法等待A线程执行完毕,A线程执行完毕后调用countDown从而唤起B线程继续执行。

实现代码如下所示:需要使用两个CountDownLatch对象,分别来控制A线程先于B线程执行,以及B线程先于C线程执行。

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 com.qiweiwei.thread;

import java.util.concurrent.CountDownLatch;

public class ThreadCountDownLatchDemo {

public static void main(String[] args) throws InterruptedException {
final CountDownLatch latchA = new CountDownLatch(1);
final CountDownLatch latchB = new CountDownLatch(1);

Thread A = new Thread(() -> {
System.out.println("A线程执行");
latchA.countDown();
});
Thread B = new Thread(() -> {
try {
System.out.println("等待A线程执行...");
latchA.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B线程执行");
latchB.countDown();
});
Thread C = new Thread(() -> {
try {
System.out.println("等待B线程执行...");
latchB.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C线程执行");
});

C.start();
B.start();
A.start();
}
}

执行结果如下所示:

等待B线程执行…
等待A线程执行…
A线程执行
B线程执行
C线程执行

注意,通过try-catch来确保子线程中的countDown得到执行,从而保证后续线程可以被成功唤醒。

1

在文章《如何保证线程按照指定次序执行-Thread.join》中,我们介绍了如何在主线程中使用Thread.join函数来保证线程的执行顺序,按照join函数的定义:

Waits for this thread to die.

An invocation of this method behaves in exactly the same way as the invocation

等待这个线程死亡。

调用此方法的行为方式与调用完全相同。

其可以让调用它的线程等待被调用线程执行完毕后才接着执行,因此,若我们有需要按照”A-B-C”的顺序来执行,则需要B中调用A.join(),C中调用B.join(),如此则可实现A先于B执行,而B先于C执行。我们来看看实现代码并测试一下。

正确的join调用方式

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
41
42
43
44
45
46
47
48
public class ThreadJoinTest2 {

public static void main(String[] args) throws InterruptedException {


Thread A = new Thread(() -> {
System.out.println("A线程执行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread B = new Thread(() -> {
try {
A.start();
A.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B线程执行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread C = new Thread(() -> {
try {
B.start();
B.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C线程执行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});

C.start();
C.join();

}
}

并且,根据join方法的源代码,join方法不会调用其线程,只是根据线程的执行状态判断是否继续wait,即是否继续阻塞调用进程的后续执行。因此join方法总是需要跟随start方法来执行。因此,一种错误的调用方式如下。

错误的join调用方式

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
41
42
43
44
45
46
47
48
49
50
/**
* 注意,该调用方式是错误的保证线程执行顺序的方式
*/
public class ThreadJoinTest2 {

public static void main(String[] args) throws InterruptedException {


Thread A = new Thread(() -> {
System.out.println("A线程执行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread B = new Thread(() -> {
try {
A.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B线程执行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread C = new Thread(() -> {
try {
B.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C线程执行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});

C.start();
Thread.sleep(5000);
B.start();
A.start();
}
}

这段代码的错误在于将join与start方法拆分开来,无法保证线程start之后可以阻塞其他线程的执行。例如上述代码在C线程执行后,主线程等待5秒钟,这5秒钟内B线程没有执行,因此B.join()无法实现执行B线程后再执行C线程。

多线程的问题测试始终是一个问题,一次运行成功不代表总是成功,一百次运行成功也不代表不存在错误的可能性,不知道是否存在良好的判断方式。

所以,笔者的判断也并不都是准确的,欢迎读者指正其中错误之处。

关于Thread类多说几句,Thread类成功的实现了将线程这一动态的抽象概念实现为面向对象中的一个可以操作中的类,实际上是有很多值得学习的东西的。

在上一篇文章《如何保证线程按照指定次序执行-Thread.join》中,介绍了如何通过Thread.join()函数实现多个线程按照指定次序执行,今天我们来看另一种方式,通过线程池的方式来实现。

首先来看基础的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadPoolTest {

public static void main(String[] args) {

Thread A = new Thread(() -> System.out.println("A"));
Thread B = new Thread(() -> System.out.println("B"));
Thread C = new Thread(() -> System.out.println("C"));

A.start();
B.start();
C.start();

}
}

上述代码无法保证程序的执行顺序,我们引入线程池。使用newSingleThreadExecutor方法。

newSingleThreadExecutor的官方释义如下:

1
public static ExecutorService newSingleThreadExecutor()

创建一个使用从无界队列运行的单个工作线程的执行程序。 (请注意,如果这个单个线程由于在关闭之前的执行过程中发生故障而终止,则如果需要执行后续任务,则新的线程将占用它。)任务保证顺序执行,并且不超过一个任务将被激活在任何给定的时间。 与其他等效的newFixedThreadPool(1) 、newFixedThreadPool(1) ,返回的执行器保证不被重新配置以使用额外的线程。

我们来看看实际编程运行的效果。

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadPoolTest {

public static void main(String[] args) throws InterruptedException {

Thread A = new Thread(() -> System.out.println("A"));
Thread B = new Thread(() -> System.out.println("B"));
Thread C = new Thread(() -> System.out.println("C"));

ExecutorService pool = Executors.newSingleThreadExecutor();
pool.submit(A);
pool.submit(B);
pool.submit(C);
pool.shutdown();
}
}

运行结果:

image-20220309182901611

根据文档,我们可以知道,还可以用newFixedThreadPool(1) 或者newFixedThreadPool(1) ,结果是一样的。

另外,线程池我们最常用的一个函数是execute(Runnable command),在本例测试用,发现使用execute时,也能达到同样的效果,那么submit与execute有什么区别呢?

execute是接口Executor的方法,其含义为:

1
void execute(Runnable command)

在将来的某个时间执行给定的命令。 该命令可以在一个新线程,一个合并的线程中或在调用线程中执行,由Executor实现。

submit是接口ExecutorService的方法,其含义为:

1
Future<?> submit(Runnable task)

提交一个可运行的任务执行,并返回一个表示该任务的未来。 未来的get方法将返回null 成功完成时。

通过newSingleThreadExecutor的队列模式,从而保证了任务按照提交的顺序来执行。

1

现在的编程语言大多数是支持并发编程,多线程的,当实现多线程时,如何保证各个线程的执行次序呢?

比如现在有线程A,线程B,线程C,如何保证线程按照A->B->C的顺序执行呢?

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadJoinTest {

public static void main(String[] args) {

Thread A = new Thread(() -> System.out.println("A"));
Thread B = new Thread(() -> System.out.println("B"));
Thread C = new Thread(() -> System.out.println("C"));

A.start();
B.start();
C.start();

}
}

通过测试,此时,执行结果是不一定的,A,B,C的次序是随机的,如下图两次执行结果就分别是B-C-A,C-B-A。

image-20220308183525031

image-20220308183555076

那么,如何保证线程按照指定的次序A-B-C来执行呢?

Thread.join函数

通过调用Thread.join函数我们可以实现按照指定次序执行。我们先来看一下Thread.join函数的解释:

Waits for this thread to die.

An invocation of this method behaves in exactly the same way as the invocation

等待这个线程死亡。

调用此方法的行为方式与调用完全相同。

解释有些看不懂。我们可以这样理解,线程调用join时,会造成主线程等待调用线程死亡后才接着执行后续代码。

我们来看看修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadJoinTest {
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(() -> System.out.println("A"));
Thread B = new Thread(() -> System.out.println("B"));
Thread C = new Thread(() -> System.out.println("C"));

A.start();
A.join();
B.start();
B.join();
C.start();
C.join();

}
}

执行结果:

image-20220308185102613

而且,不管该程序执行多少次,执行结果总是一定的,一定是按照次序A-B-C来执行。

我们来看一下join函数做了什么?

1
2
3
public final void join() throws InterruptedException {
join(0);
}

join()函数调用实际是调用join(0),我们继续看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

调用join函数时,millis=0,即我们实际执行的重要代码主要是下面这一部分:

1
2
3
while (isAlive()) {
wait(0);
}

其中isAlive()方法判断线程是否仍然在执行而没有die,如果仍然在执行,则主线程执行wait,直到线程结束才继续执行后续代码。

除了Join函数外,是否有其他方式保证执行顺序呢?我们之后介绍