0%

Chapter 10 covers web application security constraints for restricting access to certain contents. You will learn entities related to security such as principals, roles, login config, authenticators, etc. You will also write two applications that install an authenticator valve in the StandardContext object and uses basic authentication to authenticate users.

主体和之前的一样,新加的内容是安全相关的东西,更具体来说,是授权相关。可以根据配置的账户密码信息限制用户访问。这个功能现在应该挺鸡肋了,因为一般的 App 都是将这部分功能坐在内部的 login service 中的,哪里会通过这种方式作授权啊,除非买现成的但是不提供授权服务,这也太蠢了吧。。。

问题:Bootstrap2 中貌似做了一次授权之后,会将信息 cache 起来,看看它是存在哪里的

Overview

有些网站服务需要有访问限制,Tomcat 可以通过配置文件达到这种效果,访问页面时只有输入正确的用户名密码之后才能访问。

Tomcat 有一个 authenticator valve 可以用来做授权,他在系统启动后加入 context 的 pipeline 中,他会在 wrapper valve 之前被调用,做用户验证。

Realm

Tomcat 中 realm 模块可以做用户验证。一个 context 只能有一个 realm 服务,我们可以通过 context 的 setRealm() 方法设置它。

Realm 中用户信息存放的地址由配置决定,默认情况下,Tomcat 会拿 conf/tomcat-users.xml 中的用户信息做比对。当然我们也可以配置其他数据源,比如 DB。

Catalina 中使用 org.apache.catalina.Realm 这个接口表示这个概念,核心就那四个授权方法

1
2
3
4
5
6
7
8
9
public interface Realm {
public Principal authenticate(String username, String credentials);
public Principal authenticate(String username, byte[] credentials);
public Principal authenticate(String username, String digest,
String nonce, String nc, String cnonce,
String qop, String realm,
String md5a2);
public Principal authenticate(X509Certificate certs[]);
}

同时这个接口还包含 public boolean hasRole(Principal principal, String role); 方法。这个接口有一个抽象实现 org.apache.catalina.realm.RealmBase 还有几个具体实现都在同一个包下:JDBCRealm, JNDIRealm, MemoryRealm, and UserDatabaseRealm。默认使用的是 MemoryRealm,当 server 启动时,他会读取 tomcat-users.xml。

GenericPrincipal

java.security.Principal 代表 Principal 这个概念,具体实现为 org.apache.catalina.realm.GenericPrincipal。GenericPrincipal 必须关联一个 realm, 构造函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public GenericPrincipal(Realm realm, String name, String password) {
this(realm, name, password, null);
}

public GenericPrincipal(Realm realm, String name, String password, List roles) {
super();
this.realm = realm;
this.name = name;
this.password = password;
if (roles != null) {
this.roles = new String[roles.size()];
this.roles = (String[]) roles.toArray(this.roles);
if (this.roles.length > 0)
Arrays.sort(this.roles);
}
}

Principal 中也包含 hasRole() 方法,你可以传入 * 作为参数检测是否包含任意 role 的意思。

LoginConfig

login config 包含 realm name 由 org.apache.catalina.deploy.LoginConfig 这个 final class 表示. LoginConfig 包含 realm 和 authentication 的信息,auth name 必须是 BASIC, DIGEST, FORM, or CLIENT-CERT。

当服务器启动的时候,Tomcat 会读取 web.xml 信息,如果 xml 包含 login-config 元素,tomcat 就会创建一个 LoginConfig 对象并为他设置属性。authentication valve 会调用 LoginConfig 的 getRealmName() 方法并传送给浏览器的登陆界面。

Authenticator

org.apache.catalina.Authenticator 是 authenticator 的表现类,他没有任何方法,只是一个壳子。有一个抽象的实现类 org.apache.catalina.authenticator.AuthenticatorBase,它还集策划给你了 ValveBase 表明它是一个 valve。具体的实现类由 BasicAuthenticator, FormAuthenticator, DigestAuthentication 和 SSLAuthenticator。如果没有具体指明 authentication 类型,则会默认使用 NonLoginAuthenticator。它表示只检测安全限制而不需要授权。

Installing the Authenticator Valve

login-config element 在 deployment 文件中只能出现一次,其中包含有 auth-method 元素。这意味着 context 中只能有一个 LoginConfig 实例并且只能有一个 authentication class 实现。

Type Impl
BASIC BasicAuthenticator
FORM FormAuthenticator
DIGEST DigestAuthenticator
CLIENT-CERT SSLAuthenticator

如果 auth-method 没有设置,就表示使用的是 NonLoginAuthenticator。org.apache.catalina.startup.ContextConfig 是 Context 的配置类,包含 authentication 信息。下面的例子中,我们使用 SimpleContextConfig 动态加载 BasicAuthenticator 作为 StandardContext 的配置项。

The Applications

Bootsrap1

第一个例子,没有使用配置文件,而是直接在 Bootstrap 类中做了设置。新建了一个 SimpleContextConfig,它是一个 Listener,添加到 context 的监听器列表中,当 context start 时,接受到 event 并配置 context

1
2
3
4
5
// Bootstrap1 中代码如下
LifecycleListener listener = new SimpleContextConfig();
((Lifecycle) context).addLifecycleListener(listener);
//...
((Lifecycle) context).start();

实现如下, context start 后,event 触发,在 listener 中拿到对应的 context,并进行 auth 相关的设置,内容包括

  • 设置 login config
  • 设置 authenticator 到 context 的 pipeline
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
51
52
53
54
55
56
public class SimpleContextConfig implements LifecycleListener {

private Context context;

public void lifecycleEvent(LifecycleEvent event) {
if (Lifecycle.START_EVENT.equals(event.getType())) {
context = (Context) event.getLifecycle();
authenticatorConfig();
context.setConfigured(true);
}
}

private synchronized void authenticatorConfig() {
// Does this Context require an Authenticator?
SecurityConstraint constraints[] = context.findConstraints();
if ((constraints == null) || (constraints.length == 0))
return;
LoginConfig loginConfig = context.getLoginConfig();
if (loginConfig == null) {
loginConfig = new LoginConfig("NONE", null, null, null);
context.setLoginConfig(loginConfig);
}

// Has an authenticator been configured already?
Pipeline pipeline = ((StandardContext) context).getPipeline();
if (pipeline != null) {
Valve basic = pipeline.getBasic();
if ((basic != null) && (basic instanceof Authenticator))
return;
Valve valves[] = pipeline.getValves();
for (int i = 0; i < valves.length; i++) {
if (valves[i] instanceof Authenticator)
return;
}
} else { // no Pipeline, cannot install authenticator valve
return;
}

// Has a Realm been configured for us to authenticate against?
if (context.getRealm() == null) {
return;
}

// Identify the class name of the Valve we should configure
String authenticatorName = "org.apache.catalina.authenticator.BasicAuthenticator";
// Instantiate and install an Authenticator of the requested class
Valve authenticator = null;
try {
Class authenticatorClass = Class.forName(authenticatorName);
authenticator = (Valve) authenticatorClass.newInstance();
((StandardContext) context).addValve(authenticator);
System.out.println("Added authenticator valve to Context");
} catch (Throwable t) {
}
}
}

接着在 Bootstrap1 中设置 security constraint 相关的配置. 这里指定了 constraint 中只有 role 是 manager 的可以访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// add constraint
SecurityCollection securityCollection = new SecurityCollection();
securityCollection.addPattern("/");
securityCollection.addMethod("GET");

SecurityConstraint constraint = new SecurityConstraint();
constraint.addCollection(securityCollection);
constraint.addAuthRole("manager");
LoginConfig loginConfig = new LoginConfig();
loginConfig.setRealmName("Simple Realm");
// add realm
Realm realm = new SimpleRealm();

context.setRealm(realm);
context.addConstraint(constraint);
context.setLoginConfig(loginConfig);

SimpleRealm 实现如下, 它其实就是模拟了一个内存中的 DB,当开启 security constrain 后,通过 GET 访问页面就会跳出验证弹窗。输入账户信息,就会调用到下面 authenticate() 方法中做判断了

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public class SimpleRealm implements Realm {

public SimpleRealm() {
createUserDatabase();
}

private Container container;
private ArrayList users = new ArrayList();

public Container getContainer() {
return container;
}

public void setContainer(Container container) {
this.container = container;
}

public String getInfo() {
return "A simple Realm implementation";
}

public void addPropertyChangeListener(PropertyChangeListener listener) {
}

public Principal authenticate(String username, String credentials) {
System.out.println("SimpleRealm.authenticate()");
if (username == null || credentials == null)
return null;
User user = getUser(username, credentials);
if (user == null)
return null;
return new GenericPrincipal(this, user.username, user.password, user.getRoles());
}

public Principal authenticate(String username, byte[] credentials) {
return null;
}

public Principal authenticate(String username, String digest, String nonce,
String nc, String cnonce, String qop, String realm, String md5a2) {
return null;
}

public Principal authenticate(X509Certificate certs[]) {
return null;
}

public boolean hasRole(Principal principal, String role) {
if ((principal == null) || (role == null) ||
!(principal instanceof GenericPrincipal))
return (false);
GenericPrincipal gp = (GenericPrincipal) principal;
if (!(gp.getRealm() == this))
return (false);
boolean result = gp.hasRole(role);
return result;
}

public void removePropertyChangeListener(PropertyChangeListener listener) {
}

private User getUser(String username, String password) {
Iterator iterator = users.iterator();
while (iterator.hasNext()) {
User user = (User) iterator.next();
if (user.username.equals(username) && user.password.equals(password))
return user;
}
return null;
}

private void createUserDatabase() {
User user1 = new User("ken", "blackcomb");
user1.addRole("manager");
user1.addRole("programmer");
User user2 = new User("cindy", "bamboo");
user2.addRole("programmer");

users.add(user1);
users.add(user2);
}

class User {

public User(String username, String password) {
this.username = username;
this.password = password;
}

public String username;
public ArrayList roles = new ArrayList();
public String password;

public void addRole(String role) {
roles.add(role);
}

public ArrayList getRoles() {
return roles;
}
}

}

启动服务器,当我们用 role 是 manager 的 user 去访问,页面显示正常,当我们用 programer 去访问,页面不显示

流程大致描述如下

  1. 启动 server 加载配置
  2. 访问页面
  3. context 调用 pipeline
  4. pipeline 调用 valve
  5. 调用 BasicAuthenticator 的 auth 方法验证 - 在 listener 中指定

Bootstrap2

第二个例子和第一个例子很想,唯一区别就是将 Realm 的配置指定到了 tomcat-users.xml 文件

1
2
3
// add realm
Realm realm = new SimpleUserDatabaseRealm();
((SimpleUserDatabaseRealm) realm).createDatabase("conf/tomcat-users.xml");

SimpleUserDatabaseRealm 实现如下, 里面一个比较有意思的点是 MemoryUserDatabase 这个类,它会默认加载 conf/tomcat-users.xml 的内容,解析出来,格式是 hard code 的,挺有意思。逻辑和之前的基本一样,没什么新鲜的。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class SimpleUserDatabaseRealm extends RealmBase {

protected UserDatabase database = null;
protected static final String name = "SimpleUserDatabaseRealm";

protected String resourceName = "UserDatabase";

public Principal authenticate(String username, String credentials) {
// Does a user with this username exist?
User user = database.findUser(username);
if (user == null) {
return (null);
}

// Do the credentials specified by the user match?
// FIXME - Update all realms to support encoded passwords
boolean validated = false;
if (hasMessageDigest()) {
// Hex hashes should be compared case-insensitive
validated = (digest(credentials).equalsIgnoreCase(user.getPassword()));
}
else {
validated = (digest(credentials).equals(user.getPassword()));
}
if (!validated) {
return null;
}

ArrayList combined = new ArrayList();
Iterator roles = user.getRoles();
while (roles.hasNext()) {
Role role = (Role) roles.next();
String rolename = role.getRolename();
if (!combined.contains(rolename)) {
combined.add(rolename);
}
}
Iterator groups = user.getGroups();
while (groups.hasNext()) {
Group group = (Group) groups.next();
roles = group.getRoles();
while (roles.hasNext()) {
Role role = (Role) roles.next();
String rolename = role.getRolename();
if (!combined.contains(rolename)) {
combined.add(rolename);
}
}
}
return (new GenericPrincipal(this, user.getUsername(),
user.getPassword(), combined));
}

// ------------------------------------------------------ Lifecycle Methods


/**
* Prepare for active use of the public methods of this Component.
*/
protected Principal getPrincipal(String username) {
return (null);
}

protected String getPassword(String username) {
return null;
}

protected String getName() {
return this.name;
}

public void createDatabase(String path) {
database = new MemoryUserDatabase(name);
((MemoryUserDatabase) database).setPathname(path);
try {
database.open();
}
catch (Exception e) {
}
}
}

Chapter 9 discusses the manager, the component that manages sessions in session management. It explains the various types of managers and how a manager can persist session objects into a store. At the end of the chapter, you will learn how to build an application that uses a StandardManager instance to run a servlet that uses session objects to store values.

PS: 本节实验失败了,页面显示不出来,追踪了一下,servlet 可以正常加载,但是执行 init() 方法的时候报错了。感觉可以先用之前的 javaweb 项目把这个页面显示出来,在看看问题。有可能是依赖有问题。

主要知识点

  • Session 相关的接口关系
  • 通过 Manager 管理 session
  • 实现了 Lifecycle 接口
  • 提供 swap out 功能,节省内存资源 - 长时间不用的 session 暂存
  • 持久化

没什么成就感,暂时先记怎么多把

代码编译的结果从本地机器码转为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

9.1 概述

在 Class 文件格式与执行引擎这部分里,用户的程序能直接参与的内容并不多,Class 以何种格式存储,类型和施加在,如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能操作的,主要是字节码生成与类加载两个部分的功能,但仅仅在如何处理这两点上,就已经出现了许多值得欣赏借鉴的思路,这些思路后来成为很多常用功能和程序实现的基础。

9.2 案例分析

四个案例,关于类加载和字节码各两个。

9.2.1 Tomcat: 正统的类加载架构

主流的 Java Web 服务器,如 Tomcat, Jetty 等都实现了自己定义的类加载器,而且还不止一个。因为一个功能健全的 Web 服务器,都要解决如下这些问题:

  • 部署在同一个服务器上的两个 web 应用程序所使用的 Java 类库可以实现互相隔离。两个不同应用可能依赖同一个第三方类库的不同版本,不能要求每个类库在一个服务器中只能有一份,服务器应该能够保证两个独立应用程序的类库可以互相独立使用。
  • 部署在同一个服务器上的两个 web 应用所使用的 Java 类库可以互相共享。与前一个相反,但很常见,如用户可能有 10 个使用 Spring 的应用部署在统一台服务器,如果把 10 份 Spring 分别存在应用的隔离目录,将会很大的浪费资源。磁盘空间是其次,主要是良妃内存,很容易造成方法去过度膨胀的风险。
  • 服务器需要尽可能保证自身的安全不受部署的 Web 应用程序的影响。一般来说,给予安全考虑,服务器所使用的类库应该与程序类库相互独立。
  • 只是 JSP 应用的 Web 服务器,十有八九都需要支持 HotSwap 功能。JSP 由于其纯文本特性,修改几率远大于第三方类库和自己的 Class 文件。ASP,PHP 和 JSP 这些网页应用也将修改后无需重启作为优势来看待,因此,主流 Web 服务器都会支持 JSP 生成类的热替换。

由于以上问题,不是 web 应用时,单独一个 ClassPath 就不能满足要求了,所以各种 web 服务器不约而同的提供了好几个不同还以的 ClassPath 路径供用户存放第三方类库。一般这些路径都以 lib 或 classes 命名。不同路径中的类库,具备不同的访问范围和服务对象,通常每个目录都会对应一个自定义类加载器去加载防止在里面的 Java 类库。下面以 Tomcat 为例,分析其规划。

Tomcat 目录结构中,有三组目录可以设置,一组默认,供4组。分别是

  • /common 目录,类库可被 Tomcat 和所有 Web 应用共同使用
  • /server 目录,类库可被 Tomcat 使用,对所有 Web 应用不可见
  • /shared 目录,类库可以被所有 Web 应用共同使用,对 Tomcat 不可见
  • /WebApp/WEB-INF 目录,仅被该 Web 应用使用,对 Tomcat 和其他应用不可见

