并发学习记录02:Java线程
创建和运行线程
方法一,直接使用Thread
@Slf4j(topic = "ch.Test1")
public class Test01 {
public static void main(String[] args) {
//创建线程子类
Thread t = new Thread(){
//重写任务方法,run方法中写要执行的任务
@Override
public void run() {
log.debug("running");
}
};
//线程命名
t.setName("t1");
t.start();
log.debug("running");
}
}
方法二,使用Runnable配合Thread
把线程和任务分开
Thread代表线程
Runnable代表任务,run()方法中是线程要执行的代码
@Slf4j(topic = "ch.Test02")
public class Test02 {
public static void main(String[] args) {
//创建任务对象
Runnable r = () -> log.debug("running");
//创建线程对象,参数1是任务对象,参数2是任务名称
Thread t2 = new Thread(r, "t2");
t2.start();
}
}
Thread和Runnable之间的关系
方法1是把线程和任务合并在了一起,方法2是线程和任务分开了
用Runnable更容易与线程池等API配合
用Runnable让任务类脱离了Thread继承体系,更灵活
Java中有组合大于继承的设计传统,使用Runnable相当于是用一个任务对象去和线程对象进行组合,可扩展性更高。
方法三,FutureTask配合Thread
FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况
@Slf4j(topic = "ch.Test03")
public class Test03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("call");
Thread.sleep(3000);
return 55;
}
});
Thread t = new Thread(task,"t1");
t.start();
log.debug("返回结果是{}",task.get());
}
}
查看进程线程的方法
Windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
Linux
- ps -fe 查看所有进程
- ps -fT -p
查看某个进程的所有线程 - kill 杀死进程
- top 按大写H切换是否显示线程
- top -H -p
查看某个进程的所有线程
Java
jps 查看所有Java进程
jstack
jconsole 来查看某个Java进程中线程运行情况(图形界面)
jconsole 远程监控配置
在远程主机上面运行你的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类
修改/etc/hosts文件将127.0.0.1映射到主机名
原理之线程运行
栈与栈帧
JVM中由堆,栈,方法区组成,其中栈内存就是给线程用的,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
线程上下文切换(Thread Context Switch)
因为下面一些原因,导致CPU不再执行当前的线程,转而执行另一个线程的代码
线程的CPU时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
当线程上下文切换发生时,需要操作系统保持当前线程的状态,恢复另一个线程的状态
状态包括程序计数器,虚拟机栈中每个栈帧的信息,如局部变量,操作数栈和返回地址等
线程上下文切换频繁会影响性能
程序计数器的作用是记住下一条JVM指令的执行地址,它是线程私有的。
线程常见方法
方法名 | static | 功能说明 | 注意事项 |
---|---|---|---|
start() | 启动一个新线程,在新的线程中允许run方法中的代码 | start方法只是让线程进入就绪状态,里面的代码不一定立刻运行(因为还没得到CPU的时间片)。每个线程对象的start方法只能调用一次,如果调用了多次会出现异常 | |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 |
start与run
利用一个demo测试run和start:
@Slf4j(topic = "ch.TestStart")
public class TestStart {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("Thread name is {}",Thread.currentThread().getName());
FileReader.read(Constants.TEST_ADDRESS);
}
};
//run
// t1.run();
//start
t1.start();
log.debug("running");
}
}
执行可以发现,当调用run方法时,其实也只是在主线程中执行了Thread的run方法,没有启动新的线程
而使用start是启动新的线程,通过新的线程去执行Thread中的run方法
sleep和yield
sleep
- 调用sleep会让当前线程从running编程Timed Waiting状态,进入阻塞
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时候sleep方法会抛出异常
- 睡眠结束后的线程未必会立刻执行
- 建议用TimeUnit的sleep代替Thread的sleep,因为可读性更好
- sleep方法是哪个线程调用,哪个线程睡眠
yield
- 调用yield方法会让当前线程从running状态进入Runnable就绪状态,然后调度执行其他线程
- 具体的实现其实还是要依赖操作系统的任务调度器
线程优先级
- 线程优先级可以提示调度器优先调度该线程,一般优先级越高,该线程可能得到的时间片就越多
- 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用
join方法详解
为什么需要join
`@Slf4j(topic = "ch.TestJoin")
public class TestJoin {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test01();
}
public static void test01() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(){
@Override
public void run() {
log.debug(Thread.currentThread().getName() + "开始");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r++;
log.debug(Thread.currentThread().getName() + "结束");
}
};
t1.start();
//如果想加1,那么就得等t1线程执行结束
t1.join();
log.debug("结果为{}",r);
log.debug("结束");
}
}`
由于主线程和t1线程是并行的,而t1线程需要sleep1秒才会计算r++
如果主线程不等t1线程执行完成的话,就会输出r = 0
如果主线等t1线程执行完成的话,就会输出r = 1
这里遗留一个小问题:join底层用了wait,wait和sleep有什么区别?是否能相互代替?
有时效的join
t.join(long millis):等待t1线程执行,如果超过时间t1还没执行完成,就放弃等待直接运行。
如下:
@Slf4j(topic = "ch.TestJoinLimit")
public class TestJoinLimit {
static int r1 = 0;
public static void main(String[] args) throws InterruptedException {
// test03();
test04();
}
public static void test03() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 20;
}, "t1");
long start = System.currentTimeMillis();
t1.start();
//实际等了1s就等到
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1 = {},time gap is {}", r1, end - start);
}
public static void test04() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 20;
}, "t1");
long start = System.currentTimeMillis();
t1.start();
//等了1.5s没等到,直接执行了
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1 = {},time gap is {}", r1, end - start);
}
}
interrupt方法
interrupt方法可以打断sleep,wait,join的线程
这几个方法都会让线程进入阻塞状态
打断sleep的线程,会清空打断状态,以sleep为例
@Slf4j(topic = "ch.TestInterrupt01")
public class TestInterrupt01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(1);
t1.interrupt();
//打断状态应该是FALSE,因为打断sleep线程,会清空打断状态
log.debug("打断状态为{}",t1.isInterrupted());
}
}
当然我们也可以用interrupt方法打断正常运行的线程,不会清空打断的状态
@Slf4j(topic = "ch.TestInterrupt02")
public class TestInterrupt02 {
//尝试打断正常运行的线程
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
log.debug("t1运行中");
Thread cur = Thread.currentThread();
boolean interrupted = cur.isInterrupted();
if (interrupted) {
log.debug("被打断");
break;
}
}
}, "t1");
t1.start();
t1.interrupt();
log.debug("t1线程的状态是{}",t1.isInterrupted());
}
}
多线程设计模式之两阶段终止
假如t1和t2并发执行,我们需要在线程t1中终止线程t2,我们该如何做呢?
首先我们可以想到使用线程对象的stop()方法停止线程,但是stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么它被杀死后就再也没有机会释放锁,其他线程将永远无法获取这个锁
还有一种是System.exit()方法停止线程,但是这种方法会让整个程序都停止
理想的办法就是用两阶段终止,用一个线程间隔一段时间去监视isInterrupted的状态,如果正常运行时被打断,isInterrupted会被置为true,如果睡眠时期被打断,也会抛出异常。
@Slf4j(topic = "ch.InterruptPattern")
public class InterruptPattern {
public static void main(String[] args) throws InterruptedException {
InTestInterrupt testInterrupt = new InTestInterrupt();
testInterrupt.start();
Thread.sleep(3500);
log.debug("stop");
testInterrupt.stop();
}
}
@Slf4j(topic = "ch.TestInterrupt")
class InTestInterrupt {
private Thread thread;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread cur = Thread.currentThread();
//被打断的情况
if (cur.isInterrupted()) {
log.debug("优雅关闭线程");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//捕捉到异常说明
cur.interrupt();
e.printStackTrace();
}
}
}, "t1");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
打断park线程,park的作用也是让当前线程停下
打断park线程,不会清空打断状态
如果打断标记已经是TRUE,则park会失效
可能破坏同步代码块,造成线程死锁的方法
方法名 | 功能说明 | 原因 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起线程运行 | |
resume() | 恢复线程运行 |
主线程与守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束,有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,守护线程也会强行结束。
@Slf4j(topic = "ch.TestShouhuThread")
public class TestShouhuThread {
public static void main(String[] args) throws InterruptedException {
log.debug("main begin");
Thread t1 = new Thread(() -> {
log.debug("t1 begin");
try {
//按道理是要休眠20s,才结束
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 end");
}, "t1");
//设置为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1);
//守护线程会在其他线程都结束时结束
log.debug("main end");
}
}
线程的五种状态
初始状态:语言层面创建了线程对象,但是还未与操作系统的线程关联
可运行状态:也可以叫就绪状态,该线程已经被创建且与操作系统线程关联,可以由CPU调度执行
运行状态:获取了CPU时间片,线程在运行中的状态,如果CPU的时间片用完,线程会从运行状态转换为就绪状态,这时候会有一个线程上下文的切换
阻塞状态:一般是等待系统非CPU资源。比如调用了读写文件的接口,线程就暂时不会用到CPU,会导致线程上下文切换,进入阻塞状态,等到读写操作完毕,操作系统会唤醒阻塞的线程,转换到可运行状态。阻塞状态的线程要先完成等待非CPU资源做的事,变成就绪状态,才会有机会得到CPU的调度
终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态
Java中线程的六种状态
这是根据Thread.State枚举类型变量来分的。
- NEW:线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- TERMINATED 当线程代码运行结束
扩展习题之华罗庚的烧水泡茶
任务就是想喝茶,但是需要洗水壶,烧开水,洗茶壶,洗茶杯,拿茶叶之后才能泡茶
@Slf4j(topic = "ch.MakeTea")
public class MakeTea {
public static void main(String[] args) {
makeTea01();
}
public static void makeTea01() {
Thread t1 = new Thread(() -> {
log.debug("洗水壶");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("烧开水");
try {
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("洗茶壶");
log.debug("洗茶杯");
log.debug("拿茶叶");
try {
Thread.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("泡茶");
}, "t2");
t1.start();
t2.start();
}
}
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现