1.创建线程和运行线程
1.1.方式一: 直接使用Thread线程对象创建线程
@Slf4j
public class TestThread {
public static void main(String[] args) {
//创建一个线程,并且指定线程名称为"t1"
Thread thread = new Thread("t1") {
@Override
public void run() {
//子线程中要执行的任务
log.info("子线程中要执行的任务");
}
};
//启动线程
thread.start();
//主线程要执行的任务
log.info("main线程要执行的任务");
}
}
可以看到,两个线程是同时执行的
1.2.方式二: 使用Runnable线程任务对象配合Thread线程对象创建线程
把线程和任务(即要执行的代码)分开,其中Thread代表线程,Runnable表示可运行的任务(线程要执行的代码);
@Slf4j
public class TestThread2 {
public static void main(String[] args) {
//创建子线程要执行的任务
Runnable runnable = new Runnable() {
@Override
public void run() {
log.info("子线程要执行的任务");
}
};
//创建线程对象,执行子线程中要执行的任务
Thread thread = new Thread(runnable,"t2");
//启动子线程
thread.start();
//主线程要执行的任务
log.info("主线程要执行的任务");
}
}
可以看到,两个线程也是同时执行的!
java8以后可以使用Lambda表达式来精简代码
@Slf4j
public class TestThread2 {
public static void main(String[] args) {
//创建线程要执行的任务对象
Runnable task1 = (()->{
log.info("子线程要执行的任务");
});
//创建线程对象
Thread t1 = new Thread(task1, "t1");
t1.start();
//主线程中要执行的任务
log.info("主线程要执行的任务");
}
}
两种方式对比:
①.方式一是把线程和任务合并在了一起;方式二是把线程和任务分开了;
②.用Runnable更容易与线程池等高级API配合;
③.用Runnable让任务类脱离了Thread继承体系,更灵活;
1.3.方式三: 使用FutureTask任务对象配合Thread线程对象创建线程
FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况;
@Slf4j
public class TestThread3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建任务对象,注意:泛型表示任务返回结果的类型
//构造函数的参数列表是一个callable接口,该接口里面的call()方法可以抛出异常,而runnable接口中的run()方法无法抛出异常;
FutureTask<Integer> task2 = new FutureTask<Integer>(() -> {
log.info("子线程要执行的任务");
return 1;
});
//创建线程对象
Thread t2 = new Thread(task2, "t2");
t2.start();
//主线程阻塞,同步等待task任务执行完毕的结果
Integer result = task2.get();
log.info("子线程任务的执行结果是:{}", result);
}
}
2.查看进程线程的方法
2.1.windows平台
1>.任务管理器可以查看进程和线程数,也可以用来杀死进程;
2>.查看进程
tasklist
3>.进程过滤
tasklist | findstr <关键字>
4.kill某个进程
taskkill <PID>
或者
taskkill /F /<PID>
2.2.Linux平台
1>. 查看所有的进程
# ps -ef
2>.进程过滤
# ps -ef | grep <关键字>
3>.查看某个进程(PID)的所有线程
# ps -fT -p <PID>
4>.kill某个进程
# kill <PID>
5>.动态显示进程信息,按大写H切换是否显示线程
# top
6>.查看某个进程(PID)的所有线程
# top -H -p <PID>
2.3.java环境中查看进程
1>.命令查看所有java进程
jps
2>.查看某个正在运行的Java进程(PID)的所有线程状态(命令行界面)
jstack <PID>
3>.查看某个Java进程(PID)中线程的运行情况(图形界面)
jconsole <PID>
2.4.使用jconsole工具进行远程进程监控配置
1>.运行Java类:
# java -Djava.rmi.server.hostname='远程通信IP地址' -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port='远程通信端口' -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证java类
2>.修改"/etc/hosts"文件将"127.0.0.1"映射至主机名;
3>.在jconsole连接页面选择远程连接,然后配置远程进程通信IP地址和端口
IP地址:端口
注意: 如果要认证访问,还需要做如下步骤:
–1).复制jmxremote.password文件;
–2).修改jmxremote.password和jmxremote.access文件的权限为600,即文件所有者可读写;
–3).连接时填入controlRole(用户名),R&D(密码);
3.线程运行原理
3.1.栈与栈帧
Java中的栈stack其实就是Java Virtual Machine Stacks (Java虚拟机栈)!
1>.我们都知道JVM中有堆、栈、方法区等内存区域,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存;
①.每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存;
- 线程每调用一次方法(之前)就会为这个方法产生一块栈帧内存,当方法执行完毕,方法对应的栈帧内存就会被释放/回收;
②.
每个线程(同一个线程内)只能有一个活动栈帧(Frame),对应着当前正在执行的那个方法;
- 线程每次只能执行/调用一个方法;
2>.案例
①.代码
public class TestFrames {
public static void main(String[] args) {
//调用方法method1
//线程在进行方法调用的时候会通过栈帧中的"返回地址"记录将要被调用的这个方法的起始/入口位置,等到被调用的方法执行完毕,然后根据"返回地址"回到方法调用的起始/入口位置,之后继续执行下一行代码;
method1(1); //这一行代码添加一个断点
}
public static void method1(int x) {
int y = x + 1;
//调用方法method2
Object o = method2();
System.out.println(o);
}
public static Object method2() {
Object o = new Object();
return o;
}
}
②.Debug调试程序
可以看到栈是一种先进后出的数据结构!
3>.栈帧图解
3.2.多线程
1>.代码如下:
public class TestFrames2 {
public static void main(String[] args) {
//创建一个子线程
new Thread("t1") {
@Override
public void run() {
method1(10); //在这里添加断点,并且右键断点将类型改成Thread
}
}.start();
//主线程调用method1
method1(10); //这里也添加一个断点,并且右键断点将类型改成Thread
}
public static void method1(int x) {
int y = x + 1;
Object o = method2();
System.out.println(o);
}
public static Object method2() {
Object o = new Object();
return o;
}
}
2>.Debug调试程序
分别调试两个线程,将某个线程的代码执行完毕,观察两个线程生成的栈帧.可以发现,某一个线程里面的代码都执行完毕之后,它生成的所有栈帧全部释放了,而另外一个代码还没有执行完毕的线程生成的栈帧依然存在,也就是说线程生成的栈帧是相互独立的!!!
3.3.线程的上下文切换(Thread Context Switch)
每个线程只能分配到CPU短时间内的执行权,CPU时间片用完之后,那么CPU的执行权就要交出来,这个线程就处于挂起状态(Runnable),等待再次分配CPU时间片,线程从使用CPU到不使用CPU,这就是一次线程的上下文切换;
1>.因为以下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码;
①.线程的CPU时间片用完;
②.垃圾回收;
- 垃圾回收会暂停所有的工作线程,开启垃圾回收线程进行垃圾回收;
③.有更高优先级的线程需要运行;
- 如果发现有更高优先级的线程需要运行,那么当前线程就要交出CPU执行权,让优先级更高的线程先执行;
④.线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法;
2>.当线程上下文切换发生时,需要由操作系统保存当前线程的状态(类似于断点续传),并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,程序计数器是线程私有的;
①.状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等;
②. 频繁Context Switch会影响性能;
4.线程中常见的方法
4.1.概述
4.2.start与run方法
1>.代码如下:
@Slf4j
public class TestRun {
public static void main(String[] args) {
//创建一个子线程
Thread thread = new Thread("t1") {
@Override
public void run() {
log.info("running...");
}
};
//直接执行线程的run方法,run方法是在main主线程中执行!
//子线程中设置的阻塞代码仍然会被执行,或者说子线程中的阻塞代码仍然会起作用的!
//thread.run(); // [main] [INFO ] 16 com.xp.thread.TestRun --- running...
//log.info("do other things..."); //先执行上面的run方法,然后再执行当前代码;
//调用start方法启动一个新的线程,由新的线程去执行run方法
thread.start(); //[t1] [INFO ] 21 com.xp.thread.TestRun --- running...
log.info("do other things..."); //先执行当前代码,然后再执行子线程的run方法
}
}
2>.说明
①.当在main线程中直接调用/执行子线程中的run(),子线程的run()仍在main线程中运行,run()里面的代码执行还是同步的;
②.在main线程中通过线程的start()启动一个新的线程,run()在子线程中运行, run()里面的代码执行是异步的;
3>.小结
①.直接调用run()是在主线程中执行了run,没有启动新的线程;
②.使用start()是启动新的线程,通过新的线程间接执行run中的代码;
4.3.sleep与yield
1>.sleep说明
①.调用sleep会让当前线程从Running(运行)进入Timed Waiting状态(阻塞);
②.其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException;
③.睡眠结束后的线程未必会立刻得到执行,需要重新分配CPU时间片;
④.建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性;
- TimeUnit.单位名称.sleep(long n);
2>.yield说明
①.调用yield会让当前线程从Running运行状态进入Runnable就绪状态,然后调度执行其它线程;
②.具体的实现依赖于操作系统的任务调度器;
- 有的时候即使调用了yield()方法,但是该方法并不会立刻执行!
注意: 任务调度器会把CPU时间片分配给就绪状态(/可运行状态)的线程,但是不会把CPU时间片分配给阻塞状态的线程;
3>.案例: 防止CPU占用100%
在没有利用CPU进行计算时,不要让while(true),for(;;)等死循环空转浪费CPU
,这时可以使用yield或者sleep来让出CPU执行权给其它线程/程序;
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
单核CPU环境下效果更加明显!!
4.4.线程优先级
1>.线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它;
2>.如果CPU比较忙,那么优先级高(数值越大)的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用;
例如:
@Slf4j
public class TestPriority {
public static void main(String[] args) {
//创建一个线程
Thread t1 = new Thread("t1") {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
log.info("线程t1执行任务..." + i);
}
}
};
//创建一个线程
Thread t2 = new Thread("t2") {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
log.info("线程t2执行任务..." + i);
}
}
};
//设置线程优先级(必须是在线程执行start()方法之前设置线程优先级)
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
//启动线程
t1.start();
t2.start();
}
}
4.5.join方法
问题: 为什么需要join?
案例:
@Slf4j
public class TestJoin {
static int r = 0;
public static void main(String[] args) {
test1();
}
public static void test1() {
log.info("主线程开始执行...");
Thread t1 = new Thread(() -> {
log.info("子线程开始执行...");
try {
//子线程执行到这里的时候,会让出CPU执行权,暂停执行
//此时变量r的值还是0
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("子线程执行结束...");
r = 10;
}, "t1");
t1.start();
//子线程交出CPU执行权,暂停执行
//主线程获取CPU时间片继续执行,此时变量r的值还是0
log.info("结果为:{}", r); //0
log.info("主线程执行结束...");
//等到主线程执行完毕,或者CPU时间片用完,子线程继续执行
//此时变量r的值才会变成10
}
}
可以看到控制台并没有输出我们想要的结果!
如何主线程打印变量r的最终结果或者说是子线程计算之后的结果?
①.用sleep行不行?为什么?
- 使用sleep()也可以实现,但是主线程休眠时候不好控制,一般来说要大于子线程休眠时间+子线程业务处理时间;
②.用join,加在t1.start()之后即可!
代码如下
@Slf4j
public class TestJoin {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
public static void test1() throws InterruptedException {
log.info("主线程开始执行...");
Thread t1 = new Thread(() -> {
log.info("子线程开始执行...");
try {
//子线程执行到这里的时候,会让出CPU执行权,暂停执行
//此时变量r的值还是0
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("子线程执行结束...");
r = 10;
}, "t1");
t1.start();
//等待线程t1代码执行完毕,后面(主线程)的代码才能继续执行
t1.join();
//子线程交出CPU执行权,暂停执行
//主线程获取CPU时间片继续执行,此时变量r的值还是0
log.info("结果为:{}", r); //0
log.info("主线程执行结束...");
//等到主线程执行完毕,或者CPU时间片用完,子线程继续执行
//此时变量r的值才会变成10
}
}
4.6.interrupt方法
4.6.1.打断调用了sleep(),wait(),join()方法处于阻塞状态的线程
sleep(),wait(),join()这几个方法都会让线程进入阻塞状态;
打断阻塞状态
的线程,会清空打断状态
,也就是说,通过interrupt()方法打断处于阻塞状态的线程,最终会以抛出异常
的方式清空/还原打断标记,以sleep()方法为例:
@Slf4j
public class TestInterrupt {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
log.info("sleep...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.info("interrupt...");
t1.interrupt(); //处于阻塞状态的线程被打断,抛出异常,线程还可以继续执行后面的代码!
//打断标记默认值为false,当线程调用sleep(),wait(),join()方法处于阻塞状态
//而后又调用了interrupt()方法打断阻塞状态,此时打断标记就变成了true,
//但是sleep(),wait(),join()方法最终会通过抛出异常的方式清空打断标记
//因此打断标记最终的值又恢复到最初的false!!!
log.info("打断标记:{}", t1.isInterrupted()); //false
}
}
4.6.2.打断正常运行状态的线程
打断正常运行
的线程,不会清空打断状态
,也就是说,通过interrupt()打断正在运行的线程时,打断标记不会被清空!!
@Slf4j
public class TestInterrupt2 {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
//让线程一直处于运行状态
while (true) {
//根据打断标记来终止线程的运行
if (Thread.currentThread().isInterrupted()){
log.info("线程被打断,退出运行!");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.info("interrupt...");
//注意:调用interrupt()方法打断正在运行的线程,线程并不会停止,而是继续循环运行!!!
t1.interrupt();
log.info("打断标记:{}",t1.isInterrupted()); //true
}
}
4.6.3.打断park线程
打断park线程
,不会清空打断状态
,即打断标记不会恢复到原始状态;
park()函数是将当前调用线程阻塞,而unpark()函数则是将指定线程唤醒,但是要注意的是如果线程的打断标记为true,那么再执行park()函数是没有效果的;
与Object类的wait/notify机制相比,park/unpark有两个优点:
- 以thread为操作对象更符合阻塞线程的直观定义;
- 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒 一个线程,notifyAll唤醒所有等待的线程),增加了灵活性;
@Slf4j
public class TestInterrupt3 {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
log.info("park...");
//线程阻塞
LockSupport.park();
log.info("unpark...");
log.info("打断状态:{}",Thread.currentThread().isInterrupted());
//此时线程的打断状态为true,那么再执行park()函数是没有效果的!!
//如果想要再次调用park()函数阻塞线程,必须要将线程的打断状态恢复到原始状态(interrupt()/interrupted())
//LockSupport.park();
//log.info("unpark...");
},"t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
4.7.不推荐使用的方法
这些方法已经过时,容易破坏同步代码块,造成线程死锁;
5.主线程和守护线程
1>.默认情况下,Java进程需要等待所有线程都运行结束,Java进程才会结束.有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束/终止;
Java程序中的线程默认都是非守护线程!!!
例如:
@Slf4j
public class TestThread1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true){
if (Thread.currentThread().isInterrupted()){
break;
}
}
log.info("线程结束");
},"t1");
//将t1线程设置为守护线程,等到其他非守护线程执行完毕之后,该守护线程会强制结束/终止!
t1.setDaemon(true);
t1.start();
Thread.sleep(2000);
log.info("程序结束!");
}
}
注意:
①.垃圾回收器线程就是一种守护线程;
②.Tomcat中的Acceptor和Poller线程都是守护线程,因此Tomcat在接收到shutdown命令之后,不会等待他们处理完当前的请求;
6.线程的状态
6.1.五种状态(操作系统层面)
说明:
①.初始状态: 仅是在语言层面创建了线程对象(new,没有调用start()),还未与操作系统线程关联;
②.可运行状态(/就绪状态): 指该线程已经被创建(调用了start(),与操作系统线程关联),可以由CPU调度执行;
③.运行状态: 指获取了CPU时间片处于运行中的状态;
- 当CPU时间片用完,会从[运行状态]转换至[可运行状态],会导致线程的上下文切换;
④.阻塞状态:
- 如果线程中调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入"阻塞状态";(线程会让出CPU的使用权);
- 等BIO操作完毕,会由操作系统唤醒处于"阻塞状态"的线程,线程转换至"可运行状态";
- 与"可运行状态"的区别是,对于处在"阻塞状态"的线程来说只要它们一直不被唤醒,调度器就一直不会考虑调度它们;
⑤.终止状态: 表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态;
6.2.六种状态(Java API层面)
根据Thread.State枚举类
,分为六种状态:
说明:
①.NEW状态: 线程刚被创建,但是还没有调用start()方法;
②.RUNNABLE状态: 当调用了start()方法之后,注意Java API层面的RUNNABLE状态涵盖了操作系统层面的"可运行状态"、“运行状态"和"阻塞状态”(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行的);
③.BLOCKED,WAITING,TIMED_WAITING状态都是Java API层面对"阻塞状态"的细分;
- 在多线程环境下,多个线程竞争锁资源,没有获取到锁的线程就会处于"BLOCKED阻塞状态";
- 在多线程环境下,如果在某个线程中调用了另外一个死循环执行的线程的join()方法,那么该线程就会处于"waiting阻塞状态";
④.TERMINATED状态: 当线程代码运行结束;
注意:
在开发过程中,在开发工具中进行调试的时候,开发工具中显示的线程状态和上面提到的线程状态名称可能并不相同(WAITING=WAIT,BLOCKED=MONITOR,RUNNABLE=RUNNING),但是请相信,两者都是正确的!
示例代码
@Slf4j
public class TestThreadStates {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("t1 running...");
}, "t1");
Thread t2 = new Thread(() -> {
while (true) {
}
}, "t2");
t2.start();
Thread t3 = new Thread(() -> {
log.info("running...");
}, "t3");
t3.start();
Thread t4 = new Thread(() -> {
synchronized (TestThreadStates.class) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t4");
t4.start();
Thread t5 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t5");
t5.start();
Thread t6 = new Thread(() -> {
synchronized (TestThreadStates.class){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t6");
t6.start();
Thread.sleep(500);
log.info("t1 state {}",t1.getState());
log.info("t2 state {}",t2.getState());
log.info("t3 state {}",t3.getState());
log.info("t4 state {}",t4.getState());
log.info("t5 state {}",t5.getState());
log.info("t6 state {}",t6.getState());
}
}