实线节点是 JDK 自带加载起,虚线是 Tomcat 自建的加载器。Common类加载器,Catalina类加载器(也称为Server类加载器),Shared类加载器和Webapp类加载器则是 Tomcat 自定义的类加载器,分别加载 /common/, /server/, shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。WebApp 类加载器和 JSP 类加载器通常还会有多个实例,每个 Web 应用对应一个 WebApp 类加载器,每个 JSP 文件对应一个 JasperLoader 类加载器。

从图可以看出,Common 类加载器能加载的类都可以被 Catalina 类加载器和 Shared 类加载器使用,而 Catalina 类加载器和 Shared 类加载器自己能加载的类则与对方相互隔离。WebApp类加载器可以使用Shared类加载器加载到的类,但各个WebApp类加载器实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class文件,它存在的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的HotSwap功能。

上面讲的是 Tomcat6 之前的加载器架构,Tomcat6 之后对默认的目录结构做了简化,只有指定 tomcat/conf/catalina.properties 的 server.loader 和 share.loader 后才会真正建立 Catalina类加载器和Shared类加载器实例,否则用到的地方都会用 Common 类加载器实例代替,而默认的配置文件中没有设置这两项,所以 Tomcat6 之后顺理成章的把 /common, /server 和 /shared 三个目录合并在一起变成 /lib 目录,相当于之前的 /common 目录的作用,是 Tomcat 团队简化部署的一项改动。

9.2.2 OSGi:灵活的类加载器架构

没兴趣,用到再看

9.2.3 字节码生成技术与动态代理的实现

有兴趣,等 Tomcat 完结了再看,不过应该要先看完第八章的内容才行

7.1 概述

Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称作虚拟机的类加载机制。

7.2 类加载的时机

一个类从被加载到虚拟机内存中,到卸载为止,会经历七个步骤

  • 加载 Loading
  • 验证 Verification
  • 准备 Preparation
  • 解析 Resolution
  • 初始化 Initialization
  • 使用 Using
  • 写在 Unloading

验证,准备,解析三部分统称为 链接 Linking

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,而解析不一定,有些情况下可以在初始化后再开始,为了支持动态绑定

有且只有六种情况必须立即对类进行初始化

  1. 遇到 new, getstatic, putstatic 或 invokestatic 这四条字节码指令时,如果类型没有进行过初始化,则需要先出发其初始化阶段。能够生产这四条指令的典型 Java 代码场景有:
    • 使用 new 关键字实例化对象的时候
    • 读取或设置一个类型的静态字段(被 final 修饰,已在编译期把结果放入常量池的静态字段除外)
    • 调用一个类型的静态方法的时候
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  6. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

除此之外,所有引用类型的方式都不会出发初始化,称为被动引用。下面是三个被动引用的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 通过子类引用父类的静态字段,不会导致子类初始化
public class SuperClass {
static {
System.out.println("SuperClass init!");
}

public static int value = 123;
}

public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}

public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}

// SuperClass init!
// 123

只会输出 “SuperClass init!”,而不会输出 “SubClass init!”

1
2
3
4
5
6
7
// 通过数组定义来引用类,不会出发此类的初始化

public class NotInitialization02 {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}

这种情况并不会触发类的初始化,只会触发 “[Lclassloading.SuperClass” 的类初始化节点,它是虚拟机自动生成的,创建动作由 newarray 触发,代表一维数组。

第三个例子,对应第一条中的第二点,final 修饰的常量编译阶段通过常量传播优化,将值存入 NotInitialization03 类的常量池中,后面对 ConstClass.HELLOWORLD 的引用世纪都被转为 NotInitialization03 对自身常量池的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConstClass {
static {
System.out.println("ConstClass init!");
}

public static final String HELLOWORLD = "hello world";
}

public class NotInitialization03 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}

// hello world

7.3 类加载的过程

下面具体介绍 加载,验证,准备,解析和初始化这五个阶段所执行的具体动作。

7.3.1 加载

加载是 类加载 的第一个阶段,会做三件事

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 再内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

PS: 对数组类型的加载,这里还有一些描述,以后有用到再看

7.3.1 验证

验证是链接阶段第一步,目的是确保 Class 文件的字节流中包含的信息符合 Java 虚拟机规范 的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段非常重要,这个阶段是否严谨直接决定虚拟机能否承受恶意代码的攻击。从代码量和耗费的执行性能角度讲,验证阶段工作量再虚拟机类加载过程中占了相当大的比重。

验证阶段大致会完成下面四个阶段的检测动作:文件格式验证,元数据验证,字节码验证和符号引用验证

文件格式验证

第一阶段要验证字节流是否符合 Class 文件格式规范,验证点包括

  • 是否以魔数 0xCAFEBABE 开头
  • 主、次版本号是否在当前Java虚拟机接受范围之内
  • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息

以上只列举了一部分

元数据验证

第二阶段是对字节码描述的信息进行语义分析,保证其描述的信息符合规范

  • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等

字节码验证

第三阶段是这个那个验证过程中最复杂的一个阶段,通过数据流分析和控制流分析,确定程序予以是合法的,符合逻辑的。

符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在链接的第三个阶段-解析阶段发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。本阶段通常需要校验下列内容

7.3.2 准备

准备阶段是正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应该再方法区中进行分配,但方方法区本身就是一个逻辑上的区域。JDK7 之前 HotSpot 使用永久区,JDK8 之后类变量会和 Class 一起放在堆空间。这点再 4.3.1 验证过了。

准备阶段的赋值仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。其次,这里说的初始值指的是数据类型的零值,比如

1
public static int value = 123;

准备阶段后,初始值为 0 而不是 123,此时尚未执行任何 Java 方法,把 123 赋值给 value 的方法存放在类构造器 () 方法之中,所以赋值要到类初始化阶段才会被执行。Java 所有基本数据类型零值表如下

data type value data type value
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char ‘\u0000’ reference null
byte (byte)0

例外情况是前面展示过的 final 的情况,比如

1
public static final int value = 123;

编译时 Javac 将会为 value 生成 Constant Value 属性,在准备阶段虚拟机就会根据 Constant Value 的设置将value赋值为123。

7.3.4 解析

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程

  • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

PS: 中间穿插很多解析类型的解释,暂时用不到,跳过

7.3.5 初始化

类的初始化阶段是勒加载过程的最后一个步骤,之前介绍的几个动作,除了在加载阶段用户可以通过自定义类加载器的方式局部参与外,其余都由 Java 虚拟机来主导控制。直到初始化阶段,虚拟机才真正执行类中编写的 Java 程序代码,将主导权移交给应用程序。

初始化阶段就是执行类构造器 () 方法的过程,它是Javac编译器的自动生成物。

() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,顺序由源文件中顺序决定。静态语句块只能访问它之前的变量。之后的变量可以赋值,但是不能访问。

1
2
3
4
5
6
7
8
public class Test {
static {
i = 0;
System.out.println(i); // 编译器提示 非法向前引用
}

static int i = 1;
}

() 不需要显示调用父类构造器,虚拟机会保证在子类 () 执行前,父类的 () 已经执行完毕,所以 jvm 中第一个被执行的 () 肯定是 java.lang.Object.

父类 () 优先执行,即父类的静态语句块要优先于子类的变量赋值操作。下面例子中再子类使用变量之前,父类已经完成了静态块的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
static class Parent {
public static int A = 1;
static {
A = 2;
}
}

static class Sub extends Parent {
public static int B = A;
}

public static void main(String[] args) {
System.out.println(Sub.B);
}
}
// 2

() 方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 () 方法

接口仍有变量初始化赋值操作,也会生成 () 方法。但与类不同,执行接口的 () 不需要先执行弗雷接口的 ()。接口的实现类再初始化时也一样不会执行接口的 () 方法。

Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步。如果一个类的 () 方法有耗时很长的操作,很可能造成多个进程阻塞

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
public class Test22 {
static class DeadLoopClass {
static {
if (true) {
System.out.println(Thread.currentThread() + " init DeadLoopClass");
while(true) {}
}
}
}

public static void main(String[] args) {
Runnable scritp = () -> {
System.out.println(Thread.currentThread() + " start");
new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
};

Thread thread1 = new Thread(scritp);
Thread thread2 = new Thread(scritp);
thread1.start();
thread2.start();
}
}
// Thread[Thread-0,5,main] start
// Thread[Thread-1,5,main] start
// Thread[Thread-0,5,main] init DeadLoopClass

7.4 类加载器

虚拟机设计团队有意把类加载阶段中 “通过一个类的全限定名来获取描述该类的二进制字节流” 的这个动作放到 Java 虚拟机外部去实现,以便让程序自己决定如何获取所需的类。实现这个动作的代码被称作-类加载器(Class Loader)。

这项技术原来是为了支持 Java Applet 而设计的,如今,Applet 已经淘汰了,但是类加载器却在 类层次划分,OSGi, 如部署,代码加密等领域大放异彩。

7.4.1 类与类加载器

类加载器虽然只用于实现类的加载动作,但他在 Java 程序中起到的作用却远超类加载阶段。任意类,必须由加载他的类加载器和这个类本身共同确立其在 jvm 中的唯一性,俄米格类加载器都拥有一个独立的类名称空间。

通俗讲:比较两个类是否相等,只有在两个类由同一个类加载器加载的前提下才有意义,否则,这两个类逼不想等。

这里所指的“相等”,包括代表类的Class对象的equals()方法,isAssignableFrom()方法、isInstance() 方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况

下面的例子中,我们自定义了一个 class loader 并加载当前测试类,生成实例。拿这个实例和默认类加载器的测试类进行 instanceof 的比较,结果为 false。

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
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.indexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};

Object obj = myLoader.loadClass("classloading.ClassLoaderTest").newInstance();

System.out.println(obj.getClass());
System.out.println(obj instanceof classloading.ClassLoaderTest);
}
}
// class classloading.ClassLoaderTest
// false

7.4.2 双亲委派模型

从虚拟机的角度看,只有两类加载器

  • 启动类加载器(Bootstrap Class Loader): C++ 实现,虚拟机的一部分
  • 其他加载器:Java 实现,独立与虚拟机外

开发人员角度看,可以分的更细致,可以分为三层类加载器

  • 启动类加载器:加载存放在 \lib 或者 -Xbootstrapclasspth 参数指定的路径下,能被 JVM 识别的类库到内存中。不能被 Java 程序直接使用,编写自定义加载器时,返回 null 即可将加载委托给启动类加载器了
  • 扩展类加载器(Extension Class Loader): 在类 sun.misc.Lanucher$ExtClassLoader 中,负责加载 \lib\ext 或者 java.ext.dirs 系统变量锁指定的路径中所有的类库。JDK9 之后,被模块化所替代。
  • 应用程序类加载器(Application Class Loader): 由 sun.misc.Lanucher$AppClassLoader 实现。ClassLoader 的 getSystemClassLoader() 方法的返回值。记载用户类路径上所有类库。如果应用程序没有自定义过自己的类加载器,一般这个就是默认的类加载器。

JDK9 之前的 Java 应用都是由这三类加载器互相配合来完成加载的。双亲委派模型要求,除了顶层的启动类加载器外,其余的类加载器都要有自己的父类加载器。这里的父子关系不是通过继承关系实现的,而是通过使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,首先不会尝试自己去加载这个类,而是委派给父类加载器去完成,每次皆是如此。只有当父类无法实现这个加载请求时,子类才是尝试自己去加载。

使用亲委派模型的好处是具备了一种带有优先级的层次关系。例如 java.lang.Object, 无论那哪个加载器加载它,最终都会使用 rt.jar 下的 Object 定义。如果不用这个模型,用户在 ClassPath 下定义一个 java.lang.Object 类, 系统中就会出现多个不同的 Object 类,Java 类型体系中最基础的行为就无法得到保证了。

下面是 ClassLoader 中加载类的代码实现。先尝试查这个类是否已经被加载。再看是否有父加载器,如果没有则使用启动类加载器加载。如果还是没有找到类,则尝试用自己加载。

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

7.4.3 破坏双亲委派模型

双亲委派模式是推荐方式,而不是强制性约束。直到 JDK9 的模块化为止,主要出现过 3 次较大规模的被破坏情况。

第一次是 JDK1.2 之前,双亲委派模型还没出现,但是类加载器的概念和抽象类已经引入了。

第二次被破坏是由于这个模型自身的缺陷导致的。这个模型很好的解决了各个类加载器协作时基础类型的一致性问题,但程序设计往往没有绝对不变的完美规则,如果基础类型又要调用回用户的代码,该如何。典型的例子便是 JNDI 服务。JDNI 现在已经是 Java 的标准服务,代码由启动类加载器来完成加载(JDK1.3 时加入到 rt.jar),肯定属于 Java 中很基础的类型了。JNDI 目的是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的 ClassPath 下的 JNDI 服务提供者接口(Service Provider Interface, SPI) 的代码,现在问题来了,启动类加载器是绝不可能认识,加载这些代码的。

为了解决这个困境,Java 设计团队引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader). 这个加载器可以通过 java.lang.Thread 的 setContextClassLoader() 方法进行设置,如果创建线程时未设置,将会从父类线程中继承一个,如果再应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。JNDI 使用这个线程上下文类加载器去加载所需的 SPI 服务代码,这个是一个类加载器去请求子类加载器完成类加载的行为,实际上打通了双亲委派模型的层次结构来逆向使用类加载器,Java 中涉及 SPI 的加载基本都采用这种方式,比如 JNDI,JDBC,JCE,JAXB 和 JBI 等。当 SPI 多于一个时,只能采用硬编码,为了消除这种不雅的实现,JDK6 提供了 java.util.ServiceLoader 配合 META-INF/services 中的配置信息,辅以责任链模式,才算给 SPI 提供了一种相对合理的解决方案。

第三次被破坏是由于用户对程序动态性的追求导致的。典型应用场景有:代码热部署,模块热部署等。即像电脑外设一般,在没有重新启动的情况下完成功能升级。现在比较热门的实现是 IBM 的 OSGi, Oracle 的 Jigsaw 。后面还有一些 OSGi 的介绍,但是没用过,看看就过了,用到再说。

7.5 Java 模块化系统

pass, 暂时用不到

The manay faces of concurrency

并发看上去很让人摸不着头脑,主要是应为我们需要解决多个问题,并且解决问题的方法很多。这两者间也没有明确的匹配关系。所以你必须要全面的理解各种问题和场景才能更高效的使用并发。

使用并发可以解决的问题可以粗略归纳为两类

Faster execution

多处理器系统通过并发可以提高效率,这很容易理解。但是有时但处理器系统通过并发也能提高效率,听上去可能有点反直觉,但是确实如此。一般来说,在但处理器系统中使用多线程,会有上下文切换(context switch)开销导致性能下降。但是如果场景中有较多的 IO 操作,则可能开销不增反降。

Improving code design

单核系统中实现多线程,本质上,一个时间点也只能做一件事,理论上,我们可以将这个多线程转化为单线程实现。但是有时多线程可以提供更好的组织方式,比如在模拟动画的场景上。

Java 中多线程是有优先级的。通过这个优先级,JVM 会分配不同的时间片给程序执行。

Basic threading

并发可以帮你讲你的程序分成独立的 task,每个 task 可以通过 processor 中的一个 thread 执行。每个 thread 可以线性的执行程序。通过这种方式单核 CUP 也能执行多线程,而且这对使用者是透明的,你不需要关心他的具体实现。

Defining tasks

并发中一个 thread 对应一个 task, 我们通过实现 Runnable 接口并实现 run() 方法的形式实现并发。

示例说明:

多线程打印变量指,run() 方法中有个 while 循环让 countDown 值递减,然后调用 print 方法打印状态信息。countDown = 0 时结束 task 并推出。

Thread.yield() 是 Thread 类自带的方法,作用是告诉 CPU 现在是时候让渡时间片了。

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
public class LiftOff implements Runnable {
protected int countDown = 10; // Default
private static int taskCount = 0;
private final int id = taskCount++;

public LiftOff() {
}

public LiftOff(int countDown) {
this.countDown = countDown;
}

public String status() {
return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "), ";
}

public void run() {
while (countDown-- > 0) {
System.out.print(status());
Thread.yield();
}
}
}

