并发编程基础
进程:进程可以看作是程序的实例,大部分程序可以同时运行多个实例进程 比如记事本,画图,浏览器,也有些只能启动一个进程实例 比如 电脑管家,360等。
线程:一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行,线程是存在于进程内的,一个进程内可以有一到多个线程;
并行:同一时间同时做多件事情,称为并行;
并发:同一时刻,线程轮流使用CPU的做法,同一时间应对多件事情,称为并发;
一,创建线程的方法:
1.直接new Thread(){}; 通过匿名内部类实现;
@Slf4j
public class ThreadExample1 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
log.info("thread running");
}
};
// 启动线程
t.start();
}
}
2.把线程和任务分开:Runnable放线程执行的代码;
@Slf4j
public class ThreadExample2 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
log.info("runnable running");
}
};
// 开启线程
new Thread(runnable).start();
// lambda 简化 Runnable
Runnable runnable2 = () -> log.info("runnable running");
new Thread(() ->log.info("runnable running")).start();
}
}
3.FutureTask 配合Thread;FutureTask 提供了返回值;取返回值会阻塞等待
@Slf4j
public class ThreadExample3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 参数为callable接口
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("task is running");
return 10;
}
});
// 开启线程
new Thread(task).start();
// 可获取的返回结果
log.info("return value:{}",task.get());
}
}
二,查看进程线程的方法:
windows: 通过任务管理器 查看pid,
tasklist查看进程,
通过taskkill /F /PID pid号 杀死进程
Linux:ps-fe 查看所有进程;
ps -fe | grep java(java为查找的关键字)查看有关键字的进程;
ps -fT -p <PID> 查看某个进程(PID)的所以线程;
kill 杀死进程 ;
top 按大写H切换是否显示线程;
top -H -p <PID> 查看某个进程(PID)的所有 线程
Java: jps查看所有java进程;
jstack <PID> 查看某个java进程(PID)的所有线程状态;
jconsole 查看某个java进程中某个线程的运行情况(图形界面)
三,栈帧:栈内存是给线程使用,每个线程启动后,虚拟机就会为其分配一块栈内存;每个栈由多个栈帧(Frames)组成, 对应着每次方法调用时所占用的内存;每个线程只能有一个活动栈帧,对应当前正在执行的方法;按照先进后 出的顺序,生命周期随着方法的调用完毕而结束;
四,上下文切换:多个线程在执行时,会有一些一下原因导致cpu不再执行当前的线程转而执行另一个线程的代码;
线程的cpu时间片用完;垃圾回收;有更高优先级的线程需运行;线程自己调用 sleep,yield,wait,join, park,synchronized,lock等方法;
当上下文切换时(Context Switch),需要由操作系统保存当前线程的状态,并恢复另外一个线程的状态,java 中对应概念就是程序计数器,它纪录着线程执行代码行号的地址指令,且它是线程私有的;频繁切换上下问会 影响性能;
线程状态包括计数器,虚拟机中中每个栈帧信息,如局部变量,操作数栈,返回地址等。
五,一些常用方法:
sleep:
1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
3. 睡眠结束后的线程未必会立刻得到执行
4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield:
1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
2. 具体的实现依赖于操作系统的任务调度器
intrerept:
上面方法说了,打断(阻塞)sleep,wait,join会抛异常,并且把打断标记清空,也就是interrupted的值为false;
在正常打断的情况下则不会抛异常,且打断标记不会清空,但是线程不会停下来,需要手动停止;
@Slf4j
public class ThreadExample5 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
// 判断线程是否被打断
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
log.info("t1线程被打断,退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.info("main休眠结束,打断线程");
t1.interrupt();
}
}
六,设计模式-两阶段终止
使用stop()方法停止线程的弊端:stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当他被杀死后就没有机会释放锁,导致其他线程无法获取锁;
/**
* 两阶段终止模式 演示
*/
@Slf4j
public class ThreadExample6 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination termination = new TwoPhaseTermination();
termination.strat();
// 主线程睡眠两秒
Thread.sleep(3000);
// 休眠结束后 手动停止线程
termination.stop();
}
}
@Slf4j
class TwoPhaseTermination{
private Thread thread;
// 启动监控线程
public void strat(){
thread=new Thread(()->{
Thread innertThread = Thread.currentThread();
while (true){
// 是否被打断
if (innertThread.isInterrupted()){
log.info("线程被打断,退出监控-记录日志");
break;
}
try {
Thread.sleep(1000);
log.info("正常记录日志操作");
} catch (InterruptedException e) {
e.printStackTrace();
// 当阻塞时被打断,打断状态会被清空,重新打断
innertThread.interrupt();
}
}
});
thread.start();
}
// 手动 停止监控线程
public void stop(){
thread.interrupt();
}
}
七:LockSupport.park()方法:会让当前线程停下来,只有打断标记为false的时候才生效;
/**
* park打断
*/
@Slf4j
public class ThreadExample7 {
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("park..");
// park阻塞
LockSupport.park();
log.info("uppack..");
log.info("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
Thread.sleep(2000);
// 打断线程,将打断标记设为true,并影响park
t1.interrupt();
}
}
在调用了LockSupport.park()方法后,线程就不会往下执行了(阻塞),上面代码中主线程2秒后使用interrupt将线程打断,并将打断标记设置为true后,线程才得以继续往下运行;一旦打断标记为true后,再调用LockSupport.park()方法也不会起作用;之前说过interrupt打断sleep,join,wait会抛出异常,但是这个park不会;
Thread.interrupted()方法使用后,会清除打断标记,也就是设为false;
@Slf4j
public class ThreadExample7 {
public static void main(String[] args) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("park..");
// park阻塞
LockSupport.park();
log.info("uppack..");
// 查看打断状态,并清除打断标记
log.info("打断状态:{}", Thread.interrupted());
log.info("二次park..");
LockSupport.park();
log.info("二次park后...");
}, "t1");
t1.start();
Thread.sleep(2000);
// 打断线程,将打断标记设为true,并影响park
t1.interrupt();
}
}
此时就是处于这样一种阻塞状态;
八;不推荐使用的过时方法;这些方法容易造成线程死锁;
stop():停止线程运行;可以用两阶段终止代替;
supend():暂停线程运行
resume():恢复线程运行
九:守护线程,Java进程需要等待所有线程运行结束后,才会结束;而守护线程,只有其他的非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束;
setDaemon(true)则是设置为守护线程的方法;
@Slf4j
public class ThreadExample8 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("thread start...");
while (true){
if (Thread.currentThread().isInterrupted())
break;
}
log.info("thread end...");
}, "t1");
t1.start();
Thread.sleep(1000);
log.info("main end..");
}
}
可以看到t1线程一直在循环,main线程结束,t1还没有结束,导致java进程一直在等待线程结束;
现在我们给t1线程设置为守护线程,那么在main线程运行结束后,t1线程也会被强制结束;
main线程执行1秒后结束,守护线程也结束了
常见的应用场景:垃圾回收器线程也是一种守护线程;
Tomcat中的Acceptor和Poller线程都是守护线程,在tomcat使用shutdown时,会直接带着他们一起结束;
十:线程状态
①,操作系统层面:分为五种状态
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
【运行状态】指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导 致线程的上下文切换
【阻塞状态】如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】,等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】,
与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑
调度它们
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
②,Java Api层面:根据Thread.State枚举,分为六种状态
NEW,线程刚被创建,但还未调用start()放
RUNNABLE,当调用了start()方法后,Java Api 层面的RUNNABLE涵盖了操作系统层面的【可运行状态】,【运行状态】,【阻塞状态】(BIO导致的线程阻塞,java里无法区分,也是可运行状态)
BOLCK,WAITING,TINE_WAITING都是Java Api层面对阻塞状态的细分
TERMINATED,线程代码运行结束
下面演示一下六种状态:
/**
* 6种线程状态
*/
@Slf4j
public class ThreadExample9 {
public static void main(String[] args) throws InterruptedException {
// 演示 NEW
Thread t1 = new Thread(() -> {
}, "t1");
//演示 RUNNABLE
Thread t2 = new Thread(() -> {
while (true) {
}
}, "t2");
t2.start();
//演示 TIMED_WAITING
Thread t5 = new Thread(() -> {
synchronized (ThreadExample9.class) {
try {
// 休眠1000秒
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t5");
t5.start();
//演示 BLOCKED
Thread t3 = new Thread(() -> {
synchronized (ThreadExample9.class) {
}
}, "t3");
t3.start();
//演示 WAITING
Thread t4 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t4");
t4.start();
//演示 TERMINATED
Thread t6 = new Thread(() -> {
}, "t6");
t6.start();
Thread.sleep(1000);
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());
}
}
可以看到结果,现在逐一分析一下:
t1线程因为没有调用start()方法,所以是NEW状态;
t2线程属于while true 无线循环,不会结束线程,所以RUNNABL状态
t3线程使用了synchronized对类加锁,作用于这个类内的所有对象,在t3想获取锁前,t5已经获取锁了,而t5休眠1000秒,所以t3需要等待t5把锁释放,所以此时t3的状态是BLOCKED状态
t4线程使用了t2.join()方法,会阻塞,需要等待t2运行结束才能够往下执行,而t2是无线循环则不会结束,所以t4是WAITING状态,这个状态代表等待无时间限制
t5线程休眠了1000秒了,是有时间限制的,所=所以是TIMED_WAITING状态
t6线程则是在main线程休眠结束之前已经执行完毕了,结束了,所以是TERMINATED终止状态
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?