创建和运行线程

方法一,使用Thread

1
2
3
4
5
6
7
8
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();

例如:

1
2
3
4
5
6
7
8
9
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();

输出:

1
19:19:00 [t1] c.ThreadStarter - hello

方法二,使用Runnable配合Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程

  • Runnable 可运行的任务(线程要执行的代码)

1
2
3
4
5
6
7
8
9
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread(runnable);
// 启动线程
t.start();

例如:

1
2
3
4
5
6
7
8
9
10
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

输出:

1
19:19:00 [t2] c.ThreadStarter - hello

Java 8 以后可以使用 lambda 精简代码

1
2
3
4
5
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

方法三,FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

1
2
3
4
5
6
7
8
9
10
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

输出:

1
2
19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 结果是:100

小结

  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了

  • 用 Runnable 更容易与线程池等高级 API 配合

  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

观察多个线程同时运行,线程是交替执行的,谁先谁后,不由我们控制

查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程

  • tasklist 查看进程

  • taskkill 杀死进程

linux

  • ps -fe 查看所有进程

  • ps -fT -p 查看某个进程(PID)的所有线程

  • kill 杀死进程

  • top 按大写 H 切换是否显示线程

  • top -H -p 查看某个进程(PID)的所有线程

Java

  • jps 命令查看所有 Java 进程

  • jstack 查看某个 Java 进程(PID)的所有线程状态

  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置

  • 非认证远程连接应用
1
2
3
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.rmi.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证 -jar jar包

java -Djava.rmi.server.hostname=192.168.187.130 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8079 -Dcom.sun.management.jmxremote.rmi.port=8079 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false jar Test.jar
  • 使用密码远程连接应用

    • 进入$JAVA_HOME/conf/management/目录,java8是在 $JAVA_HOME/jre/lib/management目录

    • 参照目录下的jmxremote.password.template文件创建jmxremote.password

    • 在jmxremote.password新建用户名与密码

    • 在本目录的jmxremote.access文件为新增用户设置权限,这里我设置为只读
    • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600

    • 执行命令启动应用

    1
    java -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.password.file=jmxremote.password -jar Test.jar
  • 使用数字证书远程连接应用

    • 进入$JAVA_HOME/bin目录,java8是在 $JAVA_HOME/jre/bin目录

    • 执行如下命令生成客户端密钥对

    1
    2
    3
    4
    5
    6
    7
    keytool -genkeypair -alias jconsole -keyalg RSA -validity 365 -storetype pkcs12 -keystore jconsole.keystore -storepass 123456 -keypass 123456 -dname "CN=名称,OU=组织下属单位,O=组织名,L=城市,S=省份,C=CN"

    -alias jconsole 密钥对别名为jconsole
    -keystore jconsole.keystore 密钥库名称为jconsole.keystore
    -storepass 123456 存储密码为123456
    -keypass 123456 密钥密码为123456
    -dname 证书申请实体信息
    • 执行如下命令导出客户端证书
    1
    keytool -exportcert -alias jconsole -storetype pkcs12 -keystore jconsole.keystore -file jconsole.cer -storepass 123456
    • 查看生成结果,前2步骤顺利的话会在当前文件夹看到以下文件(客户端)
    • 将客户端证书传到应用服务端

    • 生成服务端应用的密钥对

    1
    keytool -genkeypair -alias app -keyalg RSA -validity 365 -storetype pkcs12 -keystore app.keystore -storepass 123456 -keypass 123456 -dname "CN=app名称,OU=app组织下属单位,O=app组织名,L=城市,S=省份,C=CN"
    • 导出服务端应用的证书
    1
    keytool -exportcert -alias app -storetype pkcs12 -keystore app.keystore -file app.cer -storepass 123456
    • 将服务端证书传到客户端

    • 执行命令将客户端证书导入到服务端的truststore(受信的密钥库)中

    1
    2
    3
    keytool -importcert -alias jconsole -file jconsole.cer -keystore app-ssl.truststore -storepass 123456 -noprompt

    -storepass 123456 受信密钥库密码为123456
    • 执行命令将服务端证书导入到客户端的truststore(受信的密钥库)中
    1
    keytool -importcert -alias app -file app.cer -keystore jconsole-ssl.truststore -storepass 123456 -noprompt
    • 设置应用启动脚本如下
    1
    2
    3
    4
    5
    6
    7
    8
    # java8
    java -Dcom.sun.management.jmxremote.port=8079 -Dcom.sun.management.jmxremote.rmi.port=8079 -Djava.rmi.server.hostname=${ip} -Dcom.sun.management.jmxremote.password.file=${path} -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.ssl=true -Dcom.sun.management.jmxremote.registry.ssl=true -Dcom.sun.management.jmxremote.ssl.need.client.auth=true -Djavax.net.ssl.trustStore=/home/iic/app-ssl.truststore -Djavax.net.ssl.trustStorePassword=123456 -Djavax.net.ssl.keyStore=${path} -Djavax.net.ssl.keyStorePassword=123456 -jar demo-0.0.1-SNAPSHOT.jar

    # java11
    java -Dcom.sun.management.jmxremote.port=8079 -Dcom.sun.management.jmxremote.rmi.port=8079 -Djava.rmi.server.hostname=${ip} -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=true -Dcom.sun.management.jmxremote.registry.ssl=true -Dcom.sun.management.jmxremote.ssl.need.client.auth=true -Djavax.net.ssl.trustStore=/home/iic/app-ssl.truststore -Djavax.net.ssl.trustStorePassword=123456 -Djavax.net.ssl.keyStore=${path} -Djavax.net.ssl.keyStorePassword=123456 -jar demo-0.0.1-SNAPSHOT.jar

    -Djavax.net.ssl.trustStore=${path} 受信密钥库路径
    -Djavax.net.ssl.trustStorePassword=123456 受信密钥库密码
    • 使用ssl连接远程服务,在jconsole目录执行以下命令
    1
    jconsole -J-Djavax.net.ssl.trustStore=F:\professional\java-cert\jconsole-ssl.truststore -J-Djavax.net.ssl.trustStorePassword=123456 -J-Djavax.net.ssl.keyStore=F:\professional\java-cert\jconsole.keystore -J-Djavax.net.ssl.keyStorePassword=123456

线程运行原理

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完

  • 垃圾回收

  • 有更高优先级的线程需要运行

  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

  • Context Switch 频繁发生会影响性能