public class MainThread {
public static void main(String[] args) {
LiftOff launch = new LiftOff();
launch.run();
}
}

// #0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!),

除了调用 run() 方法,还可以将 Runnable 类传给 Thread 类并调用 start() 方法启动线程。下面的例子中,我们在主函数中新建五个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MoreBasicThreads {
public static void main(String[] args) {
for (int i = 0; i < 5; i++)
new Thread(new LiftOff()).start();
System.out.println("Waiting for LiftOff...");
}
}

// #0(9), #1(9), #2(9), #0(8), #2(8), #1(8), #2(7), #0(7), #2(6),
// #3(9), #2(5), #3(8), #4(9), #3(7), #0(6), #3(6), Waiting for LiftOff...
// #1(7), #3(5), #0(5), #4(8), #2(4), #4(7), #0(4), #4(6), #3(4),
// #1(6), #3(3), #1(5), #4(5), #1(4), #0(3), #2(3), #1(3), #4(4),
// #2(2), #4(3), #3(2), #4(2), #2(1), #1(2), #0(2), #2(Liftoff!),
// #0(1), #4(1), #0(Liftoff!), #3(1), #4(Liftoff!), #1(1),
// #3(Liftoff!), #1(Liftoff!),

从输出我们可以看出来,各个线程的 task 是混合执行的,通过 thread scheduler 调度。如果你使用的是多核系统,scheduler 会帮你讲这些 task 分配到不同核上计算。

main() 函数并不会持有创建出来的 Thread 的引用。对普通的对象来说,这会影响到垃圾回收,但是 Thread 有特殊的机制保证这一点。他会一直存在知道 run() 方法结束为止。

Using Executors

Java SE5 以来,提供了另一种执行并发的方式 - Executor. 通过它你就不需要在 Client 中新建 Thread 来执行这个 task 了。Executor 是 Java5/6 中提倡的运行并发的方式。

Executor 提供了多种运行方式,有 CachedThreadPool, FixedThreadPool 和 SingleThreadPool.

CachedThreadPool 的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CacheThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();
}
}

// #0(9), #1(9), #2(9), #0(8), #2(8), #1(8), #3(9), #2(7),
// #0(7), #2(6), #3(8), #2(5), #1(7), #3(7), #2(4), #4(9),
// #4(8), #0(6), #4(7), #2(3), #3(6), #1(6), #3(5), #1(5),
// #2(2), #0(5), #0(4), #4(6), #0(3), #2(1), #1(4), #3(4),
// #2(Liftoff!), #3(3), #0(2), #4(5), #0(1), #3(2), #1(3),
// #3(1), #0(Liftoff!), #3(Liftoff!), #4(4), #1(2), #4(3),
// #1(1), #4(2), #1(Liftoff!), #4(1), #4(Liftoff!),

task execute 之后需要调用 shutdown 方法防止新的 task 被提交到 Executor 中。

下面是 FixedThreadPool 的使用方法,和前面基本一样,我们可以自定义 pool size

1
2
3
4
5
6
7
8
9
public class FixedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();
}
}

相比于 CachedThreadPool, FixedThreadPool 会在开始前一并将制定的 Thread 都创建完以节省创建成本。同时由于指定了线程数,可以防止资源滥用。

书中例子都是用的 CachedTheadPool, 因为方便,他的机制是,更具使用情况创建线程,如果之前的线程用完了,会重用。产品代码还是尽量使用 FixedThreadPool 为好。

SingleThreadPool 和 FixedThreadPool 很像,只不过限定只能是单线程。这种情况在 常驻线程 和 临时线程 的情况下很有用。如果多个 task 被提交到 SingleThreadPool 中的话,他会顺序执行所有的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService exec = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();
}
}

// #0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2),
// #0(1), #0(Liftoff!), #1(9), #1(8), #1(7), #1(6), #1(5),
// #1(4), #1(3), #1(2), #1(1), #1(Liftoff!), #2(9), #2(8),
// #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(Liftoff!),
// #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1),
// #3(Liftoff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3),
// #4(2), #4(1), #4(Liftoff!),

通过 SingleThreadExectuor 你可以确保同一时间只有一个线程占用某个资源。如果读写文件系统时,可以通过这中 executor 避免死锁。当然最常见的还是给资源加锁,后面有介绍。

Producing return values from tasks

Runnable 的方式,当 run 结束时即退出,是没有返回值的,如果想要在 task 结束后返回值,可以使用 Callable 接口,从 Java 5 开始支持这个接口。他只能通过 ExecutorService 的 submit 进行调用。

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 class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList<Future<String>> results = new ArrayList<>();
for (int i = 0; i < 10; i++) {
results.add(exec.submit(new TaskWithResult(i)));
}
exec.shutdown();
for (Future<String> fs : results) {
System.out.println(fs.get());
}
}
}

// result of TaskWithResult 0
// result of TaskWithResult 1
// result of TaskWithResult 2
// result of TaskWithResult 3
// result of TaskWithResult 4
// result of TaskWithResult 5
// result of TaskWithResult 6
// result of TaskWithResult 7
// result of TaskWithResult 8
// result of TaskWithResult 9

submit() 方法会产生一个 Future 对象来存储 Callable 的结果。Future 提供 isDone() 方法用以检测 task 是否执行结束。结束后可以执行 get() 方法得到结果。如果直接调用 get() 但是 task 还没有 done, 那 get() 就会 block 直到 task 完成,你也可以为 get() 设置 timeout。

Sleeping

在 task 中,你可以通过调用 sleep() 方法来影响 task 的执行。

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
public class SleepingTask extends LiftOff {
@Override
public void run() {
try {
while (countDown-- > 0) {
System.out.println(status());
// Old-style: Thread.sleep(100);
// Java SE5/6-style
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new SleepingTask());
}
exec.shutdown();
}
}
// #0(9), #2(9), #1(9), #3(9), #4(9),
// #0(8), #2(8), #1(8), #4(8), #3(8),
// #4(7), #1(7), #3(7), #2(7), #0(7),
// #1(6), #4(6), #3(6), #0(6), #2(6),
// #1(5), #4(5), #3(5), #0(5), #2(5),
// #4(4), #0(4), #2(4), #3(4), #1(4),
// #4(3), #0(3), #1(3), #3(3), #2(3),
// #3(2), #2(2), #0(2), #4(2), #1(2),
// #2(1), #3(1), #4(1), #0(1), #1(1),
// #4(Liftoff!), #3(Liftoff!), #2(Liftoff!), #0(Liftoff!), #1(Liftoff!),

sleep() 会抛出 InterruptedException 异常,你必须在 run() 方法中处理他,因为异常是不能被传递到 main 中的。 TimeUnit 是对 Thread.sleep() 更精确的处理方式。

从输出内容我们可以看到,加了 sleep 之后 task 以轮训的形式输出,但是这个是不能保证的,不同的操作系统可能有不同的行为。

Priority

priority 表示 thread 在 scheduler 总的重要程度。scheduler 会倾向于更频繁的调用 priority 高的 thread。

一般来说,你不需要人为的制定 thread priority,系统会自动为你分配。你可以调用 getPriority()/setPriority() 查看,指定优先级

示例如下,和前面的 LiftOff 基本一致,只是 run() 中的实现改为 10w 浮点计算

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class SimplePriorities implements Runnable {
private int countDown = 5;
private volatile double d; // No optimization
private int priority;

public SimplePriorities(int priority) {
this.priority = priority;
}

@Override
public String toString() {
return Thread.currentThread() + ": " + countDown;
}

@Override
public void run() {
Thread.currentThread().setPriority(priority);
while (true) {
// An expensive, interruptable operation
for (int i = 0; i < 100000; i++) {
d += (Math.PI + Math.E) / (double) i;
if (i % 1000 == 0)
Thread.yield();
}
System.out.println(this);
if (--countDown == 0) return;
}
}

public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new SimplePriorities(Thread.MIN_PRIORITY));
}
exec.execute(new SimplePriorities(Thread.MAX_PRIORITY));
exec.shutdown();
}
}
// Thread[pool-1-thread-2,1,main]: 5
// Thread[pool-1-thread-3,1,main]: 5
// Thread[pool-1-thread-5,1,main]: 5
// Thread[pool-1-thread-1,1,main]: 5
// Thread[pool-1-thread-4,1,main]: 5
// Thread[pool-1-thread-6,10,main]: 5
// Thread[pool-1-thread-3,1,main]: 4
// Thread[pool-1-thread-2,1,main]: 4
// Thread[pool-1-thread-5,1,main]: 4
// Thread[pool-1-thread-4,1,main]: 4
// Thread[pool-1-thread-1,1,main]: 4
// Thread[pool-1-thread-6,10,main]: 4
// Thread[pool-1-thread-3,1,main]: 3
// Thread[pool-1-thread-1,1,main]: 3
// Thread[pool-1-thread-5,1,main]: 3
// Thread[pool-1-thread-2,1,main]: 3
// Thread[pool-1-thread-4,1,main]: 3
// Thread[pool-1-thread-3,1,main]: 2
// Thread[pool-1-thread-6,10,main]: 3
// Thread[pool-1-thread-1,1,main]: 2
// Thread[pool-1-thread-5,1,main]: 2
// Thread[pool-1-thread-3,1,main]: 1
// Thread[pool-1-thread-6,10,main]: 2
// Thread[pool-1-thread-4,1,main]: 2
// Thread[pool-1-thread-2,1,main]: 2
// Thread[pool-1-thread-1,1,main]: 1
// Thread[pool-1-thread-6,10,main]: 1
// Thread[pool-1-thread-5,1,main]: 1
// Thread[pool-1-thread-4,1,main]: 1
// Thread[pool-1-thread-2,1,main]: 1

Thread 的 toString 方法有自定义过,输出时会打印 thread name + priority + group name.

Mac 上跑这个实验效果并不明显,期望值应该是 CPU 会优先执行 priority 为 5 的线程才对。。。

下面解释 d 变量增加这个 volatile 就是为了防止优化,不然看不到预期结果。难道 mac 上这个设置失效了?!之后调用 yield 释放线权。

JDK 有 10 个等级的优先级设置,不一定和操作系统匹配,比如 Windows 只有 7 级而 Linux 系统有 23 级。

Yielding

通过使用 yield 可以在 task 进行过程中,让渡 CPU 给其他同级别的 task,但是这个让渡并不能被保证,你不能通过他来严格控制 task 的执行顺序。

Daemon threads

守护进程可以在成勋运行时在后台提供一些其他的基础服务,但是这个服务和程序没关系。当所有 非守进程 的程序结束后,守护进程也会被杀死,程序退出。反之,只要有 非守护进程 没有结束,那么守护进程就不会结束。

示例演示

run 方法总我们指定新建的 thread 为 守护进程。当 main 结束时,守护进程也一起结束。

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
public class SimpleDaemons implements Runnable {
@Override
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true);
daemon.start();
}
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(175);
}
}

// All daemons started
// Thread[Thread-5,5,main] org.jz.c23.SimpleDaemons@501a67ca
// Thread[Thread-8,5,main] org.jz.c23.SimpleDaemons@22e706f4
// Thread[Thread-2,5,main] org.jz.c23.SimpleDaemons@4eba943c
// Thread[Thread-0,5,main] org.jz.c23.SimpleDaemons@c0a422e
// Thread[Thread-7,5,main] org.jz.c23.SimpleDaemons@317704a2
// Thread[Thread-3,5,main] org.jz.c23.SimpleDaemons@5ed3479d
// Thread[Thread-6,5,main] org.jz.c23.SimpleDaemons@2482ac6c
// Thread[Thread-9,5,main] org.jz.c23.SimpleDaemons@205caa11
// Thread[Thread-1,5,main] org.jz.c23.SimpleDaemons@71537d82
// Thread[Thread-4,5,main] org.jz.c23.SimpleDaemons@1b02d47b

我们还可以通过定制 ThreadFactory 来生成 Thread. 然后通过 Executors 来做并发

示例说明:

main 中新建了一个 ExecutorService 并用 DaemonThreadFactory 作为参数。通过这种方式,所有创建的并发线程都是守护进程。他们会每隔 100ms 打印一次信息。同时 main 会 sleep 500ms 然后退出。同时守护进程全部结束。

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
public class DaemonThreadFactory implements ThreadFactory  {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}

public class DaemonFromFactory implements Runnable {
@Override
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory());
for (int i = 0; i < 10; i++) {
exec.execute(new DaemonFromFactory());
}
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(500);
}
}

// All daemons started
// Thread[Thread-9,5,main] org.jz.c23.DaemonFromFactory@627a6c14
// Thread[Thread-6,5,main] org.jz.c23.DaemonFromFactory@43ca4e8
// ....

我们还可以使用 ThreadPoolExecutor 来简化上面的操作

1
2
3
4
5
6
public class DaemonThreadPoolExecutor extends ThreadPoolExecutor {
public DaemonThreadPoolExecutor() {
super(0, Integer.MAX_VALUE, 60L,
TimeUnit.SECONDS, new SynchronousQueue<>(), new DaemonThreadFactory());
}
}

你可以通过调用 isDaemon() 方法查看线程是否为 守护进程,由 守护进程 创建的所有 thread 都会自动变为 守护进程。

示例说明:

main 函数中为 Daemon 创建线程,并制定类型为 守护进程

Daemon 这个进程中会新建并启动十个 thread,逻辑都一样,就是生产后一直空转

打印这十个空转进程的类型,可以看到也是 守护进程

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
51
52
53
54
55
56
57
58
59
public class Daemons {

public static void main(String[] args) throws InterruptedException {
Thread d = new Thread(new Daemon());
d.setDaemon(true);
d.start();
System.out.println("d.isDaemon() = " + d.isDaemon() + ", ");
// Allow the daemon threads to finish their startup processes
TimeUnit.SECONDS.sleep(1);
}

}

class DaemonSpawn implements Runnable {
@Override
public void run() {
while (true) {
Thread.yield();
}
}
}

class Daemon implements Runnable {
private Thread[] t = new Thread[10];

@Override
public void run() {
for (int i = 0; i < t.length; i++) {
t[i] = new Thread(new DaemonSpawn());
t[i].start();
System.out.println("DaemonSpawn " + i + " started, ");
}
for (int i = 0; i < t.length; i++) {
System.out.println("t[" + i + "].isDaemon() = " + t[i].isDaemon() + ", ");
}
while (true)
Thread.yield();
}
}
// d.isDaemon() = true,
// DaemonSpawn 0 started,
// DaemonSpawn 1 started,
// DaemonSpawn 2 started,
// DaemonSpawn 3 started,
// DaemonSpawn 4 started,
// DaemonSpawn 5 started,
// DaemonSpawn 6 started,
// DaemonSpawn 7 started,
// DaemonSpawn 8 started,
// DaemonSpawn 9 started,
// t[0].isDaemon() = true,
// t[1].isDaemon() = true,
// t[2].isDaemon() = true,
// t[3].isDaemon() = true,
// t[4].isDaemon() = true,
// t[5].isDaemon() = true,
// t[6].isDaemon() = true,
// t[7].isDaemon() = true,
// t[8].isDaemon() = true,

注意:守护进程是可以在不执行 finally 的情况下退出的

示例说明:

主函数中新建一个 ADaemon 的 thread 并启动。 ADaemon 的 run 方法会答应 “Starting ADaemon” 并在 1s 后打印 “This should always run?”

祝函数启动并结束后,守护进程的 finally 中的语句并没有打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ADaemon implements Runnable {
@Override
public void run() {
try {
System.out.println("Starting ADaemon");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("This should always run?");
}
}
}

public class DaemonsDontRunFinally {
public static void main(String[] args) {
Thread t = new Thread(new ADaemon());
t.setDaemon(true);
t.start();
}
}
// Starting ADaemon

如果我们将 t.setDaemon(true); 注释掉,则 finally 中的内容会被打印出来。

守护进程在没有其他 非守护进程 的时候会立即别 JVM 杀掉。所以一般来说鼓励创建 非守护进程。我们可以通过 Executor 关闭 非守护进程,后面会介绍。

Coding variations

到现在为止,我们都用 Runnable 实现并发,其实并发还可以通过很多其他不同的方式实现

通过继承 Thread 类, 和 Runnable 的区别:

  • Runnable 需要通过 Thread 类或者 Executor 才能启动,Thread 自己就可以启动
  • Runnable 启动时需要调用 start() 方法,Thread 不需要,new 完就启动了
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
public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;

public SimpleThread() {
super(Integer.toString(++threadCount));
start();
}

@Override
public String toString() {
return "#" + getName() + "(" + countDown + "), ";
}

@Override
public void run() {
while(true) {
System.out.println(this);
if (--countDown == 0)
return;
}
}

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new SimpleThread();
}
}
}
// #1(5), #1(4), #3(5), #3(4), #3(3), #3(2), #3(1), #4(5), #4(4),
// #2(5), #4(3), #4(2), #1(3), #1(2), #1(1), #4(1), #5(5), #5(4),
// #2(4), #5(3), #2(3), #2(2), #2(1), #5(2), #5(1),

还可以在 Runnable 的实现中自启动, 这种写法的特点是,在成员变量中,将本身作为参数传给 Thread 的构造函数。在自己的构造函数中,调用 Thread 的 start 方法

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
public class SelfManaged implements Runnable {
private int countDown = 5;
private Thread t = new Thread(this);

public SelfManaged() {
t.start();
}

@Override
public String toString() {
return Thread.currentThread().getName() + "(" + countDown + "), ";
}

@Override
public void run() {
while(true) {
System.out.println(this);
if (--countDown == 0)
return;
}
}

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new SelfManaged();
}
}
}
// Thread-0(5), Thread-2(5), Thread-1(5), Thread-3(5), Thread-2(4), Thread-2(3),
// Thread-4(5), Thread-4(4), Thread-0(4), Thread-4(3), Thread-4(2), Thread-2(2),
// Thread-3(4), Thread-3(3), Thread-3(2), Thread-3(1), Thread-1(4), Thread-2(1),
// Thread-4(1), Thread-0(3), Thread-0(2), Thread-1(3), Thread-0(1), Thread-1(2),
// Thread-1(1),

PS: 这个例子中使用场景很简单,所以可能没什么风险,但是在构造函数中启动线程可能会很危险。其他 task 可能在这个 task 初始化结束之前就开始使用这个 task 对应的 thread,这个对象此时处于一个不稳定状态。这就是我们更倾向于使用 Executor 的原因

有时候,你并不想对外暴露并发类的实现,此时,你可以使用内部类的方式。以下是几种典型应用方式

通过显示的内部类实现

  • InnerThread1 包含内部类 Inner,内部类继承 Thread
  • Inner 的构造函数会调用 start 方法启动线程
  • InnerThread1 有构造函数,调用 Inner 的构造函数
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
// Using a named inner class
class InnerThread1 {
private int countDown = 5;
private Inner inner;

private class Inner extends Thread {
Inner(String name) {
super(name);
start();
}

@Override
public void run() {
try {
while (true) {
System.out.println(this);
if (--countDown == 0) return;
sleep(10);
}
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}

public String toString() {
return getName() + ": " + countDown;
}
}

public InnerThread1(String name) {
inner = new Inner(name);
}
}

通过显示的内部类实现, 和上面的例子大同小异,只是把 Thread 类的声明塞到了 InnerThread2 构造函数中,然后直接调用 start() 开启线程

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
// Using an anonymous inner class
class InnerThread2 {
private int countDown = 5;
private Thread t;

public InnerThread2(String name) {
t = new Thread(name) {
@Override
public void run() {
try {
while (true) {
System.out.println(this);
if (--countDown == 0) return;
sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}

内部类 + Runnable, 和第一个例子类似,只不过通过 Runnable 接口做实现,start 的调用直接放在 Inner 的构造函数中

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
// Using a named Runnable implementation
class InnerRunnable1 {
private int countDown = 5;
private Inner inner;

private class Inner implements Runnable {
Thread t;

public Inner(String name) {
t = new Thread(this, name);
t.start();
}

@Override
public void run() {
try {
while (true) {
System.out.println(this);
if (--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString() {
return t.getName() + ": " + countDown;
}
}

public InnerRunnable1(String name) {
inner = new Inner(name);
}
}

内部类 + Runnable, Runnable 实现放在构造函数中

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
// Using an anonymous Runnable implementation
class InnerRunnable2 {
private int countDown = 5;
private Thread t;

public InnerRunnable2(String name) {
t = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
System.out.println(this);
if (--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public String toString() {
return Thread.currentThread().getName() + ": " + countDown;
}
}, name);
t.start();
}
}

将 Runnable 放到方法中做实现

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
// A separate method tu run some code as a task
class ThreadMethod{
private int countDown = 5;
private Thread t;
private String name;

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

public void runTask() {
if (t == null) {
t = new Thread(name) {
@Override
public void run() {
try {
while(true) {
System.out.println(this);
if (--countDown == 0) return;
sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t.start();
}
}
}

客户端调用

1
2
3
4
5
6
7
8
9
public class ThreadVariations {
public static void main(String[] args) {
new InnerThread1("InnerThread1");
new InnerThread2("InnerThread2");
new InnerRunnable1("InnerRunnable1");
new InnerRunnable2("InnerRunnable2");
new ThreadMethod("ThreadMethod").runTask();
}
}

Terminology

术语解释,没发现什么有趣的点

Joining a thread

在线程 A 的执行中,如果你 call 了线程 B 的 join() 方法,那么,线程 A 会等待线程 B 结束后再执行。

上面的 join 的行为可以通过调用 B 的 interrup() 方法进行打断

示例解析:

Sleeper 继承自 Thread 通过构造函数指定线程名称和休眠时间。当被打断时输出日志。

Joiner 继承自 Thread, 通过参数指定 Thread name 和将要 join 的 thread

主程序中,创建两个 Sleeper 类,再创建两个 Joiner 类并将 Sleeper 分别传给他们。

Joiner 执行的时候,会等待传入的 Sleeper 执行结束再继续执行。其中一个 Sleper 调用 interrupt() 方法,中途中断,对应的 Joiner 继续执行

PS: 当现场被打断时,isInterrupted 被设置为 true, 当异常被捕获时,重制为 false。

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
51
52
53
54
55
56
public class joining {
public static void main(String[] args) {
Sleeper
sleepy = new Sleeper("Sleepy", 1500),
grumpy = new Sleeper("Grumpy", 1500);
Joiner
dopey = new Joiner("Dopey", sleepy),
doc = new Joiner("Doc", grumpy);
grumpy.interrupt();
}
}

class Sleeper extends Thread {
private final int duration;
public Sleeper (String name, int sleepTime) {
super(name);
duration = sleepTime;
start();
}

@Override
public void run() {
try {
sleep(duration);
}catch (InterruptedException e) {
System.out.println(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted());
return;
}
System.out.println(getName() + " has awakened");
}
}

class Joiner extends Thread {
private final Sleeper sleeper;

public Joiner(String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}

@Override
public void run() {
try {
sleeper.join();
}catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println(getName() + " join completed");
}
}

// Grumpy was interrupted. isInterrupted(): false
// Doc join completed
// Sleepy has awakened
// Dopey join completed

Creating responsive user interface

模拟图形界面,但是不太能 get 到他的点,pass

Thread groups

Thread group 是一个失败的作品,你最好忘记他的存在

Catching exceptions

由于 Thread 的特性,run() 中抛出的异常,你不能在 main 中 catch。示例如下,我们新建一个 Runnable 的实现类,并让跑抛异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ExceptionThread implements Runnable {
@Override
public void run() {
throw new RuntimeException();
}

public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
}
// Exception in thread "pool-1-thread-1" java.lang.RuntimeException
// at org.jz.c23.ExceptionThread.run(ExceptionThread.java:9)
// at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
// at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
// at java.lang.Thread.run(Thread.java:836)

我们在 executor 外面添加 try-catch 试图捕获异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class NaiveExceptionHandling {
public static void main(String[] args) {
try {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}catch (RuntimeException e) {
System.out.println("Exception has been handled...");
}
}
}
// Exception in thread "pool-1-thread-1" java.lang.RuntimeException
// at org.jz.c23.ExceptionThread.run(ExceptionThread.java:9)
// at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
// at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
// at java.lang.Thread.run(Thread.java:836)

然并卵。。。。这时,你可以结合 Executor 使用它,在 new pool 的时候指定 factory, 并在 factory 的实现中指定异常的处理器

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
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
}
}

class ExceptionThread2 implements Runnable {
@Override
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println("eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}

class HandlerThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
System.out.println(this + " creating new thread");
Thread t = new Thread(r);
System.out.println("create " + t);
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
// org.jz.c23.HandlerThreadFactory@39a054a5 creating new thread
// create Thread[Thread-0,5,main]
// eh = org.jz.c23.MyUncaughtExceptionHandler@6ed3ef1
// run() by Thread[Thread-0,5,main]
// eh = org.jz.c23.MyUncaughtExceptionHandler@6ed3ef1
// org.jz.c23.HandlerThreadFactory@39a054a5 creating new thread
// create Thread[Thread-1,5,main]
// eh = org.jz.c23.MyUncaughtExceptionHandler@78463d45
// caught java.lang.RuntimeException

PS: 不知道为毛会有两次创建 handler 的动作,好奇怪

如果所有的异常都是一个 handler 处理的,还可以直接将 handler 设置给 Thread 简化操作

1
2
3
4
5
6
7
8
public class SettingDefaultHandler {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
}
// caught java.lang.RuntimeException

中间中间好几节暂时不用,先放一放

Sharing resources

并发需要解决的是多个线程操作共用资源的问题

Improperly accessing resources

举一个多线程使用 int 生成器的例子。我们创建一个生成器的抽象接口,定义了抽象类中的方法。

  • next() - 生成 int 结果
  • cancel() - 设置 flag
  • isCancel() - 返回 flag 结果

canceled 这个 flag 还被定义为 volatile 确保其他线程可见

cancel() 为 boolean 赋值语句,是一个原子操作

1
2
3
4
5
6
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
public void cancel() { canceled = true; }
public boolean isCanceled() { return canceled; }
}

定义 EvenChecker 并发检测奇偶情况.

  • run() - 拿到生成的 int 值并判断,如果为奇数则停止线程
  • test() - 重载了两个 test 方法,启动多个线程运行 run 方法
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
public class EvenChecker implements Runnable {
private IntGenerator generator;
private final int id;

public EvenChecker(IntGenerator g, int ident) {
generator = g;
id = ident;
}

@Override
public void run() {
while (!generator.isCanceled()) {
int val = generator.next();
if (val % 2 != 0) {
System.out.println(val + " not event...");
generator.cancel();
}
}
}

public static void test(IntGenerator gp, int count) {
System.out.println("Ctrl + c to exit");
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < count; i++) {
exec.execute(new EvenChecker(gp, i));
}
exec.shutdown();
}

public static void test(IntGenerator gp) {
test(gp, 10);
}
}

生成器的实现类 EvenGenerator, 声明一个初始值,并通过两个 ++ 运算,达到偶数次递增的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;

@Override
public int next() {
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}

public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
// }
// Ctrl + c to exit
// 1515 not event...
// 1519 not event...
// 1517 not event...

运行后可以看到,三个线程检测到目标为奇数,停止了 generator。当执行 next() 时,有可能执行了一半,切到另一个线程执行 run() 中的判断了。这时,程序状态就会出错。你还可以在两个 ++ 操作中间通过新加 yield() 方法来加大重现频率。

还有一个需要注意的是 i++ 并不是一个原子操作,可能在执行间就切到另一个线程了。

Resolving shared resource contention

这里举了一个挺有意思的例子,处理并发就像是 你坐在餐桌上,准备夹一块肉的时候,突然,肉没了(你的线程被暂停,同时其他线程操作了这个资源)

你可以通过加锁防止这种事情的发生,这样保证一个资源同一时间只能由一个 task 访问,其他 task 需要排队等待解锁。

代码层面,Java 通过 synchronized 关键字来实现这一功能。shared resource 同行来说是一片系统内存,以对象的形式表现出来。也可能是一个文件,或者 IO 端口或者一些设备,比如打印机之类的。

你需要将 class 中代表你要 lock 的对象声明为 private,并且所有和这个对象相关的方法前加关键字,示例如下

1
2
synchronized void f() {}
synchronized void g() {}

当一个方法被调用的时候,其他加了关键字的方法都会被 lock 住,直到前一个方法执行完毕为止。

PS: 将用到的对象声明为 private 是很关键的一步,否则控制并发会失败。

上述对象在 JVM 有一个 field 来记录锁的数量,默认为 0,当 synchronized 方法被调用时,count + 1,当对应的 task 调用这个对象的两一个 synchronized 方法时,再 + 1.

此外还有 class level 的 lock 用来控制 static 方法的同步,保证同一时间只有一个 task 访问这个静态方法。

加锁的原则:This is an important point: Every method that accesses a critical shared resource must be synchronized or it won’t work right.

Synchronizing the EvenGenerator

根据上一节的描述,我们通过给 next 方法加锁,将之前的 EvenGenerator 改为线程安全版本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SynchronizedEvenGenerator extends IntGenerator {
private int currentEvenValue = 0;

@Override
public synchronized int next() {
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
}

public static void main(String[] args) {
EvenChecker.test(new SynchronizedEvenGenerator());
}
}

Using explicit Lock objects

Java 5 的 concurrent 包中提供了一个 Lock 类来更精确的控制锁的范围,示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MutexEvenGenerator extends IntGenerator {
private int currentEventValue = 0;
private Lock lock = new ReentrantLock();

@Override
public int next() {
lock.lock();
try {
++currentEventValue;
Thread.yield();
++currentEventValue;
return currentEventValue;
} finally {
lock.unlock();
}
}

public static void main(String[] args) {
EvenChecker.test(new MutexEvenGenerator());
}
}

用上面这种写法,你需要注意, 调用 lock() 方法之后,一定要用 try-finally 的语法,将 unlock 放到 finally 中,并切 try block 里面完成 return 的动作,防止指在外面被改动。

相比于传统的 synchronized 方式,try-finally 代码更多,但是它给你机会再程序出错时做出补救。

通过使用 concurrent 包下的方法,你可以实现 re-try 的机制

下面的例子中定义了两个方法,untimed/timed 功能都是一样的,尝试获取锁,并打印获取的情况。main 中一开始,顺序执行,两个方法可以拿到锁,并在使用完后释放。后面通过匿名类,启动一个新线程,获取锁并不释放,后面再次调用之前的方法,返回获取锁失败。

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 AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
boolean captured = lock.tryLock();
try {
System.out.println("tryLock(): " + captured);
} finally {
if(captured)
lock.unlock();
}
}

public void timed() {
boolean captured = false;
try {
captured = lock.tryLock(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);
}finally {
if (captured)
lock.unlock();
}
}

public static void main(String[] args) throws InterruptedException {
final AttemptLocking al = new AttemptLocking();
al.untimed();
al.timed();
new Thread() {
{setDaemon(true);}

@Override
public void run() {
al.lock.lock();
System.out.println("acquired");
}
}.start();
Thread.sleep(1000);
al.untimed();
al.timed();
}
}
// tryLock(): true
// tryLock(2, TimeUnit.SECONDS): true
// tryLock(): true
// acquired
// tryLock(2, TimeUnit.SECONDS): false

Atomicity and volatility

一个原子操作是指,如果这个操作开始了,那么只有在操作结束后,JVM 才会考虑进行上下文切换。考虑到原子操作这么冷门而且很危险,建议专家级别了再作原子操作代替 synchronized 的优化。

The Goetz Test: If you can write a hight-perormance JVM for a modern microprocessor, then you are qualified to think about whether you can avoid synchronizing.

鼓励使用官方为你写的工具包(concurrent),而不是自己造的轮子。

‘simple operation’ 和 除了 long/double 的 primitive 类型的数据操作都是原子操作。JVM 在处理 long/double 时会分成两个指令处理,但是如果你为这两类数据加上 volatile 修饰之后,可以保证原子性。

在多核处理器系统中,visibility 是比 atomicity 更容易出问题的点。一个 task 进行的一个原子操作,由于改动存放在本地处理器的 cache 中,导致其他 task 不知道这个改动,从而导致不同 task 之间 application 的状态不一致。synchronization 机制可以保证一个 task 的改动在其他 task 上也可以被观察到。

volatile 也可以保证这一点。如果你声明了一个 volatile 变量,一旦写操作执行了,那么所有要读他的地方会立即观察到这个变化,其实是 local cache 的情况也能保证。volatile 会保证 write 的动作立即反应到驻内存中。

atomicity 和 volatility 是两个概念,一个非 volatile 的原子操作并不会被 flush 到主内存中,如果多个 task 对他进行操作,会产生不一致。如果多个 task 都要访问一个 field,那么他就需要声明为 volatile 类型,或者用 synchronization 来管理他。如果用了 synchronizaiton 管理,就不需要用 volatile 修饰了

优先考虑用 synchronization,这个是最安全的解决方案。

Java 中赋值和返回语句是原子操作,自增/减不是。。。Java 反编译自增代码如下

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
//: concurrency/Atomicity.java
// {Exec: javap -c Atomicity}
public class Atomicity {
int i;
void f1() { i++; }
void f2() { i += 3; }
}
/* Output: (Sample)
... void f1();
Code:
0: aload_0
1: dup
2: getfield #2; //Field i:I
5: iconst_1
6: iadd
7: putfield #2; //Field i:I
10: return
void f2();
Code:
0: aload_0
1: dup
2: getfield #2; //Field i:I
5: iconst_3
6: iadd
7: putfield #2; //Field i:I
10: return

可以看到在 get 和 put 指令中间,还会执行一些其他的指令。上下文切换有可能发生在执行这些指令的时候,所以并不是原子行的。

即使是 getValue() 这样的操作,虽然说他是原子操作,但是如果不加 synchronized 也可能会出问题. 下面的程序中,我们实现了一个自增函数 evenIncrement 并用 synchronized 修饰,在 run 中让他一直运行。在 main 中启动这个线程,并打印当前 i 的值。可以看到还是会出问题。问题有两个

  • 变量没有用 volatile 修饰
  • getValue 没有用 synchronized 修饰

试了一下,即使 i 用 volatile 修饰了还是会出问题的

没有 synchronized 修饰时,程序允许 getValue 在状态不确定的情况下访问变量,所以会出问题。

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
public class AtomicityTest implements Runnable{
private int i = 0;
public int getValue() { return i; }
private synchronized void evenIncrement() { i++; i++; }

@Override
public void run() {
while(true)
evenIncrement();
}

public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);

while(true) {
int val = at.getValue();
if(val %2 !=0) {
System.out.println(val);
System.exit(0);
}
}
}
}
// 423

下面是一个更简单的例子,一个生成器给出一系列的数字

1
2
3
4
5
6
public class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static int nextSerialNumber() {
return serialNumber++; // Not thread-safe
}
}

将变量声明为 volatile 可以告诉编译器不要对它进行优化。读写会被直接反应到内存中,而不是 cache. 而且还能防止指令重排。但是它并不表示这个自增是一个原子操作。

通常来说,如果有多个 task 操作一个 field,并且至少有一个会对他进行写操作,那么你就要讲他声明为 volatile。比如 flag 的操作。

为了测试上面的这个类,我们创建了如下的测试类

CircularSet 是一个容器类,用来存储生成器产生的数据。定义了一个数组,可以指定大小。 add/contains 使用 synchronized 修饰。主线程中,启动 10 个线程生产数据并存到容器中。理论上来说,如果线程安全,容器中不会有重复数据,如果有,则报错,结束进程。

这里一个比较巧妙的设置是,CircularSet 会存储一个下标,如果生产的值超出容量,他会循环利用之前的位置,覆盖之前的值。

可以在 SerialNumberGenerator 的 nextSerialNumber 方法前添加 synchronized 修饰修复这个问题。

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
51
52
53
54
55
56
57
58
59
public class SerialNumberChecker {
private static final int SIZE = 10;
private static CircularSet serials = new CircularSet(1000);
private static ExecutorService exec = Executors.newCachedThreadPool();

static class SerialChecker implements Runnable {
@Override
public void run() {
while(true) {
int serial = SerialNumberGenerator.nextSerialNumber();
if (serials.contains(serial)) {
System.out.println("Duplicate: " + serial);
System.exit(0);
}
serials.add(serial);
}
}
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < SIZE; i++) {
exec.execute(new SerialChecker());
}

if (args.length > 0) {
TimeUnit.SECONDS.sleep(new Integer(args[0]));
System.out.println("No duplicates detected");
System.exit(0);
}
}
}

class CircularSet {
private int[] array;
private int len;
private int index = 0;
public CircularSet(int size) {
array = new int[size];
len = size;
// Initialize to a value not produced by the SerialNumberChecker
for (int i = 0; i < size; i++) {
array[i] = -1;
}
}

public synchronized void add(int i) {
array[index] = i;
// Wrap index and write over old elements
index = ++index % len;
}

public synchronized boolean contains(int val) {
for (int i = 0; i < len; i++) {
if (array[i] == val) return true;
}
return false;
}
}
// Duplicate: 47

Atomic classes

Java 5 引入了原子类,如 AtomicInteger, AtomicLong 和 AtomicReference 等提供原子级别的更新操作。

boolean compareAndSet(expectedValue, updateValue);

他们做过优化,可以保证机器层面的原子性,一般来说,你可以放心使用。下面我们用他们来重写之前的测试类 AtomicityTest.java. 内部逻辑和之前一样,我们将 volatile 和 synchronized 关键字都去了,同时定义一个 main 函数,在里面使用 Timer 设置程序 5s 之后退出,到程序结束为止一切运行正常。

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
public class AtomicIntegerTest implements Runnable {
private AtomicInteger i = new AtomicInteger(0);

public int getValue() {
return i.get();
}

private void evenIncrement() {
i.addAndGet(2);
}

@Override
public void run() {
while (true)
evenIncrement();
}

public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Aborting");
System.exit(0);
}
}, 5000);
ExecutorService exec = Executors.newCachedThreadPool();
AtomicIntegerTest ait = new AtomicIntegerTest();
exec.execute(ait);
while (true) {
int val = ait.getValue();
if (val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}

同样的,我们使用 AtomicInteger 重写 EvenGenerator

1
2
3
4
5
6
7
8
9
10
11
12
public class AtomicEvenGenerator extends IntGenerator {
private AtomicInteger currentEvenValue = new AtomicInteger(0);

@Override
public int next() {
return currentEvenValue.addAndGet(2);
}

public static void main(String[] args) {
EvenChecker.test(new AtomicEvenGenerator());
}
}

虽然 Atomic class 可以解决原子行问题,但是还是强烈推荐使用锁机制。

Critial sections

我们可以通过 critical sections 的方式,只对一段代码进行保护而不是整个方法

1
2
3
4
synchronized(syncObject) {
// This code can be accessed
// by only one task at a time
}

这种方式也叫做 synchronized block. 如果此时 syncObject 被其他 task lock 了,那么当前 task 会一直等待,直到 lock 被释放。以下示例对两种 lock 方式进行性能比较

Pair 是我们要操作的对象,线程不安全,这个模型就是内部存两个 int 变量,我们的目标是保证这两个变量想等。还有一个 checkState 方法,如果两个变量值不同,则抛异常。

PairManager 是一个抽象类,里面声明了一个用来存储 pair 的 list, 使用 Collections.synchronizedList() 得到,所以线程安全。 getPair() 也用 synchronized 修饰,线程安全。 store 虽然没有修饰但是只在实现类中调用,调用的时候会加 synchronized 限制。

PairManager1, PairManager2 都是 PairManager 的实现,区别是一个用了 method level 的 lock, 一个用了 block level 的 lock

PairManipulator 代表使用 PairManager 的 task, 我们通过它来启动多线程实现 PM 的 increment 调用

PairChecker 也是一个多线程的 task 它用来检测 PM 的状态并记录检测次数

CriticalSection 相当于 client,将上面说的这些元素整合并调用

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
public class CriticalSection {
// Test the two different approaches:
static void testApproaches(PairManager pman1, PairManager pman2) {
ExecutorService exec = Executors.newCachedThreadPool();
PairManipulator
pm1 = new PairManipulator(pman1),
pm2 = new PairManipulator(pman2);
PairChecker
pcheck1 = new PairChecker(pman1),
pcheck2 = new PairChecker(pman2);
exec.execute(pm1);
exec.execute(pm2);
exec.execute(pcheck1);
exec.execute(pcheck2);
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
System.out.println("Sleep interrupted");
}
System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
System.exit(0);
}

public static void main(String[] args) {
PairManager
pman1 = new PairManager1(),
pman2 = new PairManager2();
testApproaches(pman1, pman2);
}
}

class Pair {
private int x, y;

public Pair(int x, int y) {
this.x = x;
this.y = y;
}

public Pair() {
this(0, 0);
}

public int getX() {
return x;
}

public int getY() {
return y;
}

public void incrementX() {
x++;
}

public void incrementY() {
y++;
}

@Override
public String toString() {
return "x: " + x + ", y: " + y;
}

public class PairValuesNotEqualException extends RuntimeException {
public PairValuesNotEqualException() {
super("Pair values not equal: " + Pair.this);
}
}

public void checkState() {
if (x != y)
throw new PairValuesNotEqualException();
}
}

abstract class PairManager {
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
private List<Pair> storage = Collections.synchronizedList(new ArrayList<>());

public synchronized Pair getPair() {
// Make a copy to keep the original safe:
return new Pair(p.getX(), p.getY());
}

// Assme this is a time consuming peration
protected void store(Pair p) {
storage.add(p);
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public abstract void increment();
}

class PairManager1 extends PairManager {
@Override
public synchronized void increment() {
p.incrementX();
p.incrementY();
store(getPair());
}
}

// Use a critical section
class PairManager2 extends PairManager {
@Override
public void increment() {
Pair temp;
synchronized (this) {
p.incrementX();
p.incrementY();
temp = getPair();
}
store(temp);
}
}

class PairManipulator implements Runnable {
private PairManager pm;

public PairManipulator(PairManager pm) {
this.pm = pm;
}

@Override
public void run() {
while (true)
pm.increment();
}

@Override
public String toString() {
return "Pair: " + pm.getPair() + " checkCounter = " + pm.checkCounter.get();
}
}

class PairChecker implements Runnable {
private PairManager pm;

public PairChecker(PairManager pm) {
this.pm = pm;
}

@Override
public void run() {
while (true) {
pm.checkCounter.incrementAndGet();
pm.getPair().checkState();
}
}
}

// pm1: Pair: x: 42, y: 42 checkCounter = 2
// pm2: Pair: x: 42, y: 42 checkCounter = 2133930

PS: Note that the synchronized keyword is not part of the method signature and thus may be added during overriding.

synchronized 不是方法签名的一部分!!

从输出的实验结果可以看到方法锁的可使用率要比 block 锁低很多,完全是碾压级别的差距。block 类型的锁可以提供更多的 unlock time.

下面通过使用 Lock 类来进行精确锁, 共能和之前的例子类似,只不过新实现了两个 PairManager 类,一个还是用方法级别的锁, ExplicitPairManager1 由于已经加了 synchronized 的了,里面的 lock 其实没什么用,去掉也不影响结果

ExplicitPairManager1 中直接使用了 Lock 类进行 block level 的锁。运行结果失败,会抛异常

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
51
52
53
54
55
public class ExplicitCriticalSection {
public static void main(String[] args) {
PairManager
pman1 = new ExplicitPairManager1(),
pman2 = new ExplicitPairManager2();
CriticalSection.testApproaches(pman1, pman2);
}
}

class ExplicitPairManager1 extends PairManager {
private Lock lock = new ReentrantLock();

@Override
public synchronized void increment() {
lock.lock();
try {
p.incrementX();
p.incrementY();
store(getPair());
} finally {
lock.unlock();
}
}
}

class ExplicitPairManager2 extends PairManager {
private Lock lock = new ReentrantLock();
public void increment() {
Pair temp;
lock.lock();
try {
p.incrementX();
p.incrementY();
temp = getPair();
} finally {
lock.unlock();
}
store(temp);
}
//
// @Override
// public Pair getPair() {
// lock.lock();
// try {
// return new Pair(p.getX(), p.getY());
// } finally {
// lock.unlock();
// }
// }
}
// Exception in thread "pool-1-thread-4" org.jz.c23.Pair$PairValuesNotEqualException: Pair values not equal: x: 2, y: 1
// at org.jz.c23.Pair.checkState(CriticalSection.java:83)
// at org.jz.c23.PairChecker.run(CriticalSection.java:163)
// pm1: Pair: x: 127, y: 127 checkCounter = 3
// pm2: Pair: x: 127, y: 127 checkCounter = 1816160

搜索了一下,发先这个博客说的挺好. 总结一下就是 getPair 用的 synchronized 语法,而 increment 用的 Lock 类的方法,两个都是锁,但是拿到的锁是不一样的,将 getPair 重写一下,也用同样的 Lock 类提供的锁即可修复。

Synchronizing on other objects

synchronized block 的写法中,需要给出一个 lock 的对象,一般来说我们都会使用 this 作为参数,表示持有这个方法的对象就是我们要 lock 的对象。当然你也可以指定另一个对象,但是你一定要理清楚自己的业务逻辑,知道你要 lock 的对象是哪一个

下面例子中声明了一个 DualSynch 类,里面有两个方法,f(), g() 分别打印 5 次,f() 是 method lock,就是锁住自己的意思,g() 在内部指定锁住一个内部成员变量。在主函数中,我们通过新起线程调用 f(),在主函数中调用 g()。可以看到虽然外部 class 实体也有 lock 但是和内部的变量是不冲突的,两个 task 可以一起执行

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
public class SyncObject {
public static void main(String[] args) {
final DualSynch ds = new DualSynch();
new Thread(ds::f).start();
ds.g();
}
}

class DualSynch {
private Object syncObject = new Object();
public synchronized void f() {
for (int i = 0; i < 5; i++) {
System.out.println("f()");
Thread.yield();
}
}

public void g() {
synchronized (syncObject) {
for (int i = 0; i < 5; i++) {
System.out.println("g()");
Thread.yield();
}
}
}
}
// g()
// f()
// f()
// g()
// f()
// f()
// f()
// g()
// g()
// g()

Thread local storage

Java 还提供了另一种解决多线程使用共享资源时的冲突问题,叫做 ThreadLocal.

对与被管理的变量,Thread local storage 会在不同的 thread 总为变量创建单独的副本,所以各个 thread 彼此不会被影响到。

下面的例子中 Accessor 是一个具体的 task,他会通过 while 循环不停的调用 holder 的 increment 方法并答应对应的值。

ThreadLocalVariableHolder 中持有一个 Integer 类型的 ThreadLocal 变量,提供自增长方法,在 main 函数中,启动五个线程,调用自增方法并打印。

可以看到每个线程中拿到的 integer 值都是不一样的,而且相互不影响。

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
51
52
53
54
55
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
private Random rand = new Random(47);

protected synchronized Integer initialValue() {
return rand.nextInt(10000);
}
};

public static void increment() {
value.set(value.get() + 1);
}

public static int get() {
return value.get();
}

public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new Accessor(i));
}
TimeUnit.SECONDS.sleep(3);
exec.shutdown();
}
}

class Accessor implements Runnable {
private final int id;

public Accessor(int idn) {
id = idn;
}

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}

@Override
public String toString() {
return "#" + id + ": " + ThreadLocalVariableHolder.get();
}
}
// #0: 9259
// #1: 556
// #2: 6694
// #0: 9260
// #2: 6695
// #1: 557
// ...

Terminating tasks

本章介绍如何外部结束 task

The ornamental garden

下面的例子模拟一个植物园的场景,植物园入口处有闸机,通过统计闸机记述统计园内总人数。只做演示用,没有其他深意。

Count 用来管理总人数,提供 increment 和 value 方法,且都是 synchronized 修饰的。increment 中还包含一个 yield 方法用来提高多线程问题出发的概率。

Entrance 表示入口,他持有一个 Count 的静态变量,用来合计总人数。同时还申明了一个 number 的成员变量,用来审计从这个门进入的游客数量。声明 entrances 这个静态变量,用于线程结束后的统计,cancel 声明为 volatile 用来控制 task 的结束。

OrnamentalGarden 为 client 端,他的 main 函数会启动五个线程模拟入园操作。3s 后结束,分别答应 number 加和以及 count 值做统计,两个值应该是一样的。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class OrnamentalGarden {
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new Entrance(i));
}
// Run for a while then stop and collect the data:
TimeUnit.SECONDS.sleep(3);
Entrance.cancel();
exec.shutdown();
if (!exec.awaitTermination(250, TimeUnit.MILLISECONDS))
System.out.println("Some task were not terminated!");
System.out.println("Total: " + Entrance.getTotalCount());
System.out.println("Sum of Entrances: " + Entrance.sumEntrances());
}
}

class Count {
private int count = 0;
private Random rand = new Random(47);

// Remove the synchronized keyword to see counting fail:
public synchronized int increment() {
int temp = count;
if (rand.nextBoolean()) // Yield half the time
Thread.yield();
return (count = ++temp);
}

public synchronized int value() {
return count;
}
}

class Entrance implements Runnable {
private static Count count = new Count();
private static List<Entrance> entrances = new ArrayList<>();
private int number = 0;
// Doesn't need synchronization to read:
private final int id;
private static volatile boolean canceled = false;

// Atomic operation on a volatile field:
public static void cancel() {
canceled = true;
}

public Entrance(int id) {
this.id = id;
// Keep this task in a list. Also prevents garbage collection of dead tasks:
entrances.add(this);
}

@Override
public void run() {
while (!canceled) {
synchronized (this) {
++number;
}
System.out.println(this + " Total: " + count.increment());
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("Sleep interrupted");
}
}
System.out.println("Stopping " + this);
}

public synchronized int getValue() {
return number;
}

@Override
public String toString() {
return "Entrance " + id + ": " + getValue();
}

public static int getTotalCount() {
return count.value();
}

public static int sumEntrances() {
int sum = 0;
for (Entrance entrance : entrances) {
sum += entrance.getValue();
}
return sum;
}
}
// Entrance 0: 1 Total: 1
// Entrance 2: 1 Total: 3
// Entrance 1: 1 Total: 2
// Entrance 3: 1 Total: 4
// Entrance 4: 1 Total: 5
// ...
// Entrance 2: 30 Total: 150
// Entrance 1: 30 Total: 148
// Stopping Entrance 3: 30
// Stopping Entrance 4: 30
// Stopping Entrance 1: 30
// Stopping Entrance 2: 30
// Stopping Entrance 0: 30
// Total: 150
// Sum of Entrances: 150

Terminating when blocked

Thread states

一个 Thread 可能处于四种状态中的任意一种

  1. New: 这种状态很短暂,在创建线程的时候出现。系统为他配置所需要的资源,完成后,scheduler 会把它置于 runnable 或者 blocked
  2. Runnable: 当 CPU 有空闲时就可以运行它
  3. Blocked: 可以运行,但是被阻止了。CPU 会直接跳过它。
  4. Dead: task 结束了,不会再被 schedule。从 run() 中返回,或者被 interrupted 时会处于这种状态。

Becoming blocked

一下情况会导致 task 进入 block 状态

  • 调用 sleep() 方法
  • 调用 wait() 方法,可以调用 notify()/notifyAll() 解除
  • 等待 I/O 完成
  • 调用其他被 lock 的方法时

Interruption

和你预期的一样,在 thread 中间打断它要比等到它出来,判断 cancel flag 结束要复杂的多,你打断 thread 的时候可能要处理很多 clean up 的操作。

你可以通过 Thread.interrupt() 方法打断线程,这个方法会将线程设置为 interrupted 状态,然后这个线程就会抛出 InterruptedException. 这个状态会在异常抛出或者调用 Thread.interrupted() 方法的时候置位。interrupted() 是另一种结束 run() 而不抛异常的方法。

为了调用 interrupt() 方法,你需要持有 Thread 对象。Java 提供的 concurrent 包让你避免直接使用 Thread,你可以用 Executor 来完成这类工作。shutdownNow() 会向它开启的所有线程发送 interrupt() 指令。如果你想单独控制某个 task 你可以使用 Executor 的 submit() 方法,它会返回 Feature 对象,你可以调用 feature.cancel(true) 来给对应的 task 传递 interrupt 指令。

下面是通过 feature.cancel() 来中断线程的测试, 定义了三种 block

SleepBlocked: 普通的 Runnable 实现,在 run 方法中,sleep 100s 作为 block

IOBlocked: 普通的 Runnable 实现,run 中读取输入流的内容

SynchronizedBlocked: f() 中无限循环调用 yield, 在构造函数中新起一个线程,调用 f(), 然后 main 中通过 test 起新线程制造 lock

Interrupting: 测试类, 写了一个 test 方法,接收 Runnable 实现,并通过 submit() 运行,然后通过 cancel(true) 中断 task. main 函数中将之前定义的 block 分别进行 test。

从输出可以看出,你可以 interrupt sleep 类型的 block,但是不能打断 IO 或者 Synchronized 类型的锁

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class Interrupting {
private static ExecutorService exec = Executors.newCachedThreadPool();
static void test(Runnable r) throws InterruptedException {
Future<?> f = exec.submit(r);
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Interrupting " + r.getClass().getName());
f.cancel(true); // Interrupts if running
System.out.println("Interrupt sent to " + r.getClass().getName());
}

public static void main(String[] args) throws InterruptedException {
test(new SleepBlocked());
test(new IOBlocked(System.in));
test(new SynchronizedBlocked());
TimeUnit.SECONDS.sleep(3);
System.out.println("Aborting with System.exit(0)");
System.exit(0);
}
}

class SleepBlocked implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("InterruptedException");
}
System.out.println("Existing SleepBlocked.run()");
}
}

class IOBlocked implements Runnable {
private InputStream in;

public IOBlocked(InputStream in) {
this.in = in;
}

@Override
public void run() {
try {
System.out.println("Waiting for read(): ");
in.read();
} catch (IOException e) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted from blocked I/O");
} else {
throw new RuntimeException(e);
}
}
System.out.println("Exiting IOBlocked.run()");
}
}

class SynchronizedBlocked implements Runnable {
public synchronized void f() {
while(true)
Thread.yield();
}

public SynchronizedBlocked() {
new Thread() {
public void run() {
f(); // Lock acquired by this thread
}
}.start();
}

public void run () {
System.out.println("Trying to call f()");
f();
System.out.println("Exiting SynchronizedBlocked.run()");
}
}

// Interrupting org.jz.c23.SleepBlocked
// Interrupt sent to org.jz.c23.SleepBlocked
// InterruptedException
// Existing SleepBlocked.run()
// Waiting for read():
// Interrupting org.jz.c23.IOBlocked
// Interrupt sent to org.jz.c23.IOBlocked
// Trying to call f()
// Interrupting org.jz.c23.SynchronizedBlocked
// Interrupt sent to org.jz.c23.SynchronizedBlocked
// Aborting with System.exit(0)

有时你可以通过关闭底层的 resource 来中断 IO, 从输出可以看到, Socket 的输入流是通过异常关闭的,而 System.in 不是。

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
public class CloseResource {
public static void main(String[] args) throws IOException, InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(8080);
InputStream socketInput = new Socket("localhost", 8080).getInputStream();

exec.execute(new IOBlocked(socketInput));
exec.execute(new IOBlocked(System.in));

TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Shutting down all threads");
exec.shutdownNow();

TimeUnit.SECONDS.sleep(1);
System.out.println("Closing " + socketInput.getClass().getName());
socketInput.close(); // Releases blocked thread

TimeUnit.SECONDS.sleep(1);
System.out.println("Closing " + System.in.getClass().getName());
System.in.close();
}
}

// Waiting for read():
// Waiting for read():
// Shutting down all threads
// Closing java.net.SocketInputStream
// Interrupted from blocked I/O
// Exiting IOBlocked.run()
// Closing java.io.BufferedInputStream
// Exiting IOBlocked.run()

好消息是 nio 相关的类有提供更好的终端 IO 的方法. blocked nio channels 会自动相应 interrupt 信号。

从输出可以看到,关闭底层的 channel 会释放 block,虽然这种方式和少用到。

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 NIOInterruption {
public static void main(String[] args) throws IOException, InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(8080);
InetSocketAddress isa = new InetSocketAddress("localhost", 8080);
SocketChannel sc1 = SocketChannel.open(isa);
SocketChannel sc2 = SocketChannel.open(isa);
Future<?> f = exec.submit(new NIOBlocked(sc1));
exec.execute(new NIOBlocked(sc2));
exec.shutdown();

TimeUnit.SECONDS.sleep(1);
// Produce an interrupt via cancel;
f.cancel(true);

TimeUnit.SECONDS.sleep(1);
// Release the block by closing the channel
sc2.close();
}
}

class NIOBlocked implements Runnable {
private final SocketChannel sc;

public NIOBlocked(SocketChannel sc) {
this.sc = sc;
}

@Override
public void run() {
try {
System.out.println("Waiting for read() in " + this);
sc.read(ByteBuffer.allocate(1));
} catch (ClosedByInterruptException e) {
System.out.println("ClosedByInterruptException");
} catch (AsynchronousCloseException e) {
System.out.println("AsynchronousCloseException");
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("Exiting NIOBlocked.run() " + this);
}
}

// Waiting for read() in org.jz.c23.NIOBlocked@2bc68519
// Waiting for read() in org.jz.c23.NIOBlocked@50b0c91f
// ClosedByInterruptException
// Exiting NIOBlocked.run() org.jz.c23.NIOBlocked@2bc68519
// AsynchronousCloseException
// Exiting NIOBlocked.run() org.jz.c23.NIOBlocked@50b0c91f

Blocked by a mutex

从 Interrupting.java 的例子可以看到,如果我们调用一个对象的 synchronized 方法,如果该方法的 lock 已经被获取了,那么这个 task 会 block 并等到 lock 被释放后再调用。下面的例子展示了同一个 task 如何多次获取同一个对象的锁

MultiLock 有两个方法 f, g 分别调用对方,并用 synchronized 关键字修饰。每次方法被调用时,方法的实例都会被 lock 一次。

举一个

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
public class MultiLock {
public synchronized void f1(int count) {
if (count-- > 0) {
System.out.println("f1() calling f2() with count " + count);
f2(count);
}
}

public synchronized void f2(int count) {
if (count-- > 0) {
System.out.println("f2() calling f1() with count " + count);
f1(count);
}
}

public static void main(String[] args) {
final MultiLock multiLock = new MultiLock();
new Thread(() -> multiLock.f1(10)).start();
}
}
// f1() calling f2() with count 9
// f2() calling f1() with count 8
// f1() calling f2() with count 7
// f2() calling f1() with count 6
// f1() calling f2() with count 5
// f2() calling f1() with count 4
// f1() calling f2() with count 3
// f2() calling f1() with count 2
// f1() calling f2() with count 1
// f2() calling f1() with count 0

这个例子的说明不是很懂,但是大概就是 concurrency lib 的 RenntrantLocks 提供了一种打断机制

BlockedMutex 持有 ReentrantLock 变量,并在构造函数中 lock,提供 f() 方法,调用 lock.lockInterruptibly(); 由于构造中的 lock,这个方法会一直 block。

Blocked2 新建 BlockedMutex 对象,导致上锁。然后调用 f() 被 block。

Interrupting2 在启动 Blocked2 之后 1s 进行打断,interrupt 成功

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
public class Interrupting2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Blocked2());
t.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("Issuing t.interrupt()");
t.interrupt();
}
}

class BlockedMutex {
private Lock lock = new ReentrantLock();

public BlockedMutex() {
// Acquire it right away, to demonstrate interruption of a task blocked on a ReentrantLock
lock.lock();
}

public void f() {
try {
// This will never be available to a second task
lock.lockInterruptibly();
System.out.println("lock acquired in f()");
} catch (InterruptedException e) {
System.out.println("Interrupted from lock acquisition in f()");
}
}
}

class Blocked2 implements Runnable {
BlockedMutex blocked = new BlockedMutex();

@Override
public void run() {
System.out.println("Waiting for f() in BlockedMutex");
blocked.f();
System.out.println("Broken out of blocked call");
}
}

// Waiting for f() in BlockedMutex
// Issuing t.interrupt()
// Interrupted from lock acquisition in f()
// Broken out of blocked call

Checking for an interrupt

TBD

Cooperation between tasks

通过之前的章节,我们知道可以通过互斥锁来控制多个 task 对一个资源的访问。

这章我们会学习如何通过内置方法,协调多个 task 之间对一个资源的调用。Object 提供了 wait()/notifyAll(),concurrent lib 提供了 await()/signal() 来完成这些功能。

wait() and notifyAll()

下面将 wait/notifyAll 应用在汽车打蜡的场景

Car 代表将会被 lock 的类, 提供了四个方法,使用 synchronized 修饰,分别是打蜡,抛光,等待上蜡,等待抛光。

WaxOn 代表上蜡的 task, 接收一个 car 做参数,在 run 中,打印状态并将 car 的 flag 置位 true,等待抛光

WaxOff 代表抛光的 task, 接收一个 car 做参数,在 run 中,打印状态并将 car 的 flag 置位 false, 并等待打蜡

WaxOMatic 新建 car 对象,并启动两个 task 让他们轮流操作 car, 并在一定时间后结束操作

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public class WaxOMatic {
public static void main(String[] args) throws InterruptedException {
Car car = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOff(car));
exec.execute(new WaxOn(car));
TimeUnit.SECONDS.sleep(2);
exec.shutdownNow();
}
}

class Car {
private boolean waxOn = false;

public synchronized void waxed() {
waxOn = true; // Ready to buff
notifyAll();
}

public synchronized void buffed() {
waxOn = false; // Ready for another coat of wax
notifyAll();
}

public synchronized void waitForWaxing() throws InterruptedException {
while (waxOn == false)
wait();
}

public synchronized void waitForBuffing() throws InterruptedException {
while (waxOn == true)
wait();
}
}

class WaxOn implements Runnable {
private Car car;

public WaxOn(Car c) {
car = c;
}

@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println("Wax On!");
TimeUnit.MILLISECONDS.sleep(200);
car.waxed();
car.waitForBuffing();
}
} catch (InterruptedException e) {
System.out.println("Exiting via interrupt");
}
System.out.println("Ending Wax On task");
}
}

class WaxOff implements Runnable {
private Car car;

public WaxOff(Car c) {
car = c;
}

@Override
public void run() {
try {
while (!Thread.interrupted()) {
car.waitForWaxing();
System.out.println("Wax Off!");
TimeUnit.MILLISECONDS.sleep(200);
car.buffed();
}
} catch (InterruptedException e) {
System.out.println("Exiting via interrupt");
}
System.out.println("Ending Wax Off task");


}
}
// Wax On!
// Wax Off!
// Wax On!
// Wax Off!
// Wax On!
// Wax Off!
// Wax On!
// Wax Off!
// Wax On!
// Wax Off!
// Exiting via interrupt
// Ending Wax Off task
// Exiting via interrupt
// Ending Wax On task

上面的例子中需要强调的一点是,你必须将 wait() 用 while 包裹起来,因为

  • 多个 task 等待同一个 lock 时,前面的 task 可能会改变某些条件,当前的 task 需要 block 住知道条件允许
  • 当前 task 唤醒后,可能条件不允许,他要继续等待
  • 当前 task 唤醒后,可能操作的对象还在 block 中,那它就要继续 wait

Missed Singals

当两个 task 在通过 notify()/wait() 或者 notifyAll()/wait() 协调工作时,有可能错过一些指令,比如下面的例子

会组织 T2 调用 wait

1
2
3
4
5
6
7
8
9
10
11
12
13
14
T1: 
synchronized(sharedMonitor) {
<setup condition for T2>
sharedMonitor.notify();
}

T2:
while(someCondition) {
// Point 1
synchronized(sharedMonitor)
{
sharedMonitor.wait();
}
}

假设 T2 通过了 someCondition 的验证,在 Point1 时,切换到 T1,然后 T1 拿到锁并运行就结束,发出 notify() 这时切换回 T2 继续运行,发现 T2 一直等待,就死锁了。

T2 的正确写法应该是

1
2
3
4
synchronized(sharedMonitor) {
while(someCondition)
sharedMonitor.wait();
}

如果 T1 先运行,当返回到 T2 时,回判断 condition 不满足,将不会进入等待状态。相反,当 T2 先运行,他会进如 wait, 等待 T1 唤醒

PS: 看的很迷,我的逻辑应该是错的,但是我放弃思考了。。。

notify() vs notifyAll()

notify 是 notifyAll 的一个优化,由于 notify 只会唤醒一个 task. 如果你要使用它,请确保,在你调用的时候只有想要调用的那个 task 是处于等待状态的。

notifyAll() 并不会 wake up “all waiting tasks”, only the tasks that are waiting on a particular lock are awoken when notifyAll() is called/or that lock

实验说明如下:

Blocker 是操作的对象类,提供三个方法,waitingCall 用来停留在 wait() 状态,prod/prodAll 分别是唤醒单个和唤醒全部线程。

Task1/2 分别是两个 task, 功能一样,唯一的作用是提供提供两个操作类进行实验。

NotifyVsNotifyAll 为 client 类,先启动 5 个 Task1,再启动 1 个 Task2. 6 个线程都停留在 wait 状态。然后通过 timer 分别出发 Task1 的 prod 和 prodAll 方法,从输出可以看到,当调用 notify 时只有一个 Task1 被唤醒,当调用 notifyAll 时,所有 Task1 都醒了,Task2 毫无反应。只有最后结束时调用了 Task2 的 prodAll 时,Task2 对应的线程被唤醒。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class NotifyVsNotifyAll {
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new Task());
}
exec.execute(new Task2());
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
boolean prod = true;

@Override
public void run() {
if (prod) {
System.out.println("\nnotify() ");
Task.blocker.prod();
prod = false;
} else {
System.out.println("\nnotifyAll() ");
Task.blocker.prodAll();
prod = true;
}
}
}, 400, 400);
TimeUnit.SECONDS.sleep(5);
timer.cancel();
System.out.println("\nTimer canceled");
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("Task2.blocker.prodAll() ");
Task2.blocker.prodAll();
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("\nShutting down");
exec.shutdownNow();// Interrupt all tasks
}
}

class Blocker {
synchronized void waitingCall() {
try {
while (!Thread.interrupted()) {
wait();
System.out.println(Thread.currentThread() + " ");
}
} catch (InterruptedException e) {
// OK to exit this way
}
}

synchronized void prod() {
notify();
}

synchronized void prodAll() {
notifyAll();
}
}

class Task implements Runnable {
static Blocker blocker = new Blocker();

@Override
public void run() {
blocker.waitingCall();
}
}

class Task2 implements Runnable {
// A separate Blocker object:
static Blocker blocker = new Blocker();

@Override
public void run() {
blocker.waitingCall();
}
}
// notify()
// Thread[pool-1-thread-1,5,main]

// notifyAll()
// Thread[pool-1-thread-2,5,main]
// Thread[pool-1-thread-1,5,main]
// Thread[pool-1-thread-5,5,main]
// Thread[pool-1-thread-4,5,main]
// Thread[pool-1-thread-3,5,main]

// ....

// Timer canceled
// Task2.blocker.prodAll()
// Thread[pool-1-thread-6,5,main]

// Shutting down

暂时就先看到这儿把,耐心已经磨光了,以后有动力了再接着看

公司使用 GitHub 企业版管理代码,我自己也时常会需要在公用版 Github 上更新一些代码,经常要交互使用。2021-08-13 的时候,GitHub 官方静止了 password 类型提交代码,这不只能找找怎么本地配置双账号了,解决方案如下

前置,初始化配置:

  1. 删除 ~/.gitconfig 中的账户信息
  2. 删除 ~/.ssh 下的配置信息

配置:

  1. cd 到 ~/.ssh 目录下运行 ssh-keygen -t rsa -C "your_email@example.com" 生成公私密码,之前参考了官方文档指定类型 ed25519, 不知道为啥,配置不生效
  2. 在提示 Enter a file in which to save the key (/Users/you/.ssh/id_rsa): [Press enter] 时添加后缀指定环境, 我本地的配置情况 id_rsa_github id_rsa_github.pub id_rsa_sap id_rsa_sap.pub
  3. 后面直接回车到文件生成成功。
  4. 运行 eval "$(ssh-agent -s)" 启动代理并运行 ssh-add ~/.ssh/id_rsa_githubssh-add ~/.ssh/id_rsa_sap 将私钥添加到密钥链中。通过 ssh-add -l 可以查看已经添加的 key 信息. PS: 如果你是 Mac 系统需要加一个 -K 不然重启之后 key 就没了 ssh-add -K ~/.ssh/id_rsa_github
  5. 在 ~/.ssh/config 文件中配置账号,网站的对应关系, 示例如下
  6. 打开 github 网站,将 id_rsa_github.pub 这个公钥信息添加到目标网站 头像 -> Settings -> SSH and GPG keys -> New SSH key
  7. 测试配置是否成功,终端输入 ssh -T git@github.comssh -T git@github.wdf.sap.corp 看提示信息是否正确
  8. 配置 commit 账号,如果没有设置,commit 的时候, git 会用电脑主机号作为提交账号。我本地是将常用的公司账号信息通过 git config –global 配置为全局信息,在自己的 repo 中通过 git config –local 单独配置
1
2
3
4
5
6
7
8
9
Host github
HostName github.com
User jack-zheng
IdentityFile ~/.ssh/id_rsa_github

Host gitlab
HostName github.wdf.sap.corp
User Ixxxx
IdentityFile ~/.ssh/id_rsa_sap

参考

目的:保证一个类只有一个实例,并提供一个访问他的全局访问点

关键代码:

  • 构造函数私有化
  • 私有静态变量
  • 对外的静态方法

介绍几种单例模式的实现方式 - 有种回字的几种写法的意思,略无聊

懒汉式 - 线程不安全

1
2
3
4
5
6
7
8
9
10
11
public class Singleton01 {
private static Singleton01 instance;
private Singleton01(){}

public static Singleton01 getInstance() {
if (instance == null) {
instance = new Singleton01();
}
return instance;
}
}

简单易懂,但是线程不安全

懒汉式 - 线程安全

1
2
3
4
5
6
7
8
9
10
11
public class Singleton02 {
private static Singleton02 instance;
private Singleton02() {}

public static synchronized Singleton02 getInstance() {
if (instance == null) {
instance = new Singleton02();
}
return instance;
}
}

实现简单,线程安全,方法体加锁比较耗资源,当 getInstance() 调用不频繁时可以使用

饿汉式 - 线程安全

1
2
3
4
5
6
7
8
public class Singleton03 {
private static Singleton03 instance = new Singleton03();
private Singleton03() {}

public static Singleton03 getInstance() {
return instance;
}
}

实现简单,线程安全,通过 classloader 避免同步问题,缺点是类加载就初始化,浪费内存

双检锁/双重校验锁 DCL double-checked locking

注意 volatile 的使用,避免指令重排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton04 {
private volatile static Singleton04 instance;
private Singleton04() {}

public static Singleton04 getInstance() {
if (instance == null) {
synchronized (Singleton04.class) {
if (instance == null) {
instance = new Singleton04();
}
}
}
return instance;
}
}

略显复杂,但是性能良好效率高,懒加载,线程安全 Java 1.5 后有效。

登记式/静态内部类

1
2
3
4
5
6
7
8
9
10
public class Singleton05 {
private static class SingletonHolder {
private static final Singleton05 INSTANCE = new Singleton05();
}
private Singleton05() {}

public static Singleton05 getInstance() {
return SingletonHolder.INSTANCE;
}
}

实现简单,线程安全,懒加载。效果和双检锁一致。但是由于只在被使用时才通过 classloader 加载,效率回更高

枚举

1
2
3
4
public enum Singleton06 {
INSTANCE;
public void whateverMethod() {}
}

理论上来说最安全,最简单的实现,不过不流行。 Effective Java 作者推荐的写法

The Facade Pattern provides a unified interface to a set of interfaces in a subsytem. Facade defines a higher-level interface that makes the subsystem easier to use.
提供一套更 high-level 的接口简化子系统调用

Facade(外观) Pattern

假设我们要组一套家庭影院,我们有好多设备,比如投影仪,音响,爆米花机,DVD 等。我们每次想要看一场电影需要做如下事情

  1. 开启 爆米花 机
  2. 开始爆米花
  3. 开启影响
  4. 设置音量
  5. 开启投影仪
  6. 摄制亮度
  7. 开启 DVD
  8. 塞入光盘

而且等我们看完了,我们还需要逐个将上面的设备关掉,一套下来,可能以后再也不看电影了。

Facade 模式就是用来解决这种问题的。

A facade not only simplifies an interface, it decouples a client from a subsystem of components.
Facades and adapters may wrap multiple classes, but a facade’s intent is to simplify, while an adapter’s is to convert the interface to something different.
外观模式不仅仅是简化接口,同时他还将子系统和客户端解耦了
Facade 和 Adapter 都会在类外面包一层,但是 Facade 是为了简化,而 Adapter 是为了转换

为了简化代码,我们一拿 DVD 和投影仪举例

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
51
52
53
54
55
56
57
58
59
60
61
62
// Player 类表示 DVD 机的开/关/放电影功能
public class DvdPlayer {
public void startPlayer() {
System.out.println("Start DVD Player...");
}

public void endPlayer() {
System.out.println("End DVD Player...");
}

public void playMovie(String name) {
System.out.println("Show movie: " + name + " ...");
}
}

// 表示屏幕功能
public class Screen {
public void downScreen() {
System.out.println("Down screen...");
}

public void upScreen() {
System.out.println("Up screen...");
}
}

// 家庭影院简化版
public class HomeTheaterFacade {
private Screen screen;
private DvdPlayer player;

public HomeTheaterFacade(Screen screen, DvdPlayer player) {
this.screen = screen;
this.player = player;
}

public void startMovie(String name) {
screen.downScreen();
player.startPlayer();
player.playMovie(name);
}

public void endMovie() {
screen.upScreen();
player.endPlayer();
}
}

// 客户端播放和结束放映
public class Client {
public static void main(String[] args) {
HomeTheaterFacade facade = new HomeTheaterFacade(new Screen(), new DvdPlayer());
facade.startMovie("<<NeZha>>");
facade.endMovie();
}
}

// Down screen...
// Start DVD Player...
// Show movie: <<NeZha>> ...
// Up screen...
// End DVD Player...

其实说是 Facade 模式,但是我这里平时经常会用到,只不过我一般把这种类型的东西叫做 Util 或者 Action 类。封装一些经常使用的方法,感觉效果上还是很相似的。

The Principle of Least Knowledge

这个规则是说,我们在写代码的时候要尽量减少涉及到多种返回值类型的链式调用。

Principle of Least Knowledge - talk only to your immediate friends.

在你的代码中,你只能调用下列对象的方法:

  • 对象本身
  • 通过方法参数传入的对象
  • 任何在本类中创建的对象
  • 任何本对象的 field

这样做可以减少两个对象之间的 dependencies 但是同时也有一个弊端,你需要写跟多的代码,项目会变得更大,还可能会性能下降。

Chapter 8 explains about loaders. A loader is an important Catalina module responsible for loading servlet and other classes that a web application uses. This chapter also shows how application reloading is achieved.

之前章节我们已经给出了一个简单的 loader 实现用于加载 servlet。这章我们将介绍 tomcat 的 standard web application loader. servlet container 必须实现自己的 loader,而不能使用系统自带的那个。因为它不信任运行的 servlets。如果它像我们之前的例子那样使用默认的类加载器,那么 servlet 将可以访问任何 JVM classpath 下的 class 和 lib,这和 security 的规则相违背。

一个 servlet 只允许加载 WEB-INF/classes 和 WEB-INF/lib 文件夹下的内容, 那个 web application(context) 需要有它自己的 loader。Catalina 中,org.apache.catalina.Loader 表示 loader 类。

另一个 tomcat 需要自己的 loader 的原因是它需要支持自动加载的功能。当 WEB-INF/classes 和 lib 下的内容发生改变时,这个 loader 需要自动检测并重新加载。Tomcat 新起一个线程完成这个功能, org.apache.catalina.loader.Reloader 即代表了 reload 这个行为。

本章第一部分介绍 Java 中的类加载机制。之后介绍 Loader 接口,最后演示 tomcat 的 loader 使用案例

本章中两个术语 repository 表示 class loader 会搜索的地方,resources 表示 DirContext,它指向 context 的 document 目录。

Java Class Loader

查看 深入理解 Java 虚拟机 第 7,9 章节,说的很清楚了。

Java 允许你定制自己的 class loader,只需继承 java.lang.ClassLoader 即可。以下是 tomcat 需要定制 loader 的原因

  • To specify certain rules in loading classes.
  • To cache the previously loaded classes.
  • To pre-load classes so they are ready to use.

The Loader Interface

The Reloader Interface

为了提供自动重载的功能,loader 必须实现 org.apache.catalina.loader.Reloader 接口

1
2
3
4
5
6
7
8
public interface Reloader {

public void addRepository(String repository);

public String[] findRepositories();

public boolean modified();
}

其中最重要的是 modified() 方法,当应用中的 servlet 或者 supporting classes 有改动时,他会返回 true。

The WebappLoader Class

WebappLoader 是 Loader 接口的一个实现,他代表一个 web application 负责为这个应用加载 class。WebappLoader 会创建一个 WebappClassLoader 作为他的类加载器。和其他 Catalina 组件一样,WebappLoader 也实现了 Lifecycle 和 Runnable 接口。前者借由相关组件控制开启停止,后者可以通过多线程实现类的重载。class 重载由 Context 执行,而不是 WebappLoader, 细节将在 Chapter 12 的 StandardContext 介绍。

WebappLoader 中的主要方任务:

  • Creating a class loader
  • Setting repositories
  • Setting the class path
  • Setting permissions
  • Starting a new thread for auto-reload

Create A Class Loader

WebappLoader 将类加载委托给了一个内部的类加载器,外部并不能直接直接创建这个加载器。但是可以通过 getClassLoader() 拿到它。如果你想要指定自己的应用加载器,可以通过 setLoaderClass() 方法传入加载器全路径,需要注意的是,加载器最终是通过 createClassLoader() 方法创建的,所以自定义的类加载器必须继承自 WebappClassLoader 不然会抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private WebappClassLoader createClassLoader() throws Exception {

Class clazz = Class.forName(loaderClass);
WebappClassLoader classLoader = null;

if (parentClassLoader == null) {
// Will cause a ClassCast is the class does not extend WCL, but
// this is on purpose (the exception will be caught and rethrown)
classLoader = (WebappClassLoader) clazz.newInstance();
} else {
Class[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoader) constr.newInstance(args);
}

return classLoader;
}

Setting Repositories

WebappLoader 的 start() 方法中调用 setRepsitories 向 class loader 中添加 repositories。WEB-INF/classes 传给了 addRepository(),WEB-INF/lib 传给了 setJarPath()。

Setting the Class Path

这个 task 通过在 start() 方法中调用 setClassPath() 实现。

Setting a New Thread for Auto-Reload

当 WEB-INF/classes 或者 WEB-INF/lib 下的文件被修改了,修改的类需要在 Tomcat 不重启的情况下自动刷新。为了达到这个效果,WebappLoader 新启了一个线程,周期性的检查文件的时间戳,默认检查周期为 15s, 用户可以通过 get/setCheckinterval() 设置这个值。

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
public void run() {
if (debug >= 1)
log("BACKGROUND THREAD Starting");

// Loop until the termination semaphore is set
while (!threadDone) {

// Wait for our check interval
threadSleep();

if (!started)
break;

try {
// Perform our modification check
if (!classLoader.modified())
continue;
} catch (Exception e) {
log(sm.getString("webappLoader.failModifiedCheck"), e);
continue;
}

// Handle a need for reloading
notifyContext();
break;

}

if (debug >= 1)
log("BACKGROUND THREAD Stopping");
}

PS: Tomcat5 中将这部分 task 移到 StandardContext 中的 backgroundProcess() 中去了

run() 中的核心方法是通过 while 循环定时检测 modified 的值,流程如下

  • Sleep 一定时间
  • 调用 modified() 查看 flag, 如果 false,continue
  • 返回 true,调用 notifyContext() 方法,通过 Context 做 reload
1
2
3
4
private void notifyContext() {
WebappContextNotifier notifier = new WebappContextNotifier();
(new Thread(notifier)).start();
}

notifyContext 并没有直接调用 Context 的 relaod 方法,而是通过启动内部类 WebappContextNotifier 线程做的。

1
2
3
4
5
protected class WebappContextNotifier implements Runnable {
public void run() {
((Context) container).reload();
}
}

The WebappClassLoader Class

WebappClassLoader 继承自 URLClassLoader,在实现时兼顾了性能和安全。性能方面,它会将之前还在的类 cache 一下,当需要加载类时先从 cache 中寻在,找不到在通过加载器加载。之前加载失败的也有对应的 cache.

安全方面,WebappClassLoader 有一个黑名单,阻止加载一些类

1
2
3
4
5
6
7
8
9
10
11
private static final String[] triggers = {
"javax.servlet.Servlet"
};

private static final String[] packageTriggers = {
"javax", // Java extensions
"org.xml.sax", // SAX 1 & 2
"org.w3c.dom", // DOM 1 & 2
"org.apache.xerces", // Xerces 1 & 2
"org.apache.xalan" // Xalan
};

Caching

为了性能考虑,加载的类会被 cache 住,后面 class 加载时,会先从 cache 中拿。Caching 是通过 WebappClassLoader 中的 local cache 实现的,同时之前加载过的类通过 ClassLoader 中的 Vector 管理避免类被垃圾回收掉。

能被 WebappClassLoader 加载的类统称为 resource,通过 org.apache.catalina.loader.ResourceEntry 表示。ResourceEntry 会持有类的 byte 数据,最后修改日期,Manifest 等。

1
2
3
4
5
6
7
8
9
public class ResourceEntry {
public long lastModified = -1;
public byte[] binaryContent = null;
public Class loadedClass = null;
public URL source = null;
public URL codeBase = null;
public Manifest manifest = null;
public Certificate[] certificates = null;
}

cached resources 存在名为 resourceEntries 的 HashMap 中,以 resource name 作为 key. 没有找到的 resource 存在名为 notFoundResources 的 HashMap 中。

Loading Classes

下面是 WebappClassLoader 加载 class 的规则

  • All previously loaded classes are cached, so first check the local cache.
  • If not found in the local cache, check in the cache, i.e. by calling the findLoadedClass of the java.lang.ClassLoader class.
  • If not found in both caches, use the system’s class loader to prevent the web application from overriding J2EE class.
  • If SecurityManager is used, check if the class is allowed to be loaded. If the class is not allowed, throw a ClassNotFoundException.
  • If the delegate flag is on or if the class to be loaded belongs to the package name in the package trigger, use the parent class loader to load the class. If the parent class loader is null, use the system class loader.
  • Load the class from the current repositories.
  • If the class is not found in the current repositories, and if the delegate flag is not on, use the parent class loader. If the parent class loader is null, use the system class loader.
  • If the class is still not found, throw a ClassNotFoundException.

The Application

本章使用现成的 StandardContext 管理上下文,12 章会具体介绍,目前你只需要知道 StandardContext 会结合 listener 处理 event 即可。

本章自定义的 listener

1
2
3
4
5
6
7
8
9
public class SimpleContextConfig implements LifecycleListener {

public void lifecycleEvent(LifecycleEvent event) {
if (Lifecycle.START_EVENT.equals(event.getType())) {
Context context = (Context) event.getLifecycle();
context.setConfigured(true);
}
}
}

我们创建 StandardContext 和 SimpleContextConfig 的实例并将 SimpleContextConfig 注册到 StandardContext 中。

同时我们复用前面章节的 SimplePipeline, SimpleWrapper 和 SimpleWrapperValve.

由于使用了 StandardContext 我们必须将测试 servlet 放到 WEB-INF/classes 下,这个例子中,我们创建一个新的目录 myApp 并创建对应的目录,通过 System.setProperty("catalina.base", System.getProperty("user.dir")); 指定 myApp 文件夹。

简单过一下 Bootstrap 的代码

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
51
52
public final class Bootstrap {
public static void main(String[] args) {

//invoke: http://localhost:8080/Modern or http://localhost:8080/Primitive

System.setProperty("catalina.base", System.getProperty("user.dir"));
Connector connector = new HttpConnector();
Wrapper wrapper1 = new SimpleWrapper();
// ... 设置 wrapper

Context context = new StandardContext();
// StandardContext's start method adds a default mapper
context.setPath("/myApp");
context.setDocBase("myApp");

context.addChild(wrapper1);
context.addChild(wrapper2);

// context.addServletMapping()...
// add ContextConfig. This listener is important because it configures
// StandardContext (sets configured to true), otherwise StandardContext
// won't start
LifecycleListener listener = new SimpleContextConfig();
((Lifecycle) context).addLifecycleListener(listener);

// here is our loader
Loader loader = new WebappLoader();
// associate the loader with the Context
context.setLoader(loader);

connector.setContainer(context);

try {
connector.initialize();
((Lifecycle) connector).start();
((Lifecycle) context).start();
// now we want to know some details about WebappLoader
WebappClassLoader classLoader = (WebappClassLoader) loader.getClassLoader();
System.out.println("Resources' docBase: " + ((ProxyDirContext) classLoader.getResources()).getDocBase());
String[] repositories = classLoader.findRepositories();
for (int i = 0; i < repositories.length; i++) {
System.out.println(" repository: " + repositories[i]);
}

// make the application wait until we press a key.
System.in.read();
((Lifecycle) context).stop();
} catch (Exception e) {
e.printStackTrace();
}
}
}

整理一下思路:这张讲的内容调用点在 SimpleWrapper 的 loadServlet() 方法中。当访问页面时,最终到这个方法中,从 context 中拿到 class loader 并通过 classLoader.loadClass(cls) 加载类。这个 loader 和 classLoader 就是本章中的 WebappLoader 和 WebappClassLoader.

看完了感觉和 JVM 那边看到的有出入,很多自定义的 loader 都没讲到,热加载也没讲到,往后看看再说。

Chapter 7 covers loggers, which are components used for recording error messages and other messages.

分三节介绍 Catalina 中的 log 机制。第一部分介绍 Logger 接口,Catalina 中所有的 logger 都要实现的。第二节介绍 Tomcat 中的 loggers。第三节介绍本章中使用案例。

The Logger Interface

理论上来说,Logger 可以 attach 到任何 container,但是实际操作中,我们基本只会 attach 到 Context level 以上的 container 中。

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
public interface Logger {
public static final int FATAL = Integer.MIN_VALUE; // 严重的
public static final int ERROR = 1;
public static final int WARNING = 2;
public static final int INFORMATION = 3;
public static final int DEBUG = 4;

public Container getContainer();

public void setContainer(Container container);

public String getInfo();

public int getVerbosity();

public void setVerbosity(int verbosity);

public void addPropertyChangeListener(PropertyChangeListener listener);

// ------- 一系列重载的 log 方法,有些还支持 verbosity 设置,如果传入的 level 比当前的小就会被记载
public void log(String message);

public void log(Exception exception, String msg);

public void log(String message, Throwable throwable);

public void log(String message, int verbosity);

public void log(String message, Throwable throwable, int verbosity);

public void removePropertyChangeListener(PropertyChangeListener listener);
}

提供了很多 log 方法,最简单的只需要一个 string 参数即可。定义了五种 log 级别,还有 container 相关的方法将 logger 实体和 container 结合起来。

Tomcat’s Loggers

Tomcat 自带的 logger 有 FileLogger,SystemErrLogger 和 SystemOutLogger. 他们都继承自 LoggerBase 类, LoggerBase 实现了 Logger 接口

The LoggerBase Class

Tomcat5 的 LoggerBase 由于结合了 MBeans 的功能,所以变得有些复杂,在 20 章会介绍。这里用 Tomcat4 的做例子。

Tomcat4 中的 LoggerBase 实现了除 log(string) 外的所有接口方法,这个方法的实现放在子类中完成。默认的 verbosity level 是 ERROR。可以通过 setVerbosity() 来改变这个值。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public abstract class LoggerBase implements Logger {
protected Container container = null;
protected int debug = 0;
protected static final String info = "org.apache.catalina.logger.LoggerBase/1.0";
protected PropertyChangeSupport support = new PropertyChangeSupport(this);
protected int verbosity = ERROR;

public Container getContainer() {
return (container);
}

public void setContainer(Container container) {
Container oldContainer = this.container;
this.container = container;
support.firePropertyChange("container", oldContainer, this.container);
}

public int getDebug() {
return (this.debug);
}

public void setDebug(int debug) {
this.debug = debug;
}

public String getInfo() {
return (info);
}

public int getVerbosity() {
return (this.verbosity);
}

public void setVerbosity(int verbosity) {
this.verbosity = verbosity;
}

public void setVerbosityLevel(String verbosity) {
if ("FATAL".equalsIgnoreCase(verbosity))
this.verbosity = FATAL;
else if ("ERROR".equalsIgnoreCase(verbosity))
this.verbosity = ERROR;
else if ("WARNING".equalsIgnoreCase(verbosity))
this.verbosity = WARNING;
else if ("INFORMATION".equalsIgnoreCase(verbosity))
this.verbosity = INFORMATION;
else if ("DEBUG".equalsIgnoreCase(verbosity))
this.verbosity = DEBUG;
}

public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}

public abstract void log(String msg);

public void log(Exception exception, String msg) {
log(msg, exception);
}

public void log(String msg, Throwable throwable) {
CharArrayWriter buf = new CharArrayWriter();
PrintWriter writer = new PrintWriter(buf);
writer.println(msg);
throwable.printStackTrace(writer);
Throwable rootCause = null;
if (throwable instanceof LifecycleException)
rootCause = ((LifecycleException) throwable).getThrowable();
else if (throwable instanceof ServletException)
rootCause = ((ServletException) throwable).getRootCause();
if (rootCause != null) {
writer.println("----- Root Cause -----");
rootCause.printStackTrace(writer);
}
log(buf.toString());
}

public void log(String message, int verbosity) {
if (this.verbosity >= verbosity)
log(message);
}

public void log(String message, Throwable throwable, int verbosity) {
if (this.verbosity >= verbosity)
log(message, throwable);
}

public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
}

The SystemOutLogger Class

在它的实现中,所有 log 都会被输出到终端

1
2
3
4
5
6
7
public class SystemOutLogger extends LoggerBase {
protected static final String info = "org.apache.catalina.logger.SystemOutLogger/1.0";

public void log(String msg) {
System.out.println(msg);
}
}

The SystemErrLogger Class

与上面雷同,只不过用了 System.err.println(msg);

1
2
3
4
5
6
7
public class SystemErrLogger extends LoggerBase {
protected static final String info = "org.apache.catalina.logger.SystemErrLogger/1.0";

public void log(String msg) {
System.err.println(msg);
}
}

The FileLogger Class

FileLogger 是所有 tomcat 子类中最复杂的一个,他会将 log 记录到文件并附带时间戳信息。当初始化时他会创建一个附带当天日期的 log 文件,如果日期变了,他会创建一个新的文件保存日志。我们还可以自定义前缀后缀。

按天为单位常见新文件记录信息,允许指定前/后缀。Tomcat4 中这个类也实现了 Lifecycle 接口,这样我们实现了开启/停止服务的托管。在 Tomcat5 中,这个接口被移到父类去了。

PS: 这个类的 log 方法拿时间戳的方法挺有意思,以后同样的功能可以借鉴一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.sql.Timestamp;

public class TestFileLogger {
@Test
public void test() {
Timestamp ts = new Timestamp(System.currentTimeMillis());
System.out.println(ts);

String tsString = ts.toString().substring(0, 19);
System.out.println(tsString);

String tsDate = tsString.substring(0, 10);
System.out.println(tsDate);
}
}

// 2021-09-03 15:20:49.025
// 2021-09-03 15:20:49
// 2021-09-03

实现了 Lifecycle 接口之后,stop/start 方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FileLogger extends LoggerBase implements Lifecycle {
public void start() throws LifecycleException {
// Validate and update our current component state
if (started)
throw new LifecycleException(sm.getString("fileLogger.alreadyStarted"));
lifecycle.fireLifecycleEvent(START_EVENT, null);
started = true;
}

public void stop() throws LifecycleException {
// Validate and update our current component state
if (!started)
throw new LifecycleException(sm.getString("fileLogger.notStarted"));
lifecycle.fireLifecycleEvent(STOP_EVENT, null);
started = false;
close();
}
}

主要方法 log(String msg) 实现如下

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
public void log(String msg) {
// Construct the timestamp we will use, if requested
Timestamp ts = new Timestamp(System.currentTimeMillis());
String tsString = ts.toString().substring(0, 19);
String tsDate = tsString.substring(0, 10);

// If the date has changed, switch log files
if (!date.equals(tsDate)) {
synchronized (this) {
if (!date.equals(tsDate)) {
close();
date = tsDate;
open();
}
}
}

// Log this message, timestamped if necessary
if (writer != null) {
if (timestamp) {
writer.println(tsString + " " + msg);
} else {
writer.println(msg);
}
}
}

通常情况下 FileLogger 会操作多个文件,在操作下一个时会把当前的关掉。

The open method

先检查目录是否村子啊,没有就建一个。然后再创建 log 文件的 PrintWriter 并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void open() {

// Create the directory if necessary
File dir = new File(directory);
if (!dir.isAbsolute())
dir = new File(System.getProperty("catalina.base"), directory);
dir.mkdirs();

// Open the current log file
try {
String pathname = dir.getAbsolutePath() + File.separator +
prefix + date + suffix;
writer = new PrintWriter(new FileWriter(pathname, true), true);
} catch (IOException e) {
writer = null;
}

}

The close method

很简单,flush IO 流并重制变量

1
2
3
4
5
6
7
8
private void close() {
if (writer == null)
return;
writer.flush();
writer.close();
writer = null;
date = "";
}

The log method

这小节就是将之前贴的这些方法穿起来描述一下,log 中的工作流如下:

  • 调用 lang 包下的类和方法,拿到当前时间戳
  • 比较当前时间,如有必要则创建新的 log 文件
  • 根据 timestamp 这个 flag 决定是否在写 log 的时候加入时间戳
  • 写入 log,打完收工

The Application

测试代码和第 6 章基本一样,只是在 Bootstrap 部分添加了对 log 文件的定制

1
2
3
4
5
6
7
8
// ------ add logger --------
System.setProperty("catalina.base", System.getProperty("user.dir"));
FileLogger logger = new FileLogger();
logger.setPrefix("FileLog_");
logger.setSuffix(".txt");
logger.setTimestamp(true);
logger.setDirectory("webroot");
context.setLogger(logger);

访问 http://localhost:8080/Modern 后在 webroot 文件夹下面会生成定制的 log 文件 FileLog_2021-08-02.txt

这个案例运行的时候大概卡了快一分钟才执行完,看了 log 原来是有 class 找不到, 应该是我用的 jar 包已经是 Catalina 了,不是 tomcat 了,版本太高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2021-08-02 10:38:13 HttpProcessor[8080][4] process.invoke
java.lang.NoClassDefFoundError: org/apache/tomcat/util/log/SystemLogHandler
at org.apache.catalina.connector.RequestBase.recycle(RequestBase.java:562)
at org.apache.catalina.connector.HttpRequestBase.recycle(HttpRequestBase.java:417)
at org.apache.catalina.connector.http.HttpRequestImpl.recycle(HttpRequestImpl.java:195)
at org.apache.catalina.connector.http.HttpProcessor.process(HttpProcessor.java:1101)
at org.apache.catalina.connector.http.HttpProcessor.run(HttpProcessor.java:1151)
at java.lang.Thread.run(Thread.java:836)
Caused by: java.lang.ClassNotFoundException: org.apache.tomcat.util.log.SystemLogHandler
at java.net.URLClassLoader.findClass(URLClassLoader.java:444)
at java.lang.ClassLoader.loadClass(ClassLoader.java:480)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:384)
at java.lang.ClassLoader.loadClass(ClassLoader.java:413)
... 6 more

这个类被放到 tomcat-util 中去了,添加对应的 reference 到 pom 中即可解决

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/tomcat/tomcat-util -->
<dependency>
<groupId>tomcat</groupId>
<artifactId>tomcat-util</artifactId>
<version>4.1.31</version>
</dependency>

再后来,遇到了很多麻烦,最后将 Tomcat 源码整合到项目中了,所有问题都没了。